mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
backend compiles
This commit is contained in:
parent
f3f2d547a3
commit
7644bf1834
@ -24,6 +24,7 @@
|
||||
"axios": "^0.21.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"cors": "^2.8.5",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.5.1",
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import Decimal from 'decimal.js-light'
|
||||
dotenv.config()
|
||||
|
||||
// Set precision value
|
||||
// TODO test if this works here
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0027-decimal_types',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
amount: number
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class Balance {
|
||||
constructor(json: any) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decay = Number(json.decay)
|
||||
this.balance = json.balance
|
||||
this.decay = json.decay
|
||||
this.decayDate = json.decay_date
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
@Field(() => Decimal)
|
||||
decay: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { Decay } from './Decay'
|
||||
|
||||
@ -12,19 +13,19 @@ import { Decay } from './Decay'
|
||||
export class Transaction {
|
||||
constructor() {
|
||||
this.type = ''
|
||||
this.balance = 0
|
||||
this.totalBalance = 0
|
||||
this.balance = new Decimal(0)
|
||||
this.totalBalance = new Decimal(0)
|
||||
this.memo = ''
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
type: string
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
totalBalance: number
|
||||
@Field(() => Decimal)
|
||||
totalBalance: Decimal
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayStart?: string
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@ -8,9 +9,8 @@ export class TransactionList {
|
||||
constructor() {
|
||||
this.gdtSum = 0
|
||||
this.count = 0
|
||||
this.balance = 0
|
||||
this.decay = 0
|
||||
this.decayDate = ''
|
||||
this.balance = new Decimal(0)
|
||||
this.decayStartBlock = null
|
||||
}
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@ -20,13 +20,10 @@ export class TransactionList {
|
||||
count: number
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
@Field(() => Date, { nullable: true })
|
||||
decayStartBlock: Date | null
|
||||
|
||||
@Field(() => [Transaction])
|
||||
transactions: Transaction[]
|
||||
|
||||
@ -27,7 +27,7 @@ import { hasElopageBuys } from '../../util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { Balance } from '@entity/Balance'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
@ -306,35 +306,28 @@ export class AdminResolver {
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||
|
||||
let newBalance = 0
|
||||
let newBalance = new Decimal(0)
|
||||
if (lastUserTransaction) {
|
||||
newBalance = calculateDecay(
|
||||
Number(lastUserTransaction.balance),
|
||||
lastUserTransaction.balance,
|
||||
lastUserTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
).balance
|
||||
}
|
||||
newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()))
|
||||
// TODO pending creations decimal
|
||||
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)))
|
||||
|
||||
const transaction = new Transaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = pendingCreation.memo
|
||||
transaction.userId = pendingCreation.userId
|
||||
transaction.amount = BigInt(parseInt(pendingCreation.amount.toString()))
|
||||
// TODO pending creations decimal
|
||||
transaction.amount = new Decimal(Number(pendingCreation.amount))
|
||||
transaction.creationDate = pendingCreation.date
|
||||
transaction.balance = BigInt(newBalance)
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
await transaction.save()
|
||||
|
||||
let userBalance = await Balance.findOne({ userId: pendingCreation.userId })
|
||||
if (!userBalance) {
|
||||
userBalance = new Balance()
|
||||
userBalance.userId = pendingCreation.userId
|
||||
}
|
||||
userBalance.amount = Number(newBalance)
|
||||
userBalance.modified = receivedCallDate
|
||||
userBalance.recordDate = receivedCallDate
|
||||
await userBalance.save()
|
||||
await AdminPendingCreation.delete(pendingCreation)
|
||||
|
||||
return true
|
||||
|
||||
@ -6,9 +6,9 @@ import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { Balance } from '../model/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@Resolver()
|
||||
export class BalanceResolver {
|
||||
@ -18,24 +18,26 @@ export class BalanceResolver {
|
||||
// load user and balance
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const balanceEntity = await dbBalance.findOne({ userId: userEntity.id })
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const now = new Date()
|
||||
|
||||
const lastTransaction = await Transaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
)
|
||||
|
||||
// No balance found
|
||||
if (!balanceEntity) {
|
||||
if (!lastTransaction) {
|
||||
return new Balance({
|
||||
balance: 0,
|
||||
decay: 0,
|
||||
balance: new Decimal(0),
|
||||
decay: new Decimal(0),
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
return new Balance({
|
||||
balance: roundFloorFrom4(balanceEntity.amount),
|
||||
decay: roundFloorFrom4(
|
||||
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
|
||||
),
|
||||
balance: lastTransaction.balance,
|
||||
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, getConnection, QueryRunner } from '@dbTools/typeorm'
|
||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||
|
||||
import CONFIG from '../../config'
|
||||
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
|
||||
@ -20,68 +20,16 @@ import { Order } from '../enum/Order'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { User as dbUser, User } from '@entity/User'
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
|
||||
import { apiPost } from '../../apis/HttpRequest'
|
||||
import { roundFloorFrom4, roundCeilFrom4 } from '../../util/round'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
||||
import { calculateBalance, isHexPublicKey } from '../../util/validate'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
|
||||
// helper helper function
|
||||
async function updateStateBalance(
|
||||
user: dbUser,
|
||||
balance: number,
|
||||
received: Date,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<dbBalance> {
|
||||
let userBalance = await dbBalance.findOne({ userId: user.id })
|
||||
if (!userBalance) {
|
||||
userBalance = new dbBalance()
|
||||
userBalance.userId = user.id
|
||||
userBalance.amount = balance
|
||||
userBalance.modified = received
|
||||
} else {
|
||||
userBalance.amount = balance
|
||||
userBalance.modified = new Date()
|
||||
}
|
||||
if (userBalance.amount <= 0) {
|
||||
throw new Error('error new balance <= 0')
|
||||
}
|
||||
userBalance.recordDate = received
|
||||
return queryRunner.manager.save(userBalance).catch((error) => {
|
||||
throw new Error('error saving balance:' + error)
|
||||
})
|
||||
}
|
||||
|
||||
async function calculateNewBalance(
|
||||
userId: number,
|
||||
transactionDate: Date,
|
||||
centAmount: number,
|
||||
): Promise<number> {
|
||||
let newBalance = centAmount
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastUserTransaction = await transactionRepository.findLastForUser(userId)
|
||||
if (lastUserTransaction) {
|
||||
newBalance += Number(
|
||||
calculateDecay(
|
||||
Number(lastUserTransaction.balance),
|
||||
lastUserTransaction.balanceDate,
|
||||
transactionDate,
|
||||
).balance,
|
||||
)
|
||||
}
|
||||
|
||||
if (newBalance <= 0) {
|
||||
throw new Error('error new balance <= 0')
|
||||
}
|
||||
|
||||
return newBalance
|
||||
}
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@Authorized([RIGHTS.TRANSACTION_LIST])
|
||||
@ -97,21 +45,28 @@ export class TransactionResolver {
|
||||
}: Paginated,
|
||||
@Ctx() context: any,
|
||||
): Promise<TransactionList> {
|
||||
// load user
|
||||
// find user
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
// TODO: separate those usecases - this is a security issue
|
||||
const user = userId
|
||||
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
|
||||
: await userRepository.findByPubkeyHex(context.pubKey)
|
||||
let limit = pageSize
|
||||
let offset = 0
|
||||
let skipFirstTransaction = false
|
||||
if (currentPage > 1) {
|
||||
offset = (currentPage - 1) * pageSize - 1
|
||||
limit++
|
||||
}
|
||||
if (offset && order === Order.ASC) {
|
||||
offset--
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
)
|
||||
|
||||
if (!lastTransaction) {
|
||||
// TODO Have proper return type here
|
||||
throw new Error('User has no transactions')
|
||||
}
|
||||
|
||||
// find transactions
|
||||
const limit = currentPage === 1 && order === Order.DESC ? pageSize - 1 : pageSize
|
||||
const offset =
|
||||
currentPage === 1 ? 0 : (currentPage - 1) * pageSize - (order === Order.DESC ? 1 : 0)
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
||||
user.id,
|
||||
@ -120,66 +75,52 @@ export class TransactionResolver {
|
||||
order,
|
||||
onlyCreations,
|
||||
)
|
||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
||||
const decay = !(currentPage > 1)
|
||||
const transactions: Transaction[] = []
|
||||
if (userTransactions.length) {
|
||||
if (order === Order.DESC) {
|
||||
userTransactions.reverse()
|
||||
|
||||
// find involved users
|
||||
let involvedUserIds: number[] = []
|
||||
userTransactions.forEach((transaction: dbTransaction) => {
|
||||
involvedUserIds.push(transaction.userId)
|
||||
if (transaction.linkedUserId) {
|
||||
involvedUserIds.push(transaction.linkedUserId)
|
||||
}
|
||||
const involvedUserIds: number[] = []
|
||||
})
|
||||
// remove duplicates
|
||||
involvedUserIds = involvedUserIds.filter((value, index, self) => self.indexOf(value) === index)
|
||||
// We need to show the name for deleted users for old transactions
|
||||
const involvedUsers = await User.createQueryBuilder()
|
||||
.withDeleted()
|
||||
.where('user.id IN (:...userIds)', { involvedUserIds })
|
||||
.getMany()
|
||||
|
||||
userTransactions.forEach((transaction: dbTransaction) => {
|
||||
involvedUserIds.push(transaction.userId)
|
||||
if (
|
||||
transaction.typeId === TransactionTypeId.SEND ||
|
||||
transaction.typeId === TransactionTypeId.RECEIVE
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
involvedUserIds.push(transaction.linkedUserId!) // TODO ensure not null properly
|
||||
}
|
||||
})
|
||||
// remove duplicates
|
||||
// https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates
|
||||
const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique)
|
||||
const transactions: Transaction[] = []
|
||||
|
||||
// decay transaction
|
||||
if (currentPage === 1 && order === Order.DESC) {
|
||||
const now = new Date()
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||
const balance = decay.balance.minus(lastTransaction.balance)
|
||||
|
||||
const decayTransaction = new Transaction()
|
||||
decayTransaction.type = 'decay'
|
||||
decayTransaction.balance = balance
|
||||
// TODO
|
||||
// decayTransaction.decayDuration = decay.duration
|
||||
// decayTransaction.decayStart = decay.start
|
||||
// decayTransaction.decayEnd = decay.end
|
||||
transactions.push(decayTransaction)
|
||||
}
|
||||
|
||||
if (userTransactions.length) {
|
||||
for (let i = 0; i < userTransactions.length; i++) {
|
||||
const userTransaction = userTransactions[i]
|
||||
const finalTransaction = new Transaction()
|
||||
finalTransaction.transactionId = userTransaction.id
|
||||
finalTransaction.date = userTransaction.balanceDate.toISOString()
|
||||
finalTransaction.memo = userTransaction.memo
|
||||
finalTransaction.totalBalance = roundFloorFrom4(Number(userTransaction.balance))
|
||||
const previousTransaction = i > 0 ? userTransactions[i - 1] : null
|
||||
finalTransaction.totalBalance = userTransaction.balance
|
||||
finalTransaction.balance = userTransaction.amount
|
||||
|
||||
if (previousTransaction) {
|
||||
const currentTransaction = userTransaction
|
||||
const decay = calculateDecay(
|
||||
Number(previousTransaction.balance),
|
||||
previousTransaction.balanceDate,
|
||||
currentTransaction.balanceDate,
|
||||
)
|
||||
const balance = Number(previousTransaction.balance) - decay.balance
|
||||
|
||||
if (CONFIG.DECAY_START_TIME < currentTransaction.balanceDate) {
|
||||
finalTransaction.decay = decay
|
||||
finalTransaction.decay.balance = roundFloorFrom4(balance)
|
||||
if (
|
||||
previousTransaction.balanceDate < CONFIG.DECAY_START_TIME &&
|
||||
currentTransaction.balanceDate > CONFIG.DECAY_START_TIME
|
||||
) {
|
||||
finalTransaction.decay.decayStartBlock = (
|
||||
CONFIG.DECAY_START_TIME.getTime() / 1000
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalTransaction.balance = roundFloorFrom4(Number(userTransaction.amount)) // Todo unsafe conversion
|
||||
|
||||
const otherUser = userIndiced.find((u) => u.id === userTransaction.linkedUserId)
|
||||
const otherUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
|
||||
switch (userTransaction.typeId) {
|
||||
case TransactionTypeId.CREATION:
|
||||
finalTransaction.name = 'Gradido Akademie'
|
||||
@ -202,31 +143,7 @@ export class TransactionResolver {
|
||||
default:
|
||||
throw new Error('invalid transaction')
|
||||
}
|
||||
if (i > 0 || !skipFirstTransaction) {
|
||||
transactions.push(finalTransaction)
|
||||
}
|
||||
|
||||
if (i === userTransactions.length - 1 && decay) {
|
||||
const now = new Date()
|
||||
const decay = calculateDecay(
|
||||
Number(userTransaction.balance),
|
||||
userTransaction.balanceDate,
|
||||
now,
|
||||
)
|
||||
const balance = Number(userTransaction.balance) - decay.balance
|
||||
|
||||
const decayTransaction = new Transaction()
|
||||
decayTransaction.type = 'decay'
|
||||
decayTransaction.balance = roundCeilFrom4(balance)
|
||||
decayTransaction.decayDuration = decay.decayDuration
|
||||
decayTransaction.decayStart = decay.decayStart
|
||||
decayTransaction.decayEnd = decay.decayEnd
|
||||
transactions.push(decayTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (order === Order.DESC) {
|
||||
transactions.reverse()
|
||||
transactions.push(finalTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,16 +161,12 @@ export class TransactionResolver {
|
||||
} catch (err: any) {}
|
||||
|
||||
// get balance
|
||||
const balanceEntity = await dbBalance.findOne({ userId: user.id })
|
||||
if (balanceEntity) {
|
||||
const now = new Date()
|
||||
transactionList.balance = roundFloorFrom4(balanceEntity.amount)
|
||||
// TODO: Add a decay object here instead of static data representing the decay.
|
||||
transactionList.decay = roundFloorFrom4(
|
||||
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
|
||||
)
|
||||
transactionList.decayDate = now.toString()
|
||||
}
|
||||
transactionList.balance = lastTransaction.balance
|
||||
transactionList.decayStartBlock = CONFIG.DECAY_START_TIME
|
||||
// const now = new Date()
|
||||
// TODO this seems duplicated
|
||||
// transactionList.decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||
// transactionList.decayDate = now.toString()
|
||||
|
||||
return transactionList
|
||||
}
|
||||
@ -271,7 +184,9 @@ export class TransactionResolver {
|
||||
throw new Error('invalid sender public key')
|
||||
}
|
||||
// validate amount
|
||||
if (!hasUserAmount(senderUser, amount)) {
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
@ -287,24 +202,19 @@ export class TransactionResolver {
|
||||
throw new Error('invalid recipient public key')
|
||||
}
|
||||
|
||||
const centAmount = Math.round(amount * 10000)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
try {
|
||||
const receivedCallDate = new Date()
|
||||
// transaction
|
||||
const transactionSend = new dbTransaction()
|
||||
transactionSend.typeId = TransactionTypeId.SEND
|
||||
transactionSend.memo = memo
|
||||
transactionSend.userId = senderUser.id
|
||||
transactionSend.linkedUserId = recipientUser.id
|
||||
transactionSend.amount = BigInt(centAmount)
|
||||
const sendBalance = await calculateNewBalance(senderUser.id, receivedCallDate, -centAmount)
|
||||
transactionSend.balance = BigInt(Math.trunc(sendBalance))
|
||||
transactionSend.amount = amount
|
||||
transactionSend.balance = sendBalance
|
||||
transactionSend.balanceDate = receivedCallDate
|
||||
transactionSend.sendSenderFinalBalance = transactionSend.balance
|
||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||
|
||||
const transactionReceive = new dbTransaction()
|
||||
@ -312,15 +222,13 @@ export class TransactionResolver {
|
||||
transactionReceive.memo = memo
|
||||
transactionReceive.userId = recipientUser.id
|
||||
transactionReceive.linkedUserId = senderUser.id
|
||||
transactionReceive.amount = BigInt(centAmount)
|
||||
const receiveBalance = await calculateNewBalance(
|
||||
recipientUser.id,
|
||||
receivedCallDate,
|
||||
centAmount,
|
||||
)
|
||||
transactionReceive.balance = BigInt(Math.trunc(receiveBalance))
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
|
||||
if (!receiveBalance) {
|
||||
throw new Error('Sender user account corrupted')
|
||||
}
|
||||
transactionReceive.balance = receiveBalance
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.sendSenderFinalBalance = transactionSend.balance
|
||||
transactionReceive.linkedTransactionId = transactionSend.id
|
||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||
|
||||
@ -328,17 +236,6 @@ export class TransactionResolver {
|
||||
transactionSend.linkedTransactionId = transactionReceive.id
|
||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||
|
||||
// Update Balance sender
|
||||
await updateStateBalance(senderUser, Math.trunc(sendBalance), receivedCallDate, queryRunner)
|
||||
|
||||
// Update Balance recipient
|
||||
await updateStateBalance(
|
||||
recipientUser,
|
||||
Math.trunc(receiveBalance),
|
||||
receivedCallDate,
|
||||
queryRunner,
|
||||
)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
@ -16,7 +17,7 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
recipientFirstName: 'Peter',
|
||||
recipientLastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
amount: 42.0,
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionReceived } from './text/transactionReceived'
|
||||
|
||||
@ -7,7 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
amount: number
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const transactionReceived = {
|
||||
de: {
|
||||
subject: 'Gradido Überweisung',
|
||||
@ -7,7 +9,7 @@ export const transactionReceived = {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
amount: number
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
|
||||
@ -9,14 +9,6 @@ export class UserRepository extends Repository<User> {
|
||||
.getOneOrFail()
|
||||
}
|
||||
|
||||
async getUsersIndiced(userIds: number[]): Promise<User[]> {
|
||||
return this.createQueryBuilder('user')
|
||||
.withDeleted() // We need to show the name for deleted users for old transactions
|
||||
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
|
||||
.where('user.id IN (:...userIds)', { userIds })
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async findBySearchCriteriaPagedFiltered(
|
||||
select: string[],
|
||||
searchCriteria: string,
|
||||
|
||||
@ -1,29 +1,30 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import 'reflect-metadata' // This might be wise to load in a test setup file
|
||||
import { decayFormula, calculateDecay } from './decay'
|
||||
|
||||
describe('utils/decay', () => {
|
||||
describe('decayFormula', () => {
|
||||
it('has base 0.99999997802044727', () => {
|
||||
const amount = 1.0
|
||||
const amount = new Decimal(1.0)
|
||||
const seconds = 1
|
||||
expect(decayFormula(amount, seconds)).toBe(0.99999997802044727)
|
||||
})
|
||||
// Not sure if the following skiped tests make sence!?
|
||||
it('has negative decay?', async () => {
|
||||
const amount = -1.0
|
||||
const amount = new Decimal(1.0)
|
||||
const seconds = 1
|
||||
expect(await decayFormula(amount, seconds)).toBe(-0.99999997802044727)
|
||||
expect(decayFormula(amount, seconds)).toBe(-0.99999997802044727)
|
||||
})
|
||||
it('has correct backward calculation', async () => {
|
||||
const amount = 1.0
|
||||
const amount = new Decimal(1.0)
|
||||
const seconds = -1
|
||||
expect(await decayFormula(amount, seconds)).toBe(1.0000000219795533)
|
||||
expect(decayFormula(amount, seconds)).toBe(1.0000000219795533)
|
||||
})
|
||||
// not possible, nodejs hasn't enough accuracy
|
||||
it('has correct forward calculation', async () => {
|
||||
const amount = 1.0 / 0.99999997802044727
|
||||
const amount = new Decimal(1.0).div(0.99999997802044727)
|
||||
const seconds = 1
|
||||
expect(await decayFormula(amount, seconds)).toBe(1.0)
|
||||
expect(decayFormula(amount, seconds)).toBe(1.0)
|
||||
})
|
||||
})
|
||||
it.skip('has base 0.99999997802044727', async () => {
|
||||
@ -31,11 +32,11 @@ describe('utils/decay', () => {
|
||||
now.setSeconds(1)
|
||||
const oneSecondAgo = new Date(now.getTime())
|
||||
oneSecondAgo.setSeconds(0)
|
||||
expect(await calculateDecay(1.0, oneSecondAgo, now)).toBe(0.99999997802044727)
|
||||
expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now)).toBe(0.99999997802044727)
|
||||
})
|
||||
|
||||
it('returns input amount when from and to is the same', async () => {
|
||||
const now = new Date()
|
||||
expect((await calculateDecay(100.0, now, now)).balance).toBe(100.0)
|
||||
expect(calculateDecay(new Decimal(100.0), now, now).balance).toBe(100.0)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,45 +1,60 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import CONFIG from '../config'
|
||||
import { Decay } from '../graphql/model/Decay'
|
||||
|
||||
function decayFormula(amount: number, seconds: number): number {
|
||||
return amount * Math.pow(0.99999997802044727, seconds) // This number represents 50% decay a year
|
||||
// TODO: externalize all those definitions and functions into an external decay library
|
||||
interface Decay {
|
||||
balance: Decimal
|
||||
decay: Decimal | null
|
||||
start: Date | null
|
||||
end: Date | null
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
function calculateDecay(amount: number, from: Date, to: Date): Decay {
|
||||
function decayFormula(value: Decimal, seconds: number): Decimal {
|
||||
return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds))
|
||||
}
|
||||
|
||||
function calculateDecay(
|
||||
amount: Decimal,
|
||||
from: Date,
|
||||
to: Date,
|
||||
startBlock: Date = CONFIG.DECAY_START_TIME,
|
||||
): Decay {
|
||||
const fromMs = from.getTime()
|
||||
const toMs = to.getTime()
|
||||
const decayStartBlockMs = CONFIG.DECAY_START_TIME.getTime()
|
||||
const startBlockMs = startBlock.getTime()
|
||||
|
||||
if (toMs < fromMs) {
|
||||
throw new Error('to < from, reverse decay calculation is invalid')
|
||||
}
|
||||
|
||||
// Initialize with no decay
|
||||
const decay = new Decay({
|
||||
const decay: Decay = {
|
||||
balance: amount,
|
||||
decayStart: null,
|
||||
decayEnd: null,
|
||||
decayDuration: 0,
|
||||
decayStartBlock: (decayStartBlockMs / 1000).toString(),
|
||||
})
|
||||
decay: null,
|
||||
start: null,
|
||||
end: null,
|
||||
duration: null,
|
||||
}
|
||||
|
||||
// decay started after end date; no decay
|
||||
if (decayStartBlockMs > toMs) {
|
||||
if (startBlockMs > toMs) {
|
||||
return decay
|
||||
}
|
||||
// decay started before start date; decay for full duration
|
||||
else if (decayStartBlockMs < fromMs) {
|
||||
decay.decayStart = (fromMs / 1000).toString()
|
||||
decay.decayDuration = (toMs - fromMs) / 1000
|
||||
if (startBlockMs < fromMs) {
|
||||
decay.start = from
|
||||
decay.duration = (toMs - fromMs) / 1000
|
||||
}
|
||||
// decay started between start and end date; decay from decay start till end date
|
||||
else {
|
||||
decay.decayStart = (decayStartBlockMs / 1000).toString()
|
||||
decay.decayDuration = (toMs - decayStartBlockMs) / 1000
|
||||
decay.start = startBlock
|
||||
decay.duration = (toMs - startBlockMs) / 1000
|
||||
}
|
||||
|
||||
decay.decayEnd = (toMs / 1000).toString()
|
||||
decay.balance = decayFormula(amount, decay.decayDuration)
|
||||
decay.end = to
|
||||
decay.balance = decayFormula(amount, decay.duration)
|
||||
decay.decay = decay.balance.minus(amount)
|
||||
return decay
|
||||
}
|
||||
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 } from './round'
|
||||
|
||||
describe('utils/round', () => {
|
||||
it('roundCeilFrom4', () => {
|
||||
const amount = 11617
|
||||
expect(roundCeilFrom4(amount)).toBe(1.17)
|
||||
})
|
||||
// Not sure if the following skiped tests make sence!?
|
||||
it('roundFloorFrom4', () => {
|
||||
const amount = 11617
|
||||
expect(roundFloorFrom4(amount)).toBe(1.16)
|
||||
})
|
||||
it('roundCeilFrom2', () => {
|
||||
const amount = 1216
|
||||
expect(roundCeilFrom2(amount)).toBe(13)
|
||||
})
|
||||
// not possible, nodejs hasn't enough accuracy
|
||||
it('roundFloorFrom2', () => {
|
||||
const amount = 1216
|
||||
expect(roundFloorFrom2(amount)).toBe(12)
|
||||
})
|
||||
})
|
||||
@ -1,17 +0,0 @@
|
||||
function roundCeilFrom4(decimal: number): number {
|
||||
return Math.ceil(decimal / 100) / 100
|
||||
}
|
||||
|
||||
function roundFloorFrom4(decimal: number): number {
|
||||
return Math.floor(decimal / 100) / 100
|
||||
}
|
||||
|
||||
function roundCeilFrom2(decimal: number): number {
|
||||
return Math.ceil(decimal / 100)
|
||||
}
|
||||
|
||||
function roundFloorFrom2(decimal: number): number {
|
||||
return Math.floor(decimal / 100)
|
||||
}
|
||||
|
||||
export { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 }
|
||||
@ -1,7 +1,6 @@
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
import { getRepository } from '@dbTools/typeorm'
|
||||
import { calculateDecay } from './decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
function isStringBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
@ -15,14 +14,22 @@ function isHexPublicKey(publicKey: string): boolean {
|
||||
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
|
||||
}
|
||||
|
||||
async function hasUserAmount(user: dbUser, amount: number): Promise<boolean> {
|
||||
if (amount < 0) return false
|
||||
const balanceRepository = getRepository(dbBalance)
|
||||
const balance = await balanceRepository.findOne({ userId: user.id })
|
||||
if (!balance) return false
|
||||
async function calculateBalance(
|
||||
userId: number,
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
): Promise<Decimal | null> {
|
||||
if (amount.lessThan(0)) return null
|
||||
|
||||
const decay = calculateDecay(balance.amount, balance.recordDate, new Date()).balance
|
||||
return decay > amount
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||
if (!lastTransaction) return null
|
||||
|
||||
const accountBalance = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
time,
|
||||
).balance.add(amount)
|
||||
return accountBalance.greaterThan(0) ? accountBalance : null
|
||||
}
|
||||
|
||||
export { isHexPublicKey, hasUserAmount, isStringBoolean }
|
||||
export { isHexPublicKey, calculateBalance, isStringBoolean }
|
||||
|
||||
@ -1961,6 +1961,11 @@ debug@^3.2.6, debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
decimal.js-light@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
||||
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
|
||||
|
||||
decimal.js@^10.2.1:
|
||||
version "10.3.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user