/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { getCustomRepository } from 'typeorm' import CONFIG from '../../config' import { Transaction } from '../model/Transaction' import { TransactionList } from '../model/TransactionList' import TransactionSendArgs from '../arg/TransactionSendArgs' import Paginated from '../arg/Paginated' import { Order } from '../enum/Order' import { BalanceRepository } from '../../typeorm/repository/Balance' import { UserRepository } from '../../typeorm/repository/User' import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction' import { TransactionRepository } from '../../typeorm/repository/Transaction' import { User as dbUser } from '../../typeorm/entity/User' import { UserTransaction as dbUserTransaction } from '../../typeorm/entity/UserTransaction' import { Transaction as dbTransaction } from '../../typeorm/entity/Transaction' import { apiPost } from '../../apis/HttpRequest' import { roundFloorFrom4 } from '../../util/round' import { calculateDecay, calculateDecayWithInterval } from '../../util/decay' import { TransactionTypeId } from '../enum/TransactionTypeId' import { TransactionType } from '../enum/TransactionType' // Helper function async function calculateAndAddDecayTransactions( userTransactions: dbUserTransaction[], user: dbUser, decay: boolean, skipFirstTransaction: boolean, ): Promise { const finalTransactions: Transaction[] = [] const transactionIds: number[] = [] const involvedUserIds: number[] = [] userTransactions.forEach((userTransaction: dbUserTransaction) => { transactionIds.push(userTransaction.transactionId) }) const transactionRepository = getCustomRepository(TransactionRepository) const transactions = await transactionRepository.joinFullTransactionsByIds(transactionIds) const transactionIndiced: dbTransaction[] = [] transactions.forEach((transaction: dbTransaction) => { transactionIndiced[transaction.id] = transaction if (transaction.transactionTypeId === TransactionTypeId.SEND) { 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 userRepository = getCustomRepository(UserRepository) const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique) const decayStartTransaction = await transactionRepository.findDecayStartBlock() 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) { const current = userTransaction const decay = await calculateDecayWithInterval( prev.balance, prev.balanceDate, current.balanceDate, ) const balance = prev.balance - decay.balance if (decayStartTransaction && decayStartTransaction.received >= prev.balanceDate) { 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 sent money // group name if creation // type: gesendet / empfangen / geschöpft // transaktion nr / id // date // balance if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) { // creation const creation = transaction.transactionCreation finalTransaction.name = 'Gradido Akademie' finalTransaction.type = TransactionType.CREATION // finalTransaction.targetDate = creation.targetDate finalTransaction.balance = roundFloorFrom4(creation.amount) } else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) { // send coin const sendCoin = transaction.transactionSendCoin let otherUser: dbUser | undefined finalTransaction.balance = roundFloorFrom4(sendCoin.amount) if (sendCoin.userId === user.id) { finalTransaction.type = TransactionType.SEND otherUser = userIndiced[sendCoin.recipiantUserId] // finalTransaction.pubkey = sendCoin.recipiantPublic } else if (sendCoin.recipiantUserId === user.id) { finalTransaction.type = TransactionType.RECIEVE 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 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 } // Helper function async function listTransactions( currentPage: number, pageSize: number, order: Order, user: dbUser, ): Promise { let limit = pageSize let offset = 0 let skipFirstTransaction = false if (currentPage > 1) { offset = (currentPage - 1) * pageSize - 1 limit++ } if (offset && order === Order.ASC) { offset-- } const userTransactionRepository = getCustomRepository(UserTransactionRepository) let [userTransactions, userTransactionsCount] = await userTransactionRepository.findByUserPaged( user.id, limit, offset, order, ) skipFirstTransaction = userTransactionsCount > offset + limit const decay = !(currentPage > 1) let transactions: Transaction[] = [] if (userTransactions.length) { if (order === Order.DESC) { userTransactions = userTransactions.reverse() } transactions = await calculateAndAddDecayTransactions( userTransactions, user, decay, skipFirstTransaction, ) if (order === Order.DESC) { transactions = transactions.reverse() } } const transactionList = new TransactionList() transactionList.count = userTransactionsCount transactionList.transactions = transactions return transactionList } @Resolver() export class TransactionResolver { @Authorized() @Query(() => TransactionList) async transactionList( @Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated, @Ctx() context: any, ): Promise { // load user const userRepository = getCustomRepository(UserRepository) const userEntity = await userRepository.findByPubkeyHex(context.pubKey) const transactions = await listTransactions(currentPage, pageSize, 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 || 0 // get balance const balanceRepository = getCustomRepository(BalanceRepository) const balanceEntity = await balanceRepository.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() @Mutation(() => String) async sendCoins( @Args() { email, amount, memo }: TransactionSendArgs, @Ctx() context: any, ): Promise { const payload = { session_id: context.sessionId, target_email: email, amount: amount * 10000, memo, auto_sign: true, transaction_type: 'transfer', blockchain_type: 'mysql', } const result = await apiPost(CONFIG.LOGIN_API_URL + 'createTransaction', payload) if (!result.success) { throw new Error(result.data) } return 'success' } }