diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index 3f22fdc73..90c3e2e20 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -8,6 +8,7 @@ import { SendCoinsArgs } from './model/SendCoinsArgs' import { revertSendCoins } from './query/revertSendCoins' import { settleSendCoins } from './query/settleSendCoins' import { voteForSendCoins } from './query/voteForSendCoins' +import { revertSettledSendCoins } from './query/revertSettledSendCoins' // eslint-disable-next-line camelcase export class SendCoinsClient { @@ -79,7 +80,7 @@ export class SendCoinsClient { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(settleSendCoins, { args }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.commitSendCoins?.acknowledged) { + if (!data?.settleSendCoins?.acknowledged) { logger.warn('X-Com: settleSendCoins without response data from endpoint', this.endpoint) return false } @@ -89,4 +90,24 @@ export class SendCoinsClient { throw new LogError(`X-Com: settleSendCoins failed for endpoint=${this.endpoint}`, err) } } + + revertSettledSendCoins = async (args: SendCoinsArgs): Promise => { + logger.debug(`X-Com: revertSettledSendCoins against endpoint='${this.endpoint}'...`) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(revertSettledSendCoins, { args }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.revertSettledSendCoins?.acknowledged) { + logger.warn( + 'X-Com: revertSettledSendCoins without response data from endpoint', + this.endpoint, + ) + return false + } + logger.debug(`X-Com: revertSettledSendCoins successful from endpoint=${this.endpoint}`) + return true + } catch (err) { + throw new LogError(`X-Com: revertSettledSendCoins failed for endpoint=${this.endpoint}`, err) + } + } } diff --git a/backend/src/federation/client/1_0/query/revertSettledSendCoins.ts b/backend/src/federation/client/1_0/query/revertSettledSendCoins.ts new file mode 100644 index 000000000..0d4447507 --- /dev/null +++ b/backend/src/federation/client/1_0/query/revertSettledSendCoins.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-request' + +export const revertSettledSendCoins = gql` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $creationDate: Date! + $amount: Decimal! + $memo: String! + $communitySenderIdentifier: String! + $userSenderIdentifier: String! + $userSenderName: String! + ) { + revertSettledSendCoins( + communityReceiverIdentifier: $communityReceiverIdentifier + userReceiverIdentifier: $userReceiverIdentifier + creationDate: $creationDate + amount: $amount + memo: $memo + communitySenderIdentifier: $communitySenderIdentifier + userSenderIdentifier: $userSenderIdentifier + userSenderName: $userSenderName + ) + } +` diff --git a/backend/src/graphql/enum/PendingTransactionState.ts b/backend/src/graphql/enum/PendingTransactionState.ts index 6a614be96..d89b0b0eb 100644 --- a/backend/src/graphql/enum/PendingTransactionState.ts +++ b/backend/src/graphql/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/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index ef37685a9..40b7dd775 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -15,6 +15,7 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { calculateSenderBalance } from '@/util/calculateSenderBalance' import { fullName } from '@/util/utilities' +import { settlePendingSenderTransaction } from './settlePendingSenderTransaction' export async function processXComPendingSendCoins( receiverFCom: DbFederatedCommunity, @@ -174,6 +175,31 @@ export async function processXComCommittingSendCoins( logger.debug(`X-Com: ready for settleSendCoins with args=`, args) const acknoleged = await client.settleSendCoins(args) logger.debug(`X-Com: returnd from settleSendCoins:`, acknoleged) + if (acknoleged) { + // settle the pending transaction on receiver-side was successfull, so now settle the sender side + try { + await settlePendingSenderTransaction(senderCom, sender, pendingTx) + } catch (err) { + logger.error(`Error in writing sender pending transaction: `, err) + // revert the existing pending transaction on receiver side + let revertCount = 0 + logger.debug(`X-Com: first try to revertSetteledSendCoins of receiver`) + do { + if (await client.revertSettledSendCoins(args)) { + logger.debug( + `revertSettledSendCoins()-1_0... successfull after revertCount=`, + revertCount, + ) + // treat revertingSettledSendCoins as an error of the whole sendCoins-process + throw new LogError('Error in settle sender pending transaction: `, err') + } + } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) + throw new LogError( + `Error in reverting receiver pending transaction even after revertCount=`, + revertCount, + ) + } + } } } } catch (err) { diff --git a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts new file mode 100644 index 000000000..8db8ba695 --- /dev/null +++ b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts @@ -0,0 +1,146 @@ +/* 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 { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' +import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' + +import { getLastTransaction } from './getLastTransaction' +import { calculateSenderBalance } from '@/util/calculateSenderBalance' +import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState' + +export async function settlePendingSenderTransaction( + homeCom: DbCommunity, + senderUser: DbUser, + pendingTx: DbPendingTransaction, +): Promise { + // TODO: synchronisation with TRANSACTION_LOCK of federation-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: settlePendingSenderTransaction:', homeCom, senderUser, 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 }, + { linkedUserGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW }, + ], + }) + const openReceiverPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW }, + ], + }) + if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) { + throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient') + } + + const lastTransaction = await getLastTransaction(senderUser.id) + + if (lastTransaction?.id !== pendingTx.previous) { + throw new LogError( + `X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}`, + ) + } + + // transfer the pendingTx to the transactions table + const transactionSend = new dbTransaction() + transactionSend.typeId = pendingTx.typeId + transactionSend.memo = pendingTx.memo + transactionSend.userId = pendingTx.userId + transactionSend.userGradidoID = pendingTx.userGradidoID + transactionSend.userName = pendingTx.userName + transactionSend.linkedUserId = pendingTx.linkedUserId + transactionSend.linkedUserGradidoID = pendingTx.linkedUserGradidoID + transactionSend.linkedUserName = pendingTx.linkedUserName + transactionSend.amount = pendingTx.amount + const sendBalance = await calculateSenderBalance( + senderUser.id, + pendingTx.amount, + pendingTx.balanceDate, + ) + if (sendBalance?.balance !== pendingTx.balance) { + throw new LogError( + `X-Com: Calculation-Error on receiver balance: receiveBalance=${sendBalance?.balance}, pendingTx.balance=${pendingTx.balance}`, + ) + } + transactionSend.balance = pendingTx.balance + transactionSend.balanceDate = pendingTx.balanceDate + transactionSend.decay = pendingTx.decay + transactionSend.decayStart = pendingTx.decayStart + transactionSend.previous = pendingTx.previous + transactionSend.linkedTransactionId = pendingTx.linkedTransactionId + await queryRunner.manager.insert(dbTransaction, transactionSend) + logger.debug(`send Transaction inserted: ${dbTransaction}`) + + // and mark the pendingTx in the pending_transactions table as settled + pendingTx.state = PendingTransactionState.SETTLED + await queryRunner.manager.save(DbPendingTransaction, pendingTx) + + await queryRunner.commitTransaction() + logger.info(`commit send Transaction successful...`) + + /* + 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: send 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 +}