diff --git a/backend/src/graphql/models/Decay.ts b/backend/src/graphql/models/Decay.ts index e0234f588..09564c5ca 100644 --- a/backend/src/graphql/models/Decay.ts +++ b/backend/src/graphql/models/Decay.ts @@ -1,29 +1,42 @@ /* 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 + constructor() + constructor(json?: any) { + 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 { + if (!this.decayStartBlockTransaction) { + this.decayStartBlockTransaction = await Transaction.getDecayStartBlock() + } + return this.decayStartBlockTransaction } @Field(() => Number) balance: number @Field(() => Int, { nullable: true }) - decayStart?: number + decayStart: number @Field(() => Int, { nullable: true }) - decayEnd?: number + decayEnd: number @Field(() => String, { nullable: true }) - decayDuration?: string + decayDuration?: number @Field(() => Int, { nullable: true }) decayStartBlock?: number + + static decayStartBlockTransaction: Transaction | undefined } diff --git a/backend/src/graphql/models/Transaction.ts b/backend/src/graphql/models/Transaction.ts index 7ce7d4494..01dcf280d 100644 --- a/backend/src/graphql/models/Transaction.ts +++ b/backend/src/graphql/models/Transaction.ts @@ -13,19 +13,19 @@ export class Transaction { constructor() constructor(json: any) constructor(json?: any) { - if(json) { + if (json) { 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.decayDuration = parseFloat(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 - } + } } @Field(() => String) @@ -44,7 +44,7 @@ export class Transaction { decayEnd?: number @Field({ nullable: true }) - decayDuration?: string + decayDuration?: number @Field(() => String) memo: string diff --git a/backend/src/graphql/resolvers/BalanceResolver.ts b/backend/src/graphql/resolvers/BalanceResolver.ts index 34aedac37..02c148ffb 100644 --- a/backend/src/graphql/resolvers/BalanceResolver.ts +++ b/backend/src/graphql/resolvers/BalanceResolver.ts @@ -25,7 +25,9 @@ export class BalanceResolver { const now = new Date() const balance = new Balance({ balance: roundFloorFrom4(balanceEntity.amount), - decay: roundFloorFrom4(calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now)), + decay: roundFloorFrom4( + await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now), + ), decay_date: now.toString(), }) diff --git a/backend/src/graphql/resolvers/listTransactions.ts b/backend/src/graphql/resolvers/listTransactions.ts index fc4c6de43..9bbb77bb5 100644 --- a/backend/src/graphql/resolvers/listTransactions.ts +++ b/backend/src/graphql/resolvers/listTransactions.ts @@ -2,9 +2,8 @@ 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 { TransactionSendCoin as dbTransactionSendCoin} from '../../typeorm/entity/TransactionSendCoin' -import { TransactionCreation as dbTransactionCreation} from '../../typeorm/entity/TransactionCreation' -import calculateDecay from '../../util/decay' +import { Decay } from '../models/Decay' +import { calculateDecayWithInterval } from '../../util/decay' import { roundFloorFrom4 } from '../../util/round' async function calculateAndAddDecayTransactions( @@ -13,9 +12,9 @@ async function calculateAndAddDecayTransactions( decay: boolean, skipFirstTransaction: boolean, ): Promise { - let finalTransactions: Transaction[] = [] - let transactionIds: number[] = [] - let involvedUserIds: number[] = [] + const finalTransactions: Transaction[] = [] + const transactionIds: number[] = [] + const involvedUserIds: number[] = [] userTransactions.forEach((userTransaction: dbUserTransaction) => { transactionIds.push(userTransaction.transactionId) @@ -24,39 +23,120 @@ async function calculateAndAddDecayTransactions( // 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 = dbUser.getUsersIndiced(involvedUsersUnique) + const userIndiced = await dbUser.getUsersIndiced(involvedUsersUnique) const transactions = await dbTransaction - .createQueryBuilder('transaction') - .where('transaction.id IN (:...transactions)', { transactions: transactionIds}) - .leftJoinAndSelect('transaction.sendCoin', 'transactionSendCoin', 'transactionSendCoin.transactionid = transaction.id') - .leftJoinAndSelect('transaction.creation', 'transactionCreation', 'transactionSendCoin.transactionid = transaction.id') - .getMany() - - let transactionIndiced: dbTransaction[] = [] + .createQueryBuilder('transaction') + .where('transaction.id IN (:...transactions)', { transactions: transactionIds }) + .leftJoinAndSelect( + 'transaction.sendCoin', + 'transactionSendCoin', + 'transactionSendCoin.transactionid = transaction.id', + ) + .leftJoinAndSelect( + 'transaction.creation', + 'transactionCreation', + 'transactionSendCoin.transactionid = transaction.id', + ) + .getMany() + + const transactionIndiced: dbTransaction[] = [] transactions.forEach((transaction: dbTransaction) => { transactionIndiced[transaction.id] = transaction - }) - - const decayStartTransaction = await dbTransaction.createQueryBuilder('transaction') - .where('transaction.transactionTypeId = :transactionTypeId', { transactionTypeId: 9}) - .orderBy('received', 'ASC') - .getOne() - - userTransactions.forEach((userTransaction: dbUserTransaction, i:number) => { - const transaction = transactionIndiced[userTransaction.transactionId] - let finalTransaction = new Transaction - finalTransaction.transactionId = transaction.id - finalTransaction.date = transaction.received.toString() - finalTransaction.memo = transaction.memo - - let prev = i > 0 ? userTransactions[i-1] : null - if(prev && prev.balance > 0) { - - } - }) + const decayStartTransaction = await Decay.getDecayStartBlock() + + userTransactions.forEach(async (userTransaction: dbUserTransaction, i: number) => { + const transaction = transactionIndiced[userTransaction.transactionId] + const finalTransaction = new Transaction() + finalTransaction.transactionId = transaction.id + finalTransaction.date = transaction.received.toString() + 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(finalTransaction.decay.balance) + finalTransaction.decay.balance = roundFloorFrom4(balance) + if ( + decayStartTransaction && + prev.transactionId < decayStartTransaction.id && + current.transactionId > decayStartTransaction.id + ) { + finalTransaction.decay.decayStartBlock = decayStartTransaction.received.getTime() + } + } + } + + // 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 + }) return finalTransactions } @@ -78,7 +158,7 @@ export default async function listTransactions( if (offset && order === 'ASC') { offset-- } - let [userTransactions, userTransactionsCount] = await UserTransaction.findByUserPaged( + let [userTransactions, userTransactionsCount] = await dbUserTransaction.findByUserPaged( user.id, limit, offset, @@ -86,12 +166,12 @@ export default async function listTransactions( ) skipFirstTransaction = userTransactionsCount > offset + limit const decay = !(firstPage > 1) - const transactions: Transaction[] = [] + let transactions: Transaction[] = [] if (userTransactions.length) { if (order === 'DESC') { userTransactions = userTransactions.reverse() } - let transactions = calculateAndAddDecayTransactions( + transactions = await calculateAndAddDecayTransactions( userTransactions, user, decay, diff --git a/backend/src/typeorm/entity/Transaction.ts b/backend/src/typeorm/entity/Transaction.ts index c9ecbf29f..d8b87828c 100644 --- a/backend/src/typeorm/entity/Transaction.ts +++ b/backend/src/typeorm/entity/Transaction.ts @@ -1,4 +1,6 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, Timestamp } from 'typeorm' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm' +import { TransactionCreation } from './TransactionCreation' +import { TransactionSendCoin } from './TransactionSendCoin' @Entity('transactions') export class Transaction extends BaseEntity { @@ -15,16 +17,29 @@ export class Transaction extends BaseEntity { memo: string @Column({ type: 'timestamp' }) - received: 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 { return this.createQueryBuilder('transaction') - .where('transaction.transactionTypeId = :transactionTypeId', { transactionTypeId: transactionTypeId}) + .where('transaction.transactionTypeId = :transactionTypeId', { + transactionTypeId: transactionTypeId, + }) .getMany() } - + static async getDecayStartBlock(): Promise { + return this.createQueryBuilder('transaction') + .where('transaction.transactionTypeId = :transactionTypeId', { transactionTypeId: 9 }) + .orderBy('received', 'ASC') + .getOne() + } } diff --git a/backend/src/typeorm/entity/TransactionCreation.ts b/backend/src/typeorm/entity/TransactionCreation.ts index ec473a13b..e9efa2e23 100644 --- a/backend/src/typeorm/entity/TransactionCreation.ts +++ b/backend/src/typeorm/entity/TransactionCreation.ts @@ -1,4 +1,12 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, Timestamp, OneToOne, JoinColumn } from 'typeorm' +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + Timestamp, + OneToOne, + JoinColumn, +} from 'typeorm' import { Transaction } from './Transaction' @Entity('transaction_creations') @@ -19,7 +27,6 @@ export class TransactionCreation extends BaseEntity { targetDate: Timestamp @OneToOne(() => Transaction) - @JoinColumn() + @JoinColumn() transaction: Transaction - } diff --git a/backend/src/typeorm/entity/TransactionSendCoin.ts b/backend/src/typeorm/entity/TransactionSendCoin.ts index b5b675d42..40ceb298b 100644 --- a/backend/src/typeorm/entity/TransactionSendCoin.ts +++ b/backend/src/typeorm/entity/TransactionSendCoin.ts @@ -15,7 +15,7 @@ export class TransactionSendCoin extends BaseEntity { @Column({ name: 'state_user_id' }) userId: number - @Column({ name: 'receiver_public_key', type: 'binary', length: 32}) + @Column({ name: 'receiver_public_key', type: 'binary', length: 32 }) recipiantPublic: Buffer @Column({ name: 'receiver_user_id' }) @@ -25,7 +25,6 @@ export class TransactionSendCoin extends BaseEntity { amount: number @OneToOne(() => Transaction) - @JoinColumn() + @JoinColumn() transaction: Transaction - } diff --git a/backend/src/typeorm/entity/User.ts b/backend/src/typeorm/entity/User.ts index e7bca8647..e82ba5deb 100644 --- a/backend/src/typeorm/entity/User.ts +++ b/backend/src/typeorm/entity/User.ts @@ -36,12 +36,12 @@ export class User extends BaseEntity { static async getUsersIndiced(userIds: number[]): Promise { const users = await this.createQueryBuilder('user') .select(['user.id', 'user.firstName', 'user.lastName', 'user.email']) - .where('user.id IN (:...users)', { users: userIds}) + .where('user.id IN (:...users)', { users: userIds }) .getMany() - let usersIndiced: User[] = [] - users.forEach((value, index) => { + const usersIndiced: User[] = [] + users.forEach((value, index) => { usersIndiced[index] = value - }) + }) return usersIndiced } } diff --git a/backend/src/typeorm/entity/UserTransaction.ts b/backend/src/typeorm/entity/UserTransaction.ts index 494f57103..1f32dc454 100644 --- a/backend/src/typeorm/entity/UserTransaction.ts +++ b/backend/src/typeorm/entity/UserTransaction.ts @@ -1,4 +1,4 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, Timestamp } from 'typeorm' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' @Entity('state_user_transactions') export class UserTransaction extends BaseEntity { @@ -18,7 +18,7 @@ export class UserTransaction extends BaseEntity { balance: number @Column({ name: 'balance_date', type: 'timestamp' }) - balanceDate: Timestamp + balanceDate: number static findByUserPaged( userId: number, diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts index 44a041a00..ccb6beeb4 100644 --- a/backend/src/util/decay.ts +++ b/backend/src/util/decay.ts @@ -1,4 +1,51 @@ -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 { + // 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, + to: number, +): Promise { + const decayStartBlock = await Decay.getDecayStartBlock() + const result = new Decay() + result.balance = amount + result.decayStart = from + result.decayEnd = from + + // (amount, from.getTime(), to.getTime()) + + // if no decay start block exist or decay startet after end date + if (decayStartBlock === undefined || decayStartBlock.received.getTime() > to) { + return result + } + + // if decay start date is before start date we calculate decay for full duration + if (decayStartBlock.received.getTime() < from) { + result.decayDuration = to - from + } + // if decay start in between start date and end date we caculcate decay from decay start time to end date + else { + result.decayDuration = to - decayStartBlock.received.getTime() + } + // js use timestamp in milliseconds but we calculate with seconds + result.decayDuration /= 1000 + result.balance = decayFormula(amount, result.decayDuration) + + return result +} + +export { calculateDecay, calculateDecayWithInterval }