backend compiles

This commit is contained in:
Ulf Gebhardt 2022-02-26 03:42:20 +01:00
parent f3f2d547a3
commit 7644bf1834
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
19 changed files with 201 additions and 315 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),
})
}

View File

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

View File

@ -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!',
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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