From d403087ed14c860b7d52ccef896a4e71779263df Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Wed, 30 Aug 2023 15:42:18 +0200 Subject: [PATCH] revert settlement in federation modul --- federation/package.json | 1 + .../api/1_0/enum/PendingTransactionState.ts | 7 +- .../api/1_0/resolver/SendCoinsResolver.ts | 117 +++++++++++---- .../util/revertSettledReceiveTransaction.ts | 139 ++++++++++++++++++ .../util/settlePendingReceiveTransaction.ts | 89 ++--------- federation/tsconfig.json | 6 +- federation/yarn.lock | 5 + 7 files changed, 248 insertions(+), 116 deletions(-) create mode 100644 federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts diff --git a/federation/package.json b/federation/package.json index b70196149..eb78d6be0 100644 --- a/federation/package.json +++ b/federation/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "apollo-server-express": "^2.25.2", + "await-semaphore": "0.1.3", "class-validator": "^0.13.2", "cors": "2.8.5", "cross-env": "^7.0.3", diff --git a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts index 6a614be96..d89b0b0eb 100644 --- a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts +++ b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts @@ -2,10 +2,9 @@ import { registerEnumType } from 'type-graphql' export enum PendingTransactionState { NEW = 1, - WAIT_ON_PENDING = 2, - PENDING = 3, - WAIT_ON_CONFIRM = 4, - CONFIRMED = 5, + PENDING = 2, + SETTLED = 3, + REVERTED = 4, } registerEnumType(PendingTransactionState, { diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index fff969ae9..9d43ada62 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -14,6 +14,7 @@ import { fullName } from '@/graphql/util/fullName' import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '../const/const' import { checkTradingLevel } from '@/graphql/util/checkTradingLevel' +import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -146,7 +147,7 @@ export class SendCoinsResolver { linkedUserGradidoID: userSenderIdentifier, }) logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx) - if (pendingTx && pendingTx.amount === amount) { + if (pendingTx && pendingTx.amount === amount && pendingTx.memo === memo) { logger.debug('XCom: revertSendCoins matching pendingTX for remove...') try { await pendingTx.remove() @@ -193,28 +194,6 @@ export class SendCoinsResolver { ): Promise { logger.debug(`settleSendCoins() via apiVersion=1_0 ...`) try { - // first check if receiver community is correct - const homeCom = await DbCommunity.findOneByOrFail({ - communityUuid: communityReceiverIdentifier, - }) - /* - if (!homeCom) { - throw new LogError( - `settleSendCoins with wrong communityReceiverIdentifier`, - communityReceiverIdentifier, - ) - } - */ - // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneByOrFail({ gradidoID: userReceiverIdentifier }) - /* - if (!receiverUser) { - throw new LogError( - `settleSendCoins with unknown userReceiverIdentifier in the community=`, - homeCom.name, - ) - } - */ const pendingTx = await DbPendingTransaction.findOneBy({ userCommunityUuid: communityReceiverIdentifier, userGradidoID: userReceiverIdentifier, @@ -227,12 +206,15 @@ export class SendCoinsResolver { logger.debug('XCom: settleSendCoins found pendingTX=', pendingTx) if (pendingTx && pendingTx.amount === amount && pendingTx.memo === memo) { logger.debug('XCom: settleSendCoins matching pendingTX for settlement...') - try { - await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx) - logger.debug('XCom: settlePendingReceiveTransaction successfully...') - } catch (err) { - throw new LogError('Error in settlePendingReceiveTransaction: ', err) - } + + const homeCom = await DbCommunity.findOneByOrFail({ + communityUuid: communityReceiverIdentifier, + }) + const receiverUser = await DbUser.findOneByOrFail({ gradidoID: userReceiverIdentifier }) + + await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx) + logger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successfull`) + return true } else { logger.debug( 'XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...', @@ -251,10 +233,83 @@ export class SendCoinsResolver { userSenderName, ) } - logger.debug(`settlePendingReceiveTransaction()-1_0... successfull`) - return true } catch (err) { throw new LogError(`Error in settlePendingReceiveTransaction: `, err) } } + + @Mutation(() => Boolean) + async revertSettledSendCoins( + @Args() + { + communityReceiverIdentifier, + userReceiverIdentifier, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + }: SendCoinsArgs, + ): Promise { + try { + logger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`) + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: communityReceiverIdentifier, + }) + if (!homeCom) { + throw new LogError( + `revertSettledSendCoins with wrong communityReceiverIdentifier`, + communityReceiverIdentifier, + ) + } + // second check if receiver user exists in this community + const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) + if (!receiverUser) { + throw new LogError( + `revertSettledSendCoins with unknown userReceiverIdentifier in the community=`, + homeCom.name, + ) + } + const pendingTx = await DbPendingTransaction.findOneBy({ + userCommunityUuid: communityReceiverIdentifier, + userGradidoID: userReceiverIdentifier, + state: PendingTransactionState.SETTLED, + typeId: TransactionTypeId.RECEIVE, + balanceDate: creationDate, + linkedUserCommunityUuid: communitySenderIdentifier, + linkedUserGradidoID: userSenderIdentifier, + }) + logger.debug('XCom: revertSettledSendCoins found pendingTX=', pendingTx) + if (pendingTx && pendingTx.amount === amount && pendingTx.memo === memo) { + logger.debug('XCom: revertSettledSendCoins matching pendingTX for remove...') + try { + await revertSettledReceiveTransaction(homeCom, receiverUser, pendingTx) + logger.debug('XCom: revertSettledSendCoins pendingTX successfully') + } catch (err) { + throw new LogError('Error in revertSettledSendCoins of receiver: ', err) + } + } else { + logger.debug('XCom: revertSettledSendCoins NOT matching pendingTX...') + throw new LogError( + `Can't find in revertSettledSendCoins the pending receiver TX for args=`, + communityReceiverIdentifier, + userReceiverIdentifier, + PendingTransactionState.SETTLED, + TransactionTypeId.RECEIVE, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + ) + } + logger.debug(`revertSendCoins()-1_0... successfull`) + return true + } catch (err) { + throw new LogError(`Error in revertSendCoins: `, err) + } + } } diff --git a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts new file mode 100644 index 000000000..ecf615667 --- /dev/null +++ b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts @@ -0,0 +1,139 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable new-cap */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { getConnection } from '@dbTools/typeorm' +import { Community as DbCommunity } from '@entity/Community' +import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' +import { Transaction as dbTransaction } from '@entity/Transaction' +import { User as DbUser } from '@entity/User' + +import { PendingTransactionState } from '../enum/PendingTransactionState' + +import { LogError } from '@/server/LogError' +import { federationLogger as logger } from '@/server/logger' + +import { getLastTransaction } from '@/graphql/util/getLastTransaction' +import { TRANSACTIONS_LOCK } from '@/graphql/util/TRANSACTIONS_LOCK' + +export async function revertSettledReceiveTransaction( + homeCom: DbCommunity, + receiverUser: DbUser, + pendingTx: DbPendingTransaction, +): Promise { + // TODO: synchronisation with TRANSACTION_LOCK of backend-modul necessary!!! + // acquire lock + const releaseLock = await TRANSACTIONS_LOCK.acquire() + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction('REPEATABLE READ') + logger.debug(`start Transaction for write-access...`) + + try { + logger.info('X-Com: revertSettledReceiveTransaction:', homeCom, receiverUser, pendingTx) + + // ensure that no other pendingTx with the same sender or recipient exists + const openSenderPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW }, + { userGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.SETTLED }, + { + linkedUserGradidoID: pendingTx.linkedUserGradidoID!, + state: PendingTransactionState.NEW, + }, + { + linkedUserGradidoID: pendingTx.linkedUserGradidoID!, + state: PendingTransactionState.SETTLED, + }, + ], + }) + const openReceiverPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW }, + { userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.SETTLED }, + { linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.SETTLED }, + ], + }) + if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) { + throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient') + } + + const lastTransaction = await getLastTransaction(receiverUser.id) + // now the last Tx must be the equivalant to the pendingTX + if ( + lastTransaction && + lastTransaction.balance === pendingTx.balance && + lastTransaction.balanceDate === pendingTx.balanceDate && + lastTransaction.userGradidoID === pendingTx.userGradidoID && + lastTransaction.userName === pendingTx.userName && + lastTransaction.amount === pendingTx.amount && + lastTransaction.memo === pendingTx.memo && + lastTransaction.linkedUserGradidoID === pendingTx.linkedUserGradidoID && + lastTransaction.linkedUserName === pendingTx.linkedUserName + ) { + await queryRunner.manager.remove(dbTransaction, lastTransaction) + logger.debug(`X-Com: revert settlement receive Transaction removed:`, lastTransaction) + // and mark the pendingTx in the pending_transactions table as reverted + pendingTx.state = PendingTransactionState.REVERTED + await queryRunner.manager.save(DbPendingTransaction, pendingTx) + + await queryRunner.commitTransaction() + logger.info(`commit revert settlement recipient Transaction successful...`) + } else { + // TODO: if the last TX is not equivelant to pendingTX, the transactions must be corrected in EXPERT-MODE + throw new LogError( + `X-Com: missmatching transaction order for revert settlement! lastTransation=${lastTransaction} != pendingTx=${pendingTx}`, + ) + } + + /* + await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount) + + await EVENT_TRANSACTION_RECEIVE( + recipient, + sender, + transactionReceive, + transactionReceive.amount, + ) + */ + // trigger to send transaction via dlt-connector + // void sendTransactionsToDltConnector() + } catch (e) { + await queryRunner.rollbackTransaction() + throw new LogError('X-Com: revert settlement recipient Transaction was not successful', e) + } finally { + await queryRunner.release() + releaseLock() + } + /* + void sendTransactionReceivedEmail({ + firstName: recipient.firstName, + lastName: recipient.lastName, + email: recipient.emailContact.email, + language: recipient.language, + senderFirstName: sender.firstName, + senderLastName: sender.lastName, + senderEmail: sender.emailContact.email, + transactionAmount: amount, + }) + if (transactionLink) { + void sendTransactionLinkRedeemedEmail({ + firstName: sender.firstName, + lastName: sender.lastName, + email: sender.emailContact.email, + language: sender.language, + senderFirstName: recipient.firstName, + senderLastName: recipient.lastName, + senderEmail: recipient.emailContact.email, + transactionAmount: amount, + transactionMemo: memo, + }) + } + logger.info(`finished executeTransaction successfully`) + } finally { + releaseLock() + } + */ + return true +} diff --git a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts index 938f3438f..0823e7d41 100644 --- a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts @@ -2,7 +2,7 @@ /* eslint-disable new-cap */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { getConnection, In } from '@dbTools/typeorm' +import { getConnection } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' import { Transaction as dbTransaction } from '@entity/Transaction' @@ -22,12 +22,13 @@ export async function settlePendingReceiveTransaction( receiverUser: DbUser, pendingTx: DbPendingTransaction, ): Promise { + // TODO: synchronisation with TRANSACTION_LOCK of backend-modul necessary!!! // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') - logger.debug(`open Transaction to write...`) + logger.debug(`start Transaction for write-access...`) try { logger.info('X-Com: settlePendingReceiveTransaction:', homeCom, receiverUser, pendingTx) @@ -57,6 +58,7 @@ export async function settlePendingReceiveTransaction( ) } + // transfer the pendingTx to the transactions table const transactionReceive = new dbTransaction() transactionReceive.typeId = pendingTx.typeId transactionReceive.memo = pendingTx.memo @@ -86,85 +88,12 @@ export async function settlePendingReceiveTransaction( await queryRunner.manager.insert(dbTransaction, transactionReceive) logger.debug(`receive Transaction inserted: ${dbTransaction}`) - /* - // validate amount - const receivedCallDate = new Date() - const sendBalance = await calculateBalance( - sender.id, - amount.mul(-1), - receivedCallDate, - transactionLink, - ) - logger.debug(`calculated Balance=${sendBalance}`) - if (!sendBalance) { - throw new LogError('User has not enough GDD or amount is < 0', sendBalance) - } + // and mark the pendingTx in the pending_transactions table as settled + pendingTx.state = PendingTransactionState.SETTLED + await queryRunner.manager.save(DbPendingTransaction, pendingTx) - const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction('REPEATABLE READ') - logger.debug(`open Transaction to write...`) - try { - // transaction - const transactionSend = new dbTransaction() - transactionSend.typeId = TransactionTypeId.SEND - transactionSend.memo = memo - transactionSend.userId = sender.id - transactionSend.userGradidoID = sender.gradidoID - transactionSend.userName = fullName(sender.firstName, sender.lastName) - transactionSend.linkedUserId = recipient.id - transactionSend.linkedUserGradidoID = recipient.gradidoID - transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName) - transactionSend.amount = amount.mul(-1) - transactionSend.balance = sendBalance.balance - transactionSend.balanceDate = receivedCallDate - transactionSend.decay = sendBalance.decay.decay - transactionSend.decayStart = sendBalance.decay.start - transactionSend.previous = sendBalance.lastTransactionId - transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null - await queryRunner.manager.insert(dbTransaction, transactionSend) - - logger.debug(`sendTransaction inserted: ${dbTransaction}`) - - const transactionReceive = new dbTransaction() - transactionReceive.typeId = TransactionTypeId.RECEIVE - transactionReceive.memo = memo - transactionReceive.userId = recipient.id - transactionReceive.userGradidoID = recipient.gradidoID - transactionReceive.userName = fullName(recipient.firstName, recipient.lastName) - transactionReceive.linkedUserId = sender.id - transactionReceive.linkedUserGradidoID = sender.gradidoID - transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName) - transactionReceive.amount = amount - const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) - transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount - transactionReceive.balanceDate = receivedCallDate - transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) - transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null - transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null - transactionReceive.linkedTransactionId = transactionSend.id - transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null - await queryRunner.manager.insert(dbTransaction, transactionReceive) - logger.debug(`receive Transaction inserted: ${dbTransaction}`) - - // Save linked transaction id for send - transactionSend.linkedTransactionId = transactionReceive.id - await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) - logger.debug('send Transaction updated', transactionSend) - - if (transactionLink) { - logger.info('transactionLink', transactionLink) - transactionLink.redeemedAt = receivedCallDate - transactionLink.redeemedBy = recipient.id - await queryRunner.manager.update( - dbTransactionLink, - { id: transactionLink.id }, - transactionLink, - ) - } - */ await queryRunner.commitTransaction() - logger.info(`commit Transaction successful...`) + logger.info(`commit recipient Transaction successful...`) /* await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount) @@ -180,7 +109,7 @@ export async function settlePendingReceiveTransaction( // void sendTransactionsToDltConnector() } catch (e) { await queryRunner.rollbackTransaction() - throw new LogError('Transaction was not successful', e) + throw new LogError('X-Com: recipient Transaction was not successful', e) } finally { await queryRunner.release() releaseLock() diff --git a/federation/tsconfig.json b/federation/tsconfig.json index 2326786ac..ba0d6fef3 100644 --- a/federation/tsconfig.json +++ b/federation/tsconfig.json @@ -53,13 +53,17 @@ // "@model/*": ["src/graphql/model/*"], "@repository/*": ["src/typeorm/repository/*"], "@test/*": ["test/*"], + /* common */ + "@common/*": ["../common/src/*"], + "@email/*": ["../common/scr/email/*"], + "@event/*": ["../common/src/event/*"], /* external */ "@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"], "@dbTools/*": ["../database/src/*", "../../database/build/src/*"], "@entity/*": ["../database/entity/*", "../../database/build/entity/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ diff --git a/federation/yarn.lock b/federation/yarn.lock index a811712fa..758e7e4ce 100644 --- a/federation/yarn.lock +++ b/federation/yarn.lock @@ -1529,6 +1529,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +await-semaphore@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" + integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q== + babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444"