From 6acf2f3c5b869709d6ad2726d86f4d027a4e68e2 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 29 Aug 2023 00:49:51 +0200 Subject: [PATCH] first draft for settlePendingReceiverTransaction --- .../federation/client/1_0/SendCoinsClient.ts | 15 +- .../client/1_0/query/settleSendCoins.ts | 25 +++ .../graphql/resolver/TransactionResolver.ts | 18 ++ .../resolver/util/processXComSendCoins.ts | 67 ++++++ federation/src/graphql/api/1_0/const/const.ts | 12 ++ .../api/1_0/resolver/SendCoinsResolver.ts | 93 +++++++- .../1_0}/util/calculateRecepientBalance.ts | 7 +- .../util/settlePendingReceiveTransaction.ts | 199 ++++++++++++++++++ .../src/graphql/util/TRANSACTIONS_LOCK.ts | 4 + 9 files changed, 427 insertions(+), 13 deletions(-) create mode 100644 backend/src/federation/client/1_0/query/settleSendCoins.ts create mode 100644 federation/src/graphql/api/1_0/const/const.ts rename federation/src/graphql/{ => api/1_0}/util/calculateRecepientBalance.ts (76%) create mode 100644 federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts create mode 100644 federation/src/graphql/util/TRANSACTIONS_LOCK.ts diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index c57ca4823..d26ad80ee 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -7,6 +7,7 @@ import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' import { revertSendCoins } from './query/revertSendCoins' import { voteForSendCoins } from './query/voteForSendCoins' +import { settleSendCoins } from './query/settleSendCoins' // eslint-disable-next-line camelcase export class SendCoinsClient { @@ -72,22 +73,20 @@ export class SendCoinsClient { } } - /* - commitSendCoins = async (args: SendCoinsArgs): Promise => { - logger.debug(`X-Com: commitSendCoins against endpoint='${this.endpoint}'...`) + settleSendCoins = async (args: SendCoinsArgs): Promise => { + logger.debug(`X-Com: settleSendCoins against endpoint='${this.endpoint}'...`) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(commitSendCoins, { args }) + const { data } = await this.client.rawRequest(settleSendCoins, { args }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (!data?.commitSendCoins?.acknowledged) { - logger.warn('X-Com: commitSendCoins without response data from endpoint', this.endpoint) + logger.warn('X-Com: settleSendCoins without response data from endpoint', this.endpoint) return false } - logger.debug(`X-Com: commitSendCoins successful from endpoint=${this.endpoint}`) + logger.debug(`X-Com: settleSendCoins successful from endpoint=${this.endpoint}`) return true } catch (err) { - throw new LogError(`X-Com: commitSendCoins failed for endpoint=${this.endpoint}`, err) + throw new LogError(`X-Com: settleSendCoins failed for endpoint=${this.endpoint}`, err) } } - */ } diff --git a/backend/src/federation/client/1_0/query/settleSendCoins.ts b/backend/src/federation/client/1_0/query/settleSendCoins.ts new file mode 100644 index 000000000..99f784bc7 --- /dev/null +++ b/backend/src/federation/client/1_0/query/settleSendCoins.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-request' + +export const settleSendCoins = gql` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $creationDate: Date! + $amount: Decimal! + $memo: String! + $communitySenderIdentifier: String! + $userSenderIdentifier: String! + $userSenderName: String! + ) { + settleSendCoins( + communityReceiverIdentifier: $communityReceiverIdentifier + userReceiverIdentifier: $userReceiverIdentifier + creationDate: $creationDate + amount: $amount + memo: $memo + communitySenderIdentifier: $communitySenderIdentifier + userSenderIdentifier: $userSenderIdentifier + userSenderName: $userSenderName + ) + } +` diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ba5d6e155..2a3dd645a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -3,6 +3,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { getConnection, In } from '@dbTools/typeorm' +import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' import { Transaction as dbTransaction } from '@entity/Transaction' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { User as dbUser } from '@entity/User' @@ -12,6 +13,7 @@ import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { Paginated } from '@arg/Paginated' import { TransactionSendArgs } from '@arg/TransactionSendArgs' import { Order } from '@enum/Order' +import { PendingTransactionState } from '@enum/PendingTransactionState' import { TransactionTypeId } from '@enum/TransactionTypeId' import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' @@ -52,6 +54,22 @@ export const executeTransaction = async ( try { logger.info('executeTransaction', amount, memo, sender, recipient) + const openSenderPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: sender.gradidoID, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: sender.gradidoID, state: PendingTransactionState.NEW }, + ], + }) + const openReceiverPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW }, + ], + }) + if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) { + throw new LogError('There are still pending Transactions for Sender and/or Recipient') + } + if (sender.id === recipient.id) { throw new LogError('Sender and Recipient are the same', sender.id) } diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 107f65cde..ef37685a9 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -114,3 +114,70 @@ export async function processXComPendingSendCoins( } return true } + +export async function processXComCommittingSendCoins( + receiverFCom: DbFederatedCommunity, + receiverCom: DbCommunity, + senderCom: DbCommunity, + creationDate: Date, + amount: Decimal, + memo: string, + sender: dbUser, + recipient: dbUser, +): Promise { + try { + logger.debug( + `XCom: processXComCommittingSendCoins...`, + receiverFCom, + receiverCom, + senderCom, + creationDate, + amount, + memo, + sender, + recipient, + ) + // first find pending Tx with given parameters + const pendingTx = await DbPendingTransaction.findOneBy({ + userCommunityUuid: senderCom.communityUuid ? senderCom.communityUuid : 'homeCom-UUID', + userGradidoID: sender.gradidoID, + userName: fullName(sender.firstName, sender.lastName), + linkedUserCommunityUuid: receiverCom.communityUuid + ? receiverCom.communityUuid + : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID, + linkedUserGradidoID: recipient.gradidoID, + typeId: TransactionTypeId.SEND, + state: PendingTransactionState.NEW, + balanceDate: creationDate, + memo, + }) + if (pendingTx) { + logger.debug(`X-Com: find pending Tx for settlement:`, pendingTx) + const client = SendCoinsClientFactory.getInstance(receiverFCom) + // eslint-disable-next-line camelcase + if (client instanceof V1_0_SendCoinsClient) { + const args = new SendCoinsArgs() + args.communityReceiverIdentifier = pendingTx.linkedUserCommunityUuid + ? pendingTx.linkedUserCommunityUuid + : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID + if (pendingTx.linkedUserGradidoID) { + args.userReceiverIdentifier = pendingTx.linkedUserGradidoID + } + args.creationDate = pendingTx.balanceDate + args.amount = pendingTx.amount + args.memo = pendingTx.memo + args.communitySenderIdentifier = pendingTx.userCommunityUuid + args.userSenderIdentifier = pendingTx.userGradidoID + if (pendingTx.userName) { + args.userSenderName = pendingTx.userName + } + logger.debug(`X-Com: ready for settleSendCoins with args=`, args) + const acknoleged = await client.settleSendCoins(args) + logger.debug(`X-Com: returnd from settleSendCoins:`, acknoleged) + } + } + } catch (err) { + logger.error(`Error:`, err) + } + return true +} diff --git a/federation/src/graphql/api/1_0/const/const.ts b/federation/src/graphql/api/1_0/const/const.ts new file mode 100644 index 000000000..b97694221 --- /dev/null +++ b/federation/src/graphql/api/1_0/const/const.ts @@ -0,0 +1,12 @@ +import { Decimal } from 'decimal.js-light' + +export const MAX_CREATION_AMOUNT = new Decimal(1000) +export const FULL_CREATION_AVAILABLE = [ + MAX_CREATION_AMOUNT, + MAX_CREATION_AMOUNT, + MAX_CREATION_AMOUNT, +] +export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100 +export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5 +export const MEMO_MAX_CHARS = 255 +export const MEMO_MIN_CHARS = 5 diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index d03f1e4f0..97c59f3ba 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -8,9 +8,11 @@ import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' import { PendingTransactionState } from '../enum/PendingTransactionState' import { TransactionTypeId } from '../enum/TransactionTypeId' -import { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance' +import { calculateRecepientBalance } from '../util/calculateRecepientBalance' import Decimal from 'decimal.js-light' import { fullName } from '@/graphql/util/fullName' +import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '../const/const' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -50,6 +52,14 @@ export class SendCoinsResolver { homeCom.name, ) } + if (memo.length < MEMO_MIN_CHARS) { + throw new LogError('Memo text is too short', memo.length) + } + + if (memo.length > MEMO_MAX_CHARS) { + throw new LogError('Memo text is too long', memo.length) + } + const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, creationDate) const pendingTx = DbPendingTransaction.create() pendingTx.amount = amount @@ -151,4 +161,85 @@ export class SendCoinsResolver { throw new LogError(`Error in revertSendCoins: `, err) } } + + @Mutation(() => Boolean) + async settleSendCoins( + @Args() + { + communityReceiverIdentifier, + userReceiverIdentifier, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + }: SendCoinsArgs, + ): 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, + state: PendingTransactionState.NEW, + typeId: TransactionTypeId.RECEIVE, + balanceDate: creationDate, + linkedUserCommunityUuid: communitySenderIdentifier, + linkedUserGradidoID: userSenderIdentifier, + }) + 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) + } + } else { + logger.debug( + 'XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...', + ) + throw new LogError( + `Can't find in settlePendingReceiveTransaction the pending receiver TX for args=`, + communityReceiverIdentifier, + userReceiverIdentifier, + PendingTransactionState.NEW, + TransactionTypeId.RECEIVE, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + ) + } + logger.debug(`settlePendingReceiveTransaction()-1_0... successfull`) + return true + } catch (err) { + throw new LogError(`Error in settlePendingReceiveTransaction: `, err) + } + } } diff --git a/federation/src/graphql/util/calculateRecepientBalance.ts b/federation/src/graphql/api/1_0/util/calculateRecepientBalance.ts similarity index 76% rename from federation/src/graphql/util/calculateRecepientBalance.ts rename to federation/src/graphql/api/1_0/util/calculateRecepientBalance.ts index 56914afba..228862041 100644 --- a/federation/src/graphql/util/calculateRecepientBalance.ts +++ b/federation/src/graphql/api/1_0/util/calculateRecepientBalance.ts @@ -1,8 +1,7 @@ +import { calculateDecay } from '@/graphql/util/decay' +import { getLastTransaction } from '@/graphql/util/getLastTransaction' import { Decimal } from 'decimal.js-light' - -import { getLastTransaction } from './getLastTransaction' -import { calculateDecay } from './decay' -import { Decay } from '../api/1_0/model/Decay' +import { Decay } from '../model/Decay' export async function calculateRecepientBalance( userId: number, diff --git a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts new file mode 100644 index 000000000..4c6334074 --- /dev/null +++ b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts @@ -0,0 +1,199 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable new-cap */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { getConnection, In } 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 { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { User as DbUser } from '@entity/User' +import { Decimal } from 'decimal.js-light' + +import { PendingTransactionState } from '../enum/PendingTransactionState' +import { TransactionTypeId } from '../enum/TransactionTypeId' + +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 settlePendingReceiveTransaction( + homeCom: DbCommunity, + receiverUser: DbUser, + pendingTx: DbPendingTransaction, +): Promise { + // acquire lock + const releaseLock = await TRANSACTIONS_LOCK.acquire() + try { + logger.info('X-Com: settlePendingReceiveTransaction:', homeCom, receiverUser, pendingTx) + + 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: userReceiverIdentifier, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: userReceiverIdentifier, state: PendingTransactionState.NEW }, + ], + }) + if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) { + throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient') + } + + if ( + communityReceiverIdentifier === communitySenderIdentifier && + communitySenderIdentifier === userSenderIdentifier + ) { + throw new LogError('Sender and Recipient are the same user: ', userSenderName) + } + + if (memo.length < MEMO_MIN_CHARS) { + throw new LogError('Memo text is too short', memo.length) + } + + if (memo.length > MEMO_MAX_CHARS) { + throw new LogError('Memo text is too long', memo.length) + } + + const recipientUser = await DbUser.findOneByOrFail({ gradidoID: userReceiverIdentifier }) + const lastTransaction = await getLastTransaction(recipientUser.id) + const pendingTx = await DbPendingTransaction.findOneByOrFail({ + userId: recipientUser.id, + userGradidoID: recipientUser.gradidoID, + }) + + if (lastTransaction?.id !== pendingTx.previous) { + throw new LogError( + `X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}`, + ) + } + // 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) + } + + 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...`) + + 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('Transaction was not successful', e) + } finally { + await queryRunner.release() + } + 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/util/TRANSACTIONS_LOCK.ts b/federation/src/graphql/util/TRANSACTIONS_LOCK.ts new file mode 100644 index 000000000..847386e4d --- /dev/null +++ b/federation/src/graphql/util/TRANSACTIONS_LOCK.ts @@ -0,0 +1,4 @@ +import { Semaphore } from 'await-semaphore' + +const CONCURRENT_TRANSACTIONS = 1 +export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)