mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #893 from gradido/apollo_listTransactions
Apollo list transactions
This commit is contained in:
commit
830d11633e
4712
backend/package-lock.json
generated
4712
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@ export class TransactionListInput {
|
||||
items: number
|
||||
|
||||
@Field(() => String)
|
||||
order: string
|
||||
order: 'ASC' | 'DESC'
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
|
||||
@ -1,29 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import { Transaction } from '../../typeorm/entity/Transaction'
|
||||
|
||||
@ObjectType()
|
||||
export class Decay {
|
||||
constructor(json: any) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.decayStartBlock = json.decay_start_block
|
||||
if (json) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.decayStartBlock = json.decay_start_block
|
||||
}
|
||||
}
|
||||
|
||||
static async getDecayStartBlock(): Promise<Transaction | undefined> {
|
||||
if (!this.decayStartBlockTransaction) {
|
||||
this.decayStartBlockTransaction = await Transaction.getDecayStartBlock()
|
||||
}
|
||||
return this.decayStartBlockTransaction
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStart?: number
|
||||
decayStart: string
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayEnd?: number
|
||||
decayEnd: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
decayDuration?: string
|
||||
decayDuration?: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStartBlock?: number
|
||||
decayStartBlock?: string
|
||||
|
||||
static decayStartBlockTransaction: Transaction | undefined
|
||||
}
|
||||
|
||||
@ -10,18 +10,11 @@ import { Decay } from './Decay'
|
||||
|
||||
@ObjectType()
|
||||
export class Transaction {
|
||||
constructor(json: any) {
|
||||
this.type = json.type
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.memo = json.memo
|
||||
this.transactionId = json.transaction_id
|
||||
this.name = json.name
|
||||
this.email = json.email
|
||||
this.date = json.date
|
||||
this.decay = json.decay ? new Decay(json.decay) : undefined
|
||||
constructor() {
|
||||
this.type = ''
|
||||
this.balance = 0
|
||||
this.totalBalance = 0
|
||||
this.memo = ''
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
@ -30,14 +23,17 @@ export class Transaction {
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayStart?: number
|
||||
@Field(() => Number)
|
||||
totalBalance: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayEnd?: number
|
||||
decayStart?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayDuration?: string
|
||||
decayEnd?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayDuration?: number
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
@ -60,15 +56,12 @@ export class Transaction {
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionList {
|
||||
constructor(json: any) {
|
||||
this.gdtSum = Number(json.gdtSum)
|
||||
this.count = json.count
|
||||
this.balance = Number(json.balance)
|
||||
this.decay = Number(json.decay)
|
||||
this.decayDate = json.decay_date
|
||||
this.transactions = json.transactions.map((el: any) => {
|
||||
return new Transaction(el)
|
||||
})
|
||||
constructor() {
|
||||
this.gdtSum = 0
|
||||
this.count = 0
|
||||
this.balance = 0
|
||||
this.decay = 0
|
||||
this.decayDate = ''
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
|
||||
@ -7,7 +7,7 @@ import { Balance } from '../models/Balance'
|
||||
import { apiGet } from '../../apis/HttpRequest'
|
||||
import { User as dbUser } from '../../typeorm/entity/User'
|
||||
import { Balance as dbBalance } from '../../typeorm/entity/Balance'
|
||||
import calculateDecay from '../../util/decay'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
|
||||
@Resolver()
|
||||
@ -22,12 +22,23 @@ export class BalanceResolver {
|
||||
// load user and balance
|
||||
const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex)
|
||||
const balanceEntity = await dbBalance.findByUser(userEntity.id)
|
||||
let balance: Balance
|
||||
const now = new Date()
|
||||
const balance = new Balance({
|
||||
balance: roundFloorFrom4(balanceEntity.amount),
|
||||
decay: roundFloorFrom4(calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now)),
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
if (balanceEntity) {
|
||||
balance = new Balance({
|
||||
balance: roundFloorFrom4(balanceEntity.amount),
|
||||
decay: roundFloorFrom4(
|
||||
await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now),
|
||||
),
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
} else {
|
||||
balance = new Balance({
|
||||
balance: 0,
|
||||
decay: 0,
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
return balance
|
||||
}
|
||||
|
||||
@ -6,6 +6,11 @@ import CONFIG from '../../config'
|
||||
import { TransactionList } from '../models/Transaction'
|
||||
import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput'
|
||||
import { apiGet, apiPost } from '../../apis/HttpRequest'
|
||||
import { User as dbUser } from '../../typeorm/entity/User'
|
||||
import { Balance as dbBalance } from '../../typeorm/entity/Balance'
|
||||
import listTransactions from './listTransactions'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@ -15,11 +20,34 @@ export class TransactionResolver {
|
||||
@Args() { firstPage = 1, items = 25, order = 'DESC' }: TransactionListInput,
|
||||
@Ctx() context: any,
|
||||
): Promise<TransactionList> {
|
||||
const result = await apiGet(
|
||||
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${context.sessionId}`,
|
||||
)
|
||||
// get public key for current logged in user
|
||||
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
|
||||
if (!result.success) throw new Error(result.data)
|
||||
return new TransactionList(result.data)
|
||||
|
||||
// load user
|
||||
const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex)
|
||||
|
||||
const transactions = await listTransactions(firstPage, items, order, userEntity)
|
||||
|
||||
// get gdt sum
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: userEntity.email,
|
||||
})
|
||||
if (!resultGDTSum.success) throw new Error(resultGDTSum.data)
|
||||
transactions.gdtSum = resultGDTSum.data.sum
|
||||
|
||||
// get balance
|
||||
const balanceEntity = await dbBalance.findByUser(userEntity.id)
|
||||
if (balanceEntity) {
|
||||
const now = new Date()
|
||||
transactions.balance = roundFloorFrom4(balanceEntity.amount)
|
||||
transactions.decay = roundFloorFrom4(
|
||||
await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now),
|
||||
)
|
||||
transactions.decayDate = now.toString()
|
||||
}
|
||||
|
||||
return transactions
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
|
||||
194
backend/src/graphql/resolvers/listTransactions.ts
Normal file
194
backend/src/graphql/resolvers/listTransactions.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import { User as dbUser } from '../../typeorm/entity/User'
|
||||
import { TransactionList, Transaction } from '../models/Transaction'
|
||||
import { UserTransaction as dbUserTransaction } from '../../typeorm/entity/UserTransaction'
|
||||
import { Transaction as dbTransaction } from '../../typeorm/entity/Transaction'
|
||||
import { Decay } from '../models/Decay'
|
||||
import { calculateDecayWithInterval } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
|
||||
async function calculateAndAddDecayTransactions(
|
||||
userTransactions: dbUserTransaction[],
|
||||
user: dbUser,
|
||||
decay: boolean,
|
||||
skipFirstTransaction: boolean,
|
||||
): Promise<Transaction[]> {
|
||||
const finalTransactions: Transaction[] = []
|
||||
const transactionIds: number[] = []
|
||||
const involvedUserIds: number[] = []
|
||||
|
||||
userTransactions.forEach((userTransaction: dbUserTransaction) => {
|
||||
transactionIds.push(userTransaction.transactionId)
|
||||
})
|
||||
|
||||
const transactions = await dbTransaction
|
||||
.createQueryBuilder('transaction')
|
||||
.where('transaction.id IN (:...transactions)', { transactions: transactionIds })
|
||||
.leftJoinAndSelect(
|
||||
'transaction.transactionSendCoin',
|
||||
'transactionSendCoin',
|
||||
// 'transactionSendCoin.transaction_id = transaction.id',
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
'transaction.transactionCreation',
|
||||
'transactionCreation',
|
||||
// 'transactionSendCoin.transaction_id = transaction.id',
|
||||
)
|
||||
.getMany()
|
||||
|
||||
const transactionIndiced: dbTransaction[] = []
|
||||
transactions.forEach((transaction: dbTransaction) => {
|
||||
transactionIndiced[transaction.id] = transaction
|
||||
if (transaction.transactionTypeId === 2) {
|
||||
involvedUserIds.push(transaction.transactionSendCoin.userId)
|
||||
involvedUserIds.push(transaction.transactionSendCoin.recipiantUserId)
|
||||
}
|
||||
})
|
||||
// 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 userIndiced = await dbUser.getUsersIndiced(involvedUsersUnique)
|
||||
|
||||
const decayStartTransaction = await Decay.getDecayStartBlock()
|
||||
|
||||
for (let i = 0; i < userTransactions.length; i++) {
|
||||
const userTransaction = userTransactions[i]
|
||||
const transaction = transactionIndiced[userTransaction.transactionId]
|
||||
const finalTransaction = new Transaction()
|
||||
finalTransaction.transactionId = transaction.id
|
||||
finalTransaction.date = transaction.received.toISOString()
|
||||
finalTransaction.memo = transaction.memo
|
||||
finalTransaction.totalBalance = roundFloorFrom4(userTransaction.balance)
|
||||
const prev = i > 0 ? userTransactions[i - 1] : null
|
||||
|
||||
if (prev && prev.balance > 0) {
|
||||
const current = userTransaction
|
||||
const decay = await calculateDecayWithInterval(
|
||||
prev.balance,
|
||||
prev.balanceDate,
|
||||
current.balanceDate,
|
||||
)
|
||||
const balance = prev.balance - decay.balance
|
||||
|
||||
if (balance) {
|
||||
finalTransaction.decay = decay
|
||||
finalTransaction.decay.balance = roundFloorFrom4(balance)
|
||||
if (
|
||||
decayStartTransaction &&
|
||||
prev.transactionId < decayStartTransaction.id &&
|
||||
current.transactionId > decayStartTransaction.id
|
||||
) {
|
||||
finalTransaction.decay.decayStartBlock = (
|
||||
decayStartTransaction.received.getTime() / 1000
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sender or receiver when user has sended money
|
||||
// group name if creation
|
||||
// type: gesendet / empfangen / geschöpft
|
||||
// transaktion nr / id
|
||||
// date
|
||||
// balance
|
||||
if (userTransaction.transactionTypeId === 1) {
|
||||
// creation
|
||||
const creation = transaction.transactionCreation
|
||||
|
||||
finalTransaction.name = 'Gradido Akademie'
|
||||
finalTransaction.type = 'creation'
|
||||
// finalTransaction.targetDate = creation.targetDate
|
||||
finalTransaction.balance = roundFloorFrom4(creation.amount)
|
||||
} else if (userTransaction.transactionTypeId === 2) {
|
||||
// send coin
|
||||
const sendCoin = transaction.transactionSendCoin
|
||||
let otherUser: dbUser | undefined
|
||||
finalTransaction.balance = roundFloorFrom4(sendCoin.amount)
|
||||
if (sendCoin.userId === user.id) {
|
||||
finalTransaction.type = 'send'
|
||||
otherUser = userIndiced[sendCoin.recipiantUserId]
|
||||
// finalTransaction.pubkey = sendCoin.recipiantPublic
|
||||
} else if (sendCoin.recipiantUserId === user.id) {
|
||||
finalTransaction.type = 'receive'
|
||||
otherUser = userIndiced[sendCoin.userId]
|
||||
// finalTransaction.pubkey = sendCoin.senderPublic
|
||||
} else {
|
||||
throw new Error('invalid transaction')
|
||||
}
|
||||
if (otherUser) {
|
||||
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
|
||||
finalTransaction.email = otherUser.email
|
||||
}
|
||||
}
|
||||
if (i > 0 || !skipFirstTransaction) {
|
||||
finalTransactions.push(finalTransaction)
|
||||
}
|
||||
|
||||
if (i === userTransactions.length - 1 && decay) {
|
||||
const now = new Date()
|
||||
const decay = await calculateDecayWithInterval(
|
||||
userTransaction.balance,
|
||||
userTransaction.balanceDate,
|
||||
now.getTime(),
|
||||
)
|
||||
const balance = userTransaction.balance - decay.balance
|
||||
if (balance) {
|
||||
const decayTransaction = new Transaction()
|
||||
decayTransaction.type = 'decay'
|
||||
decayTransaction.balance = roundFloorFrom4(balance)
|
||||
decayTransaction.decayDuration = decay.decayDuration
|
||||
decayTransaction.decayStart = decay.decayStart
|
||||
decayTransaction.decayEnd = decay.decayEnd
|
||||
finalTransactions.push(decayTransaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalTransactions
|
||||
}
|
||||
|
||||
export default async function listTransactions(
|
||||
firstPage: number,
|
||||
items: number,
|
||||
order: 'ASC' | 'DESC',
|
||||
user: dbUser,
|
||||
): Promise<TransactionList> {
|
||||
let limit = items
|
||||
let offset = 0
|
||||
let skipFirstTransaction = false
|
||||
if (firstPage > 1) {
|
||||
offset = (firstPage - 1) * items - 1
|
||||
limit++
|
||||
}
|
||||
|
||||
if (offset && order === 'ASC') {
|
||||
offset--
|
||||
}
|
||||
let [userTransactions, userTransactionsCount] = await dbUserTransaction.findByUserPaged(
|
||||
user.id,
|
||||
limit,
|
||||
offset,
|
||||
order,
|
||||
)
|
||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
||||
const decay = !(firstPage > 1)
|
||||
let transactions: Transaction[] = []
|
||||
if (userTransactions.length) {
|
||||
if (order === 'DESC') {
|
||||
userTransactions = userTransactions.reverse()
|
||||
}
|
||||
transactions = await calculateAndAddDecayTransactions(
|
||||
userTransactions,
|
||||
user,
|
||||
decay,
|
||||
skipFirstTransaction,
|
||||
)
|
||||
if (order === 'DESC') {
|
||||
transactions = transactions.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
const transactionList = new TransactionList()
|
||||
transactionList.count = userTransactionsCount
|
||||
transactionList.transactions = transactions
|
||||
return transactionList
|
||||
}
|
||||
@ -17,9 +17,7 @@ export class Balance extends BaseEntity {
|
||||
@Column({ type: 'bigint' })
|
||||
amount: number
|
||||
|
||||
static findByUser(userId: number): Promise<Balance> {
|
||||
return this.createQueryBuilder('balance')
|
||||
.where('balance.userId = :userId', { userId })
|
||||
.getOneOrFail()
|
||||
static findByUser(userId: number): Promise<Balance | undefined> {
|
||||
return this.createQueryBuilder('balance').where('balance.userId = :userId', { userId }).getOne()
|
||||
}
|
||||
}
|
||||
|
||||
45
backend/src/typeorm/entity/Transaction.ts
Normal file
45
backend/src/typeorm/entity/Transaction.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
|
||||
import { TransactionCreation } from './TransactionCreation'
|
||||
import { TransactionSendCoin } from './TransactionSendCoin'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_type_id' })
|
||||
transactionTypeId: number
|
||||
|
||||
@Column({ name: 'tx_hash', type: 'binary', length: 48 })
|
||||
txHash: Buffer
|
||||
|
||||
@Column()
|
||||
memo: string
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
received: Date
|
||||
|
||||
@Column({ name: 'blockchain_type_id' })
|
||||
blockchainTypeId: number
|
||||
|
||||
@OneToOne(() => TransactionSendCoin, (transactionSendCoin) => transactionSendCoin.transaction)
|
||||
transactionSendCoin: TransactionSendCoin
|
||||
|
||||
@OneToOne(() => TransactionCreation, (transactionCreation) => transactionCreation.transaction)
|
||||
transactionCreation: TransactionCreation
|
||||
|
||||
static async findByTransactionTypeId(transactionTypeId: number): Promise<Transaction[]> {
|
||||
return this.createQueryBuilder('transaction')
|
||||
.where('transaction.transactionTypeId = :transactionTypeId', {
|
||||
transactionTypeId: transactionTypeId,
|
||||
})
|
||||
.getMany()
|
||||
}
|
||||
|
||||
static async getDecayStartBlock(): Promise<Transaction | undefined> {
|
||||
return this.createQueryBuilder('transaction')
|
||||
.where('transaction.transactionTypeId = :transactionTypeId', { transactionTypeId: 9 })
|
||||
.orderBy('received', 'ASC')
|
||||
.getOne()
|
||||
}
|
||||
}
|
||||
32
backend/src/typeorm/entity/TransactionCreation.ts
Normal file
32
backend/src/typeorm/entity/TransactionCreation.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Timestamp,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@Entity('transaction_creations')
|
||||
export class TransactionCreation extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'state_user_id' })
|
||||
userId: number
|
||||
|
||||
@Column()
|
||||
amount: number
|
||||
|
||||
@Column({ name: 'target_date', type: 'timestamp' })
|
||||
targetDate: Timestamp
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction: Transaction
|
||||
}
|
||||
30
backend/src/typeorm/entity/TransactionSendCoin.ts
Normal file
30
backend/src/typeorm/entity/TransactionSendCoin.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@Entity('transaction_send_coins')
|
||||
export class TransactionSendCoin extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'sender_public_key', type: 'binary', length: 32 })
|
||||
senderPublic: Buffer
|
||||
|
||||
@Column({ name: 'state_user_id' })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'receiver_public_key', type: 'binary', length: 32 })
|
||||
recipiantPublic: Buffer
|
||||
|
||||
@Column({ name: 'receiver_user_id' })
|
||||
recipiantUserId: number
|
||||
|
||||
@Column()
|
||||
amount: number
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction: Transaction
|
||||
}
|
||||
@ -32,4 +32,16 @@ export class User extends BaseEntity {
|
||||
.where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex })
|
||||
.getOneOrFail()
|
||||
}
|
||||
|
||||
static async getUsersIndiced(userIds: number[]): Promise<User[]> {
|
||||
const users = await this.createQueryBuilder('user')
|
||||
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
|
||||
.where('user.id IN (:...users)', { users: userIds })
|
||||
.getMany()
|
||||
const usersIndiced: User[] = []
|
||||
users.forEach((value) => {
|
||||
usersIndiced[value.id] = value
|
||||
})
|
||||
return usersIndiced
|
||||
}
|
||||
}
|
||||
|
||||
36
backend/src/typeorm/entity/UserTransaction.ts
Normal file
36
backend/src/typeorm/entity/UserTransaction.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
|
||||
@Entity('state_user_transactions')
|
||||
export class UserTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'state_user_id' })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'transaction_type_id' })
|
||||
transactionTypeId: number
|
||||
|
||||
@Column({ name: 'balance', type: 'bigint' })
|
||||
balance: number
|
||||
|
||||
@Column({ name: 'balance_date', type: 'timestamp' })
|
||||
balanceDate: Date
|
||||
|
||||
static findByUserPaged(
|
||||
userId: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
order: 'ASC' | 'DESC',
|
||||
): Promise<[UserTransaction[], number]> {
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.getManyAndCount()
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,55 @@
|
||||
export default function (amount: number, from: Date, to: Date): number {
|
||||
const decayDuration = (to.getTime() - from.getTime()) / 1000
|
||||
return amount * Math.pow(0.99999997802044727, decayDuration)
|
||||
import { Decay } from '../graphql/models/Decay'
|
||||
|
||||
function decayFormula(amount: number, durationInSeconds: number): number {
|
||||
return amount * Math.pow(0.99999997802044727, durationInSeconds)
|
||||
}
|
||||
|
||||
async function calculateDecay(amount: number, from: Date, to: Date): Promise<number> {
|
||||
// load decay start block
|
||||
const decayStartBlock = await Decay.getDecayStartBlock()
|
||||
|
||||
// if decay hasn't started yet we return input amount
|
||||
if (!decayStartBlock) return amount
|
||||
|
||||
const decayDuration = (to.getTime() - from.getTime()) / 1000
|
||||
return decayFormula(amount, decayDuration)
|
||||
}
|
||||
|
||||
async function calculateDecayWithInterval(
|
||||
amount: number,
|
||||
from: number | Date,
|
||||
to: number | Date,
|
||||
): Promise<Decay> {
|
||||
const decayStartBlock = await Decay.getDecayStartBlock()
|
||||
|
||||
const result = new Decay(undefined)
|
||||
result.balance = amount
|
||||
const fromMillis = typeof from === 'number' ? from : from.getTime()
|
||||
const toMillis = typeof to === 'number' ? to : to.getTime()
|
||||
result.decayStart = (fromMillis / 1000).toString()
|
||||
result.decayEnd = (toMillis / 1000).toString()
|
||||
|
||||
// (amount, from.getTime(), to.getTime())
|
||||
|
||||
// if no decay start block exist or decay startet after end date
|
||||
if (!decayStartBlock || decayStartBlock.received.getTime() > toMillis) {
|
||||
return result
|
||||
}
|
||||
const decayStartBlockMillis = decayStartBlock.received.getTime()
|
||||
|
||||
// if decay start date is before start date we calculate decay for full duration
|
||||
if (decayStartBlockMillis < fromMillis) {
|
||||
result.decayDuration = toMillis - fromMillis
|
||||
}
|
||||
// if decay start in between start date and end date we caculcate decay from decay start time to end date
|
||||
else {
|
||||
result.decayDuration = toMillis - decayStartBlockMillis
|
||||
result.decayStart = (decayStartBlockMillis / 1000).toString()
|
||||
}
|
||||
// js use timestamp in milliseconds but we calculate with seconds
|
||||
result.decayDuration /= 1000
|
||||
result.balance = decayFormula(amount, result.decayDuration)
|
||||
return result
|
||||
}
|
||||
|
||||
export { calculateDecay, calculateDecayWithInterval }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user