diff --git a/backend/jest.config.js b/backend/jest.config.js index 1529fad55..8b322d76c 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 86, + lines: 85, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 26e759c47..7c849f7d9 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,7 +12,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0071-add-pending_transactions-table', + DB_VERSION: '0072-add_communityuuid_to_transactions_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index c57ca4823..3b3bf4f2c 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -5,7 +5,10 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' +import { SendCoinsResult } from './model/SendCoinsResult' import { revertSendCoins } from './query/revertSendCoins' +import { revertSettledSendCoins } from './query/revertSettledSendCoins' +import { settleSendCoins } from './query/settleSendCoins' import { voteForSendCoins } from './query/voteForSendCoins' // eslint-disable-next-line camelcase @@ -20,7 +23,7 @@ export class SendCoinsClient { dbCom.apiVersion }/` this.client = new GraphQLClient(this.endpoint, { - method: 'GET', + method: 'POST', jsonSerializer: { parse: JSON.parse, stringify: JSON.stringify, @@ -28,27 +31,26 @@ export class SendCoinsClient { }) } - voteForSendCoins = async (args: SendCoinsArgs): Promise => { + voteForSendCoins = async (args: SendCoinsArgs): Promise => { logger.debug('X-Com: voteForSendCoins against endpoint=', this.endpoint) try { + logger.debug(`X-Com: SendCoinsClient: voteForSendCoins with args=`, args) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(voteForSendCoins, { args }) + logger.debug(`X-Com: SendCoinsClient: after rawRequest...data:`, data) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.voteForSendCoins?.voteForSendCoins) { - logger.warn( - 'X-Com: voteForSendCoins failed with: ', - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - data.voteForSendCoins.voteForSendCoins, - ) - return + if (!data?.voteForSendCoins?.vote) { + logger.warn('X-Com: voteForSendCoins failed with: ', data) + return new SendCoinsResult() } - logger.debug( - 'X-Com: voteForSendCoins successful with result=', - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - data.voteForSendCoins, - ) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - return data.voteForSendCoins.voteForSendCoins + const result = new SendCoinsResult() + result.vote = true + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + result.recipGradidoID = data.voteForSendCoins.recipGradidoID + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + result.recipName = data.voteForSendCoins.recipName + logger.debug('X-Com: voteForSendCoins successful with result=', result) + return result } catch (err) { throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err) } @@ -57,37 +59,79 @@ export class SendCoinsClient { revertSendCoins = async (args: SendCoinsArgs): Promise => { logger.debug('X-Com: revertSendCoins against endpoint=', this.endpoint) try { + logger.debug(`X-Com: SendCoinsClient: revertSendCoins with args=`, args) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(revertSendCoins, { args }) + logger.debug(`X-Com: SendCoinsClient: after revertSendCoins: data=`, data) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.revertSendCoins?.revertSendCoins) { + if (!data?.revertSendCoins) { logger.warn('X-Com: revertSendCoins without response data from endpoint', this.endpoint) return false } - logger.debug(`X-Com: revertSendCoins successful from endpoint=${this.endpoint}`) + logger.debug( + `X-Com: SendCoinsClient: revertSendCoins successful from endpoint=${this.endpoint}`, + ) return true } catch (err) { - logger.error(`X-Com: revertSendCoins failed for endpoint=${this.endpoint}`, err) + logger.error( + `X-Com: SendCoinsClient: revertSendCoins failed for endpoint=${this.endpoint}`, + err, + ) return false } } - /* - 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 { + logger.debug(`X-Com: SendCoinsClient: settleSendCoins with args=`, args) // 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 }) + logger.debug(`X-Com: SendCoinsClient: after settleSendCoins: data=`, data) // 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) + if (!data?.settleSendCoins) { + logger.warn( + 'X-Com: SendCoinsClient: settleSendCoins without response data from endpoint', + this.endpoint, + ) return false } - logger.debug(`X-Com: commitSendCoins successful from endpoint=${this.endpoint}`) + logger.debug( + `X-Com: SendCoinsClient: 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: SendCoinsClient: settleSendCoins failed for endpoint=${this.endpoint}`, + err, + ) + } + } + + revertSettledSendCoins = async (args: SendCoinsArgs): Promise => { + logger.debug(`X-Com: revertSettledSendCoins against endpoint='${this.endpoint}'...`) + try { + logger.debug(`X-Com: SendCoinsClient: revertSettledSendCoins with args=`, args) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(revertSettledSendCoins, { args }) + logger.debug(`X-Com: SendCoinsClient: after revertSettledSendCoins: data=`, data) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.revertSettledSendCoins) { + logger.warn( + 'X-Com: SendCoinsClient: revertSettledSendCoins without response data from endpoint', + this.endpoint, + ) + return false + } + logger.debug( + `X-Com: SendCoinsClient: revertSettledSendCoins successful from endpoint=${this.endpoint}`, + ) + return true + } catch (err) { + throw new LogError( + `X-Com: SendCoinsClient: revertSettledSendCoins failed for endpoint=${this.endpoint}`, + err, + ) } } - */ } diff --git a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts index 545aab822..fb97da925 100644 --- a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts +++ b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts @@ -4,10 +4,10 @@ import { ArgsType, Field } from 'type-graphql' @ArgsType() export class SendCoinsArgs { @Field(() => String) - communityReceiverIdentifier: string + recipientCommunityUuid: string @Field(() => String) - userReceiverIdentifier: string + recipientUserIdentifier: string @Field(() => String) creationDate: string @@ -19,11 +19,11 @@ export class SendCoinsArgs { memo: string @Field(() => String) - communitySenderIdentifier: string + senderCommunityUuid: string @Field(() => String) - userSenderIdentifier: string + senderUserUuid: string @Field(() => String) - userSenderName: string + senderUserName: string } diff --git a/backend/src/federation/client/1_0/model/SendCoinsResult.ts b/backend/src/federation/client/1_0/model/SendCoinsResult.ts index 1897410cc..930d22ff5 100644 --- a/backend/src/federation/client/1_0/model/SendCoinsResult.ts +++ b/backend/src/federation/client/1_0/model/SendCoinsResult.ts @@ -1,6 +1,6 @@ -import { ArgsType, Field } from 'type-graphql' +import { Field, ObjectType } from 'type-graphql' -@ArgsType() +@ObjectType() export class SendCoinsResult { constructor() { this.vote = false @@ -9,9 +9,9 @@ export class SendCoinsResult { @Field(() => Boolean) vote: boolean - @Field(() => String) - receiverFirstName: string + @Field(() => String, { nullable: true }) + recipGradidoID: string | null - @Field(() => String) - receiverLastName: string + @Field(() => String, { nullable: true }) + recipName: string | null } diff --git a/backend/src/federation/client/1_0/query/revertSendCoins.ts b/backend/src/federation/client/1_0/query/revertSendCoins.ts index 881107cb4..ea7d28f77 100644 --- a/backend/src/federation/client/1_0/query/revertSendCoins.ts +++ b/backend/src/federation/client/1_0/query/revertSendCoins.ts @@ -1,25 +1,30 @@ import { gql } from 'graphql-request' export const revertSendCoins = gql` + mutation ($args: SendCoinsArgs!) { + revertSendCoins(data: $args) + } +` +/* mutation ( - $communityReceiverIdentifier: String! - $userReceiverIdentifier: String! + $recipientCommunityUuid: String! + $recipientUserIdentifier: String! $creationDate: String! $amount: Decimal! $memo: String! - $communitySenderIdentifier: String! - $userSenderIdentifier: String! - $userSenderName: String! + $senderCommunityUuid: String! + $senderUserUuid: String! + $senderUserName: String! ) { revertSendCoins( - communityReceiverIdentifier: $communityReceiverIdentifier - userReceiverIdentifier: $userReceiverIdentifier + recipientCommunityUuid: $recipientCommunityUuid + recipientUserIdentifier: $recipientUserIdentifier creationDate: $creationDate amount: $amount memo: $memo - communitySenderIdentifier: $communitySenderIdentifier - userSenderIdentifier: $userSenderIdentifier - userSenderName: $userSenderName + senderCommunityUuid: $senderCommunityUuid + senderUserUuid: $senderUserUuid + senderUserName: $senderUserName ) } -` +*/ 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..3965df396 --- /dev/null +++ b/backend/src/federation/client/1_0/query/revertSettledSendCoins.ts @@ -0,0 +1,31 @@ +import { gql } from 'graphql-request' + +export const revertSettledSendCoins = gql` + mutation ($args: SendCoinsArgs!) { + revertSettledSendCoins(data: $args) + } +` +/* + + mutation ( + $recipientCommunityUuid: String! + $recipientUserIdentifier: String! + $creationDate: String! + $amount: Decimal! + $memo: String! + $senderCommunityUuid: String! + $senderUserUuid: String! + $senderUserName: String! + ) { + revertSettledSendCoins( + recipientCommunityUuid: $recipientCommunityUuid + recipientUserIdentifier: $recipientUserIdentifier + creationDate: $creationDate + amount: $amount + memo: $memo + senderCommunityUuid: $senderCommunityUuid + senderUserUuid: $senderUserUuid + senderUserName: $senderUserName + ) + } +*/ 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..f5143b27d --- /dev/null +++ b/backend/src/federation/client/1_0/query/settleSendCoins.ts @@ -0,0 +1,30 @@ +import { gql } from 'graphql-request' + +export const settleSendCoins = gql` + mutation ($args: SendCoinsArgs!) { + settleSendCoins(data: $args) + } +` +/* + mutation ( + $recipientCommunityUuid: String! + $recipientUserIdentifier: String! + $creationDate: String! + $amount: Decimal! + $memo: String! + $senderCommunityUuid: String! + $senderUserUuid: String! + $senderUserName: String! + ) { + settleSendCoins( + recipientCommunityUuid: $recipientCommunityUuid + recipientUserIdentifier: $recipientUserIdentifier + creationDate: $creationDate + amount: $amount + memo: $memo + senderCommunityUuid: $senderCommunityUuid + senderUserUuid: $senderUserUuid + senderUserName: $senderUserName + ) + } +*/ diff --git a/backend/src/federation/client/1_0/query/voteForSendCoins.ts b/backend/src/federation/client/1_0/query/voteForSendCoins.ts index f0f75198f..a5456bec7 100644 --- a/backend/src/federation/client/1_0/query/voteForSendCoins.ts +++ b/backend/src/federation/client/1_0/query/voteForSendCoins.ts @@ -1,25 +1,41 @@ import { gql } from 'graphql-request' export const voteForSendCoins = gql` + mutation ($args: SendCoinsArgs!) { + voteForSendCoins(data: $args) { + vote + recipGradidoID + recipName + } + } +` + +/* mutation ( - $communityReceiverIdentifier: String! - $userReceiverIdentifier: String! + $recipientCommunityUuid: String! + $recipientUserIdentifier: String! $creationDate: String! $amount: Decimal! $memo: String! - $communitySenderIdentifier: String! - $userSenderIdentifier: String! - $userSenderName: String! + $senderCommunityUuid: String! + $senderUserUuid: String! + $senderUserName: String! ) { voteForSendCoins( - communityReceiverIdentifier: $communityReceiverIdentifier - userReceiverIdentifier: $userReceiverIdentifier - creationDate: $creationDate - amount: $amount - memo: $memo - communitySenderIdentifier: $communitySenderIdentifier - userSenderIdentifier: $userSenderIdentifier - userSenderName: $userSenderName - ) + data: { + recipientCommunityUuid: $recipientCommunityUuid + recipientUserIdentifier: $recipientUserIdentifier + creationDate: $creationDate + amount: $amount + memo: $memo + senderCommunityUuid: $senderCommunityUuid + senderUserUuid: $senderUserUuid + senderUserName: $senderUserName + } + ) { + vote + recipGradidoID + recipName + } } -` +*/ 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/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 32ec5a654..63134a9a8 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -165,6 +165,7 @@ export class TransactionLinkResolver { @Ctx() context: Context, ): Promise { const clientTimezoneOffset = getClientTimezoneOffset(context) + // const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } }) const user = getUser(context) if (code.match(/^CL-/)) { @@ -271,6 +272,11 @@ export class TransactionLinkResolver { transaction.typeId = TransactionTypeId.CREATION transaction.memo = contribution.memo transaction.userId = contribution.userId + /* local transaction will not carry homeComUuid for local users + if (homeCom.communityUuid) { + transaction.userCommunityUuid = homeCom.communityUuid + } + */ transaction.userGradidoID = user.gradidoID transaction.userName = fullName(user.firstName, user.lastName) transaction.previous = lastTransaction ? lastTransaction.id : null diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 68a49301b..f368abca3 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, IsNull } 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' @@ -51,6 +53,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) } @@ -330,13 +348,12 @@ export class TransactionResolver { @Mutation(() => Boolean) async sendCoins( @Args() - { /* recipientCommunityIdentifier, */ recipientIdentifier, amount, memo }: TransactionSendArgs, + { recipientCommunityIdentifier, recipientIdentifier, amount, memo }: TransactionSendArgs, @Ctx() context: Context, ): Promise { logger.info( - `sendCoins(recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`, + `sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`, ) - const senderUser = getUser(context) // validate recipient user diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index b39985aaa..5befe2192 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -1,3 +1,4 @@ +/* import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' @@ -16,6 +17,8 @@ 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, receiverCom: DbCommunity, @@ -26,6 +29,7 @@ export async function processXComPendingSendCoins( sender: dbUser, recipient: dbUser, ): Promise { + try { logger.debug( `XCom: processXComPendingSendCoins...`, @@ -49,22 +53,20 @@ export async function processXComPendingSendCoins( // eslint-disable-next-line camelcase if (client instanceof V1_0_SendCoinsClient) { const args = new SendCoinsArgs() - args.communityReceiverIdentifier = receiverCom.communityUuid + args.recipientCommunityUuid = receiverCom.communityUuid ? receiverCom.communityUuid : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID - args.userReceiverIdentifier = recipient.gradidoID + args.recipientUserIdentifier = recipient.gradidoID args.creationDate = creationDate.toISOString() args.amount = amount args.memo = memo - args.communitySenderIdentifier = senderCom.communityUuid - ? senderCom.communityUuid - : 'homeCom-UUID' - args.userSenderIdentifier = sender.gradidoID - args.userSenderName = fullName(sender.firstName, sender.lastName) + args.senderCommunityUuid = senderCom.communityUuid ? senderCom.communityUuid : 'homeCom-UUID' + args.senderUserUuid = sender.gradidoID + args.senderUserName = fullName(sender.firstName, sender.lastName) logger.debug(`X-Com: ready for voteForSendCoins with args=`, args) - const recipientName = await client.voteForSendCoins(args) - logger.debug(`X-Com: returnd from voteForSendCoins:`, recipientName) - if (recipientName) { + const sendCoinsResult = await client.voteForSendCoins(args) + logger.debug(`X-Com: returnd from voteForSendCoins:`, sendCoinsResult) + if (sendCoinsResult) { // writing the pending transaction on receiver-side was successfull, so now write the sender side try { const pendingTx = DbPendingTransaction.create() @@ -77,7 +79,7 @@ export async function processXComPendingSendCoins( ? receiverCom.communityUuid : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID pendingTx.linkedUserGradidoID = recipient.gradidoID - pendingTx.linkedUserName = recipientName + pendingTx.linkedUserName = sendCoinsResult.recipName pendingTx.memo = memo pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null pendingTx.state = PendingTransactionState.NEW @@ -114,3 +116,96 @@ 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.recipientCommunityUuid = pendingTx.linkedUserCommunityUuid + ? pendingTx.linkedUserCommunityUuid + : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID + if (pendingTx.linkedUserGradidoID) { + args.recipientUserIdentifier = pendingTx.linkedUserGradidoID + } + args.creationDate = pendingTx.balanceDate.toISOString() + args.amount = pendingTx.amount + args.memo = pendingTx.memo + args.senderCommunityUuid = pendingTx.userCommunityUuid + args.senderUserUuid = pendingTx.userGradidoID + if (pendingTx.userName) { + args.senderUserName = pendingTx.userName + } + logger.debug(`X-Com: ready for settleSendCoins with args=`, args) + const acknowledge = await client.settleSendCoins(args) + logger.debug(`X-Com: returnd from settleSendCoins:`, acknowledge) + if (acknowledge) { + // 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) { + logger.error(`Error:`, err) + } + return true +} +*/ diff --git a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts new file mode 100644 index 000000000..c10855f93 --- /dev/null +++ b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts @@ -0,0 +1,147 @@ +/* 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 '@/graphql/enum/PendingTransactionState' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' +import { calculateSenderBalance } from '@/util/calculateSenderBalance' +import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' + +import { getLastTransaction } from './getLastTransaction' + +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 +} +*/ diff --git a/backend/src/seeds/community/index.ts b/backend/src/seeds/community/index.ts new file mode 100644 index 000000000..6a639ee44 --- /dev/null +++ b/backend/src/seeds/community/index.ts @@ -0,0 +1,34 @@ +import { Community as DbCommunity } from '@entity/Community' +import { v4 as uuidv4 } from 'uuid' + +import { CONFIG } from '@/config' + +export async function writeHomeCommunityEntry(): Promise { + try { + // check for existing homeCommunity entry + let homeCom = await DbCommunity.findOne({ where: { foreign: false } }) + if (homeCom) { + // simply update the existing entry, but it MUST keep the ID and UUID because of possible relations + homeCom.publicKey = Buffer.from('public-key-data-seeding') // keyPair.publicKey + // homeCom.privateKey = keyPair.secretKey + homeCom.url = 'http://localhost/api/' + homeCom.name = CONFIG.COMMUNITY_NAME + homeCom.description = CONFIG.COMMUNITY_DESCRIPTION + await DbCommunity.save(homeCom) + } else { + // insert a new homecommunity entry including a new ID and a new but ensured unique UUID + homeCom = new DbCommunity() + homeCom.foreign = false + homeCom.publicKey = Buffer.from('public-key-data-seeding') // keyPair.publicKey + // homeCom.privateKey = keyPair.secretKey + homeCom.communityUuid = uuidv4() // await newCommunityUuid() + homeCom.url = 'http://localhost/api/' + homeCom.name = CONFIG.COMMUNITY_NAME + homeCom.description = CONFIG.COMMUNITY_DESCRIPTION + homeCom.creationDate = new Date() + await DbCommunity.insert(homeCom) + } + } catch (err) { + throw new Error(`Seeding: Error writing HomeCommunity-Entry`) // : ${err}`) + } +} diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index bc7950f26..b22409759 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -12,6 +12,7 @@ import { CONFIG } from '@/config' import { createServer } from '@/server/createServer' import { backendLogger as logger } from '@/server/logger' +import { writeHomeCommunityEntry } from './community' import { contributionLinks } from './contributionLink/index' import { creations } from './creation/index' import { contributionLinkFactory } from './factory/contributionLink' @@ -57,6 +58,9 @@ const run = async () => { await cleanDB() logger.info('##seed## clean database successful...') + // seed home community + await writeHomeCommunityEntry() + // seed the standard users for (const user of users) { await userFactory(seedClient, user) diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index a10e566d1..063f926e0 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -58,6 +58,8 @@ const virtualLinkTransaction = ( userName: null, linkedUserGradidoID: null, linkedUserName: null, + userCommunityUuid: null, + linkedUserCommunityUuid: null, } return new Transaction(linkDbTransaction, user) } @@ -92,6 +94,8 @@ const virtualDecayTransaction = ( userName: null, linkedUserGradidoID: null, linkedUserName: null, + userCommunityUuid: null, + linkedUserCommunityUuid: null, } return new Transaction(decayDbTransaction, user) } diff --git a/database/entity/0072-add_communityuuid_to_transactions_table/Transaction.ts b/database/entity/0072-add_communityuuid_to_transactions_table/Transaction.ts new file mode 100644 index 000000000..8f13de58a --- /dev/null +++ b/database/entity/0072-add_communityuuid_to_transactions_table/Transaction.ts @@ -0,0 +1,163 @@ +/* eslint-disable no-use-before-define */ +import { Decimal } from 'decimal.js-light' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Contribution } from '../Contribution' +import { DltTransaction } from '../DltTransaction' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null }) + previous: number | null + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + @Column({ + name: 'transaction_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + transactionLinkId?: number | null + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + balance: Decimal + + @Column({ + name: 'balance_date', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + decay: Decimal + + @Column({ + name: 'decay_start', + type: 'datetime', + nullable: true, + default: null, + }) + decayStart: Date | null + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null }) + creationDate: Date | null + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ + name: 'user_community_uuid', + type: 'varchar', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + userCommunityUuid: string | null + + @Column({ + name: 'user_gradido_id', + type: 'varchar', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + userGradidoID: string + + @Column({ + name: 'user_name', + type: 'varchar', + length: 512, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + userName: string | null + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_user_community_uuid', + type: 'varchar', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserCommunityUuid: string | null + + @Column({ + name: 'linked_user_gradido_id', + type: 'varchar', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserGradidoID: string | null + + @Column({ + name: 'linked_user_name', + type: 'varchar', + length: 512, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserName: string | null + + @Column({ + name: 'linked_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedTransactionId?: number | null + + @OneToOne(() => Contribution, (contribution) => contribution.transaction) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + contribution?: Contribution | null + + @OneToOne(() => DltTransaction, (dlt) => dlt.transactionId) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + dltTransaction?: DltTransaction | null + + @OneToOne(() => Transaction) + @JoinColumn({ name: 'previous' }) + previousTransaction?: Transaction | null +} diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index d08c84667..d1d7075a9 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0070-add_dlt_transactions_table/Transaction' +export { Transaction } from './0072-add_communityuuid_to_transactions_table/Transaction' diff --git a/database/migrations/0072-add_communityuuid_to_transactions_table.ts b/database/migrations/0072-add_communityuuid_to_transactions_table.ts new file mode 100644 index 000000000..22e7c36eb --- /dev/null +++ b/database/migrations/0072-add_communityuuid_to_transactions_table.ts @@ -0,0 +1,38 @@ +/* MIGRATION TO add users that have a transaction but do not exist */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `user_community_uuid` char(36) DEFAULT NULL NULL AFTER `user_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `linked_user_community_uuid` char(36) DEFAULT NULL NULL AFTER `linked_user_id`;', + ) + /* the migration of the HomeCom-UUID for local users in the transactions table will be skipped + and be solved with the future users table migration for treating home- and foreign-users including + homeCom- and foreignCom-UUIDs + + // read the community uuid of the homeCommunity + const result = await queryFn(`SELECT c.community_uuid from communities as c WHERE c.foreign = 0`) + // and if uuid exists enter the home_community_uuid for sender and recipient of each still existing transaction + if (result && result[0]) { + await queryFn( + `UPDATE transactions as t SET t.user_community_uuid = "${result[0].community_uuid}" WHERE t.user_id IS NOT NULL AND t.user_community_uuid IS NULL`, + ) + await queryFn( + `UPDATE transactions as t SET t.linked_user_community_uuid = "${result[0].community_uuid}" WHERE t.linked_user_id IS NOT NULL AND t.linked_user_community_uuid IS NULL`, + ) + } + // leads to an error in case of empty communties table during CD/CI-pipeline-tests + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `user_community_uuid` char(36) NOT NULL AFTER `user_id`;', + ) + */ +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_community_uuid`;') + await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_community_uuid`;') +} diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 1d5d0672e..78755f280 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0071-add-pending_transactions-table', + DB_VERSION: '0072-add_communityuuid_to_transactions_table', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', diff --git a/federation/jest.config.js b/federation/jest.config.js index 25ff58fb3..f859bbe2f 100644 --- a/federation/jest.config.js +++ b/federation/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 76, + lines: 75, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/federation/package.json b/federation/package.json index b70196149..aaeaff08d 100644 --- a/federation/package.json +++ b/federation/package.json @@ -16,7 +16,9 @@ "lint": "eslint --max-warnings=0 --ext .js,.ts ." }, "dependencies": { + "@types/uuid": "8.3.4", "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", @@ -27,7 +29,8 @@ "lodash.clonedeep": "^4.5.0", "log4js": "^6.7.1", "reflect-metadata": "^0.1.13", - "type-graphql": "^1.1.1" + "type-graphql": "^1.1.1", + "uuid": "8.3.2" }, "devDependencies": { "@types/express": "4.17.12", diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 74a53ed1b..e88267589 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -1,6 +1,7 @@ // ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) import { Decimal } from 'decimal.js-light' import dotenv from 'dotenv' + dotenv.config() Decimal.set({ @@ -9,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0071-add-pending_transactions-table', + DB_VERSION: '0072-add_communityuuid_to_transactions_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info @@ -53,9 +54,14 @@ const federation = { FEDERATION_API: process.env.FEDERATION_API || '1_0', FEDERATION_PORT: process.env.FEDERATION_PORT || 5010, FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, + FEDERATION_TRADING_LEVEL: { + RECEIVER_COMMUNITY_URL: 'https://stage3.gradido.net/api/', + SEND_COINS: true, + AMOUNT: 100, + }, } -const CONFIG = { +export const CONFIG = { ...constants, ...server, ...database, 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/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/model/SendCoinsArgs.ts b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts index 545aab822..e658209ca 100644 --- a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts +++ b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts @@ -1,13 +1,13 @@ import { Decimal } from 'decimal.js-light' -import { ArgsType, Field } from 'type-graphql' +import { Field, InputType } from 'type-graphql' -@ArgsType() +@InputType() export class SendCoinsArgs { @Field(() => String) - communityReceiverIdentifier: string + recipientCommunityUuid: string @Field(() => String) - userReceiverIdentifier: string + recipientUserIdentifier: string @Field(() => String) creationDate: string @@ -19,11 +19,11 @@ export class SendCoinsArgs { memo: string @Field(() => String) - communitySenderIdentifier: string + senderCommunityUuid: string @Field(() => String) - userSenderIdentifier: string + senderUserUuid: string @Field(() => String) - userSenderName: string + senderUserName: string } diff --git a/federation/src/graphql/api/1_0/model/SendCoinsResult.ts b/federation/src/graphql/api/1_0/model/SendCoinsResult.ts new file mode 100644 index 000000000..930d22ff5 --- /dev/null +++ b/federation/src/graphql/api/1_0/model/SendCoinsResult.ts @@ -0,0 +1,17 @@ +import { Field, ObjectType } from 'type-graphql' + +@ObjectType() +export class SendCoinsResult { + constructor() { + this.vote = false + } + + @Field(() => Boolean) + vote: boolean + + @Field(() => String, { nullable: true }) + recipGradidoID: string | null + + @Field(() => String, { nullable: true }) + recipName: string | null +} diff --git a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts index d18a30a7c..08544834f 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts @@ -4,11 +4,12 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '@/server/createServer' import { Community as DbCommunity } from '@entity/Community' import CONFIG from '@/config' +import { Connection } from '@dbTools/typeorm' let query: any // to do: We need a setup for the tests that closes the connection -let con: any +let con: Connection CONFIG.FEDERATION_API = '1_0' diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts index 7b580b240..83e502a03 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts @@ -4,11 +4,14 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { Community as DbCommunity } from '@entity/Community' import CONFIG from '@/config' import { User as DbUser } from '@entity/User' +import { UserContact as DbUserContact } from '@entity/UserContact' import { fullName } from '@/graphql/util/fullName' import { GraphQLError } from 'graphql' import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' import { Connection } from '@dbTools/typeorm' +import Decimal from 'decimal.js-light' +import { SendCoinsArgs } from '../model/SendCoinsArgs' let mutate: ApolloServerTestClient['mutate'], con: Connection // let query: ApolloServerTestClient['query'] @@ -21,18 +24,18 @@ let testEnv: { CONFIG.FEDERATION_API = '1_0' +let homeCom: DbCommunity +let foreignCom: DbCommunity +let sendUser: DbUser +let sendContact: DbUserContact +let recipUser: DbUser +let recipContact: DbUserContact + beforeAll(async () => { testEnv = await testEnvironment(logger) mutate = testEnv.mutate // query = testEnv.query con = testEnv.con - - // const server = await createServer() - // con = server.con - // query = createTestClient(server.apollo).query - // mutate = createTestClient(server.apollo).mutate - // DbCommunity.clear() - // DbUser.clear() await cleanDB() }) @@ -43,115 +46,100 @@ afterAll(async () => { describe('SendCoinsResolver', () => { const voteForSendCoinsMutation = ` - mutation ( - $communityReceiverIdentifier: String! - $userReceiverIdentifier: String! - $creationDate: String! - $amount: Decimal! - $memo: String! - $communitySenderIdentifier: String! - $userSenderIdentifier: String! - $userSenderName: String! - ) { - voteForSendCoins( - communityReceiverIdentifier: $communityReceiverIdentifier - userReceiverIdentifier: $userReceiverIdentifier - creationDate: $creationDate - amount: $amount - memo: $memo - communitySenderIdentifier: $communitySenderIdentifier - userSenderIdentifier: $userSenderIdentifier - userSenderName: $userSenderName - ) - } -` + mutation ($args: SendCoinsArgs!) { + voteForSendCoins(data: $args) { + vote + recipGradidoID + recipName + } + }` + const settleSendCoinsMutation = ` + mutation ($args: SendCoinsArgs!) { + settleSendCoins(data: $args) + }` const revertSendCoinsMutation = ` - mutation ( - $communityReceiverIdentifier: String! - $userReceiverIdentifier: String! - $creationDate: String! - $amount: Decimal! - $memo: String! - $communitySenderIdentifier: String! - $userSenderIdentifier: String! - $userSenderName: String! - ) { - revertSendCoins( - communityReceiverIdentifier: $communityReceiverIdentifier - userReceiverIdentifier: $userReceiverIdentifier - creationDate: $creationDate - amount: $amount - memo: $memo - communitySenderIdentifier: $communitySenderIdentifier - userSenderIdentifier: $userSenderIdentifier - userSenderName: $userSenderName - ) - } -` + mutation ($args: SendCoinsArgs!) { + revertSendCoins(data: $args) + }` + const revertSettledSendCoinsMutation = ` + mutation ($args: SendCoinsArgs!) { + revertSettledSendCoins(data: $args) + }` + + beforeEach(async () => { + await cleanDB() + homeCom = DbCommunity.create() + homeCom.foreign = false + homeCom.url = 'homeCom-url' + homeCom.name = 'homeCom-Name' + homeCom.description = 'homeCom-Description' + homeCom.creationDate = new Date() + homeCom.publicKey = Buffer.from('homeCom-publicKey') + homeCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba' + await DbCommunity.insert(homeCom) + + foreignCom = DbCommunity.create() + foreignCom.foreign = true + foreignCom.url = 'foreignCom-url' + foreignCom.name = 'foreignCom-Name' + foreignCom.description = 'foreignCom-Description' + foreignCom.creationDate = new Date() + foreignCom.publicKey = Buffer.from('foreignCom-publicKey') + foreignCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb' + await DbCommunity.insert(foreignCom) + + sendUser = DbUser.create() + sendUser.alias = 'sendUser-alias' + sendUser.firstName = 'sendUser-FirstName' + sendUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebc' + sendUser.lastName = 'sendUser-LastName' + await DbUser.insert(sendUser) + + sendContact = await newEmailContact('send.user@email.de', sendUser.id) + sendContact = await DbUserContact.save(sendContact) + + sendUser.emailContact = sendContact + sendUser.emailId = sendContact.id + await DbUser.save(sendUser) + + recipUser = DbUser.create() + recipUser.alias = 'recipUser-alias' + recipUser.firstName = 'recipUser-FirstName' + recipUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebd' + recipUser.lastName = 'recipUser-LastName' + await DbUser.insert(recipUser) + + recipContact = await newEmailContact('recip.user@email.de', recipUser.id) + recipContact = await DbUserContact.save(recipContact) + + recipUser.emailContact = recipContact + recipUser.emailId = recipContact.id + await DbUser.save(recipUser) + }) describe('voteForSendCoins', () => { - let homeCom: DbCommunity - let foreignCom: DbCommunity - let sendUser: DbUser - let recipUser: DbUser - - beforeEach(async () => { - await cleanDB() - homeCom = DbCommunity.create() - homeCom.foreign = false - homeCom.url = 'homeCom-url' - homeCom.name = 'homeCom-Name' - homeCom.description = 'homeCom-Description' - homeCom.creationDate = new Date() - homeCom.publicKey = Buffer.from('homeCom-publicKey') - homeCom.communityUuid = 'homeCom-UUID' - await DbCommunity.insert(homeCom) - - foreignCom = DbCommunity.create() - foreignCom.foreign = true - foreignCom.url = 'foreignCom-url' - foreignCom.name = 'foreignCom-Name' - foreignCom.description = 'foreignCom-Description' - foreignCom.creationDate = new Date() - foreignCom.publicKey = Buffer.from('foreignCom-publicKey') - foreignCom.communityUuid = 'foreignCom-UUID' - await DbCommunity.insert(foreignCom) - - sendUser = DbUser.create() - sendUser.alias = 'sendUser-alias' - sendUser.firstName = 'sendUser-FirstName' - sendUser.gradidoID = 'sendUser-GradidoID' - sendUser.lastName = 'sendUser-LastName' - await DbUser.insert(sendUser) - - recipUser = DbUser.create() - recipUser.alias = 'recipUser-alias' - recipUser.firstName = 'recipUser-FirstName' - recipUser.gradidoID = 'recipUser-GradidoID' - recipUser.lastName = 'recipUser-LastName' - await DbUser.insert(recipUser) - }) - describe('unknown recipient community', () => { it('throws an error', async () => { jest.clearAllMocks() + const args = new SendCoinsArgs() + args.recipientCommunityUuid = 'invalid foreignCom' + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = new Date().toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) expect( await mutate({ mutation: voteForSendCoinsMutation, - variables: { - communityReceiverIdentifier: 'invalid foreignCom', - userReceiverIdentifier: recipUser.gradidoID, - creationDate: new Date().toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('voteForSendCoins with wrong communityReceiverIdentifier')], + errors: [new GraphQLError('voteForSendCoins with wrong recipientCommunityUuid')], }), ) }) @@ -160,25 +148,29 @@ describe('SendCoinsResolver', () => { describe('unknown recipient user', () => { it('throws an error', async () => { jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = 'invalid recipient' + args.creationDate = new Date().toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) expect( await mutate({ mutation: voteForSendCoinsMutation, - variables: { - communityReceiverIdentifier: foreignCom.communityUuid, - userReceiverIdentifier: 'invalid recipient', - creationDate: new Date().toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }), ).toEqual( expect.objectContaining({ errors: [ new GraphQLError( - 'voteForSendCoins with unknown userReceiverIdentifier in the community=', + 'voteForSendCoins with unknown recipientUserIdentifier in the community=', ), ], }), @@ -189,24 +181,32 @@ describe('SendCoinsResolver', () => { describe('valid X-Com-TX voted', () => { it('throws an error', async () => { jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = new Date().toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) expect( await mutate({ mutation: voteForSendCoinsMutation, - variables: { - communityReceiverIdentifier: foreignCom.communityUuid, - userReceiverIdentifier: recipUser.gradidoID, - creationDate: new Date().toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }), ).toEqual( expect.objectContaining({ data: { - voteForSendCoins: 'recipUser-FirstName recipUser-LastName', + voteForSendCoins: { + recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd', + recipName: 'recipUser-FirstName recipUser-LastName', + vote: true, + }, }, }), ) @@ -215,83 +215,50 @@ describe('SendCoinsResolver', () => { }) describe('revertSendCoins', () => { - let homeCom: DbCommunity - let foreignCom: DbCommunity - let sendUser: DbUser - let recipUser: DbUser const creationDate = new Date() beforeEach(async () => { - await cleanDB() - homeCom = DbCommunity.create() - homeCom.foreign = false - homeCom.url = 'homeCom-url' - homeCom.name = 'homeCom-Name' - homeCom.description = 'homeCom-Description' - homeCom.creationDate = new Date() - homeCom.publicKey = Buffer.from('homeCom-publicKey') - homeCom.communityUuid = 'homeCom-UUID' - await DbCommunity.insert(homeCom) - - foreignCom = DbCommunity.create() - foreignCom.foreign = true - foreignCom.url = 'foreignCom-url' - foreignCom.name = 'foreignCom-Name' - foreignCom.description = 'foreignCom-Description' - foreignCom.creationDate = new Date() - foreignCom.publicKey = Buffer.from('foreignCom-publicKey') - foreignCom.communityUuid = 'foreignCom-UUID' - await DbCommunity.insert(foreignCom) - - sendUser = DbUser.create() - sendUser.alias = 'sendUser-alias' - sendUser.firstName = 'sendUser-FirstName' - sendUser.gradidoID = 'sendUser-GradidoID' - sendUser.lastName = 'sendUser-LastName' - await DbUser.insert(sendUser) - - recipUser = DbUser.create() - recipUser.alias = 'recipUser-alias' - recipUser.firstName = 'recipUser-FirstName' - recipUser.gradidoID = 'recipUser-GradidoID' - recipUser.lastName = 'recipUser-LastName' - await DbUser.insert(recipUser) - + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) await mutate({ mutation: voteForSendCoinsMutation, - variables: { - communityReceiverIdentifier: foreignCom.communityUuid, - userReceiverIdentifier: recipUser.gradidoID, - creationDate: creationDate.toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }) }) describe('unknown recipient community', () => { it('throws an error', async () => { jest.clearAllMocks() + const args = new SendCoinsArgs() + args.recipientCommunityUuid = 'invalid foreignCom' + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) expect( await mutate({ mutation: revertSendCoinsMutation, - variables: { - communityReceiverIdentifier: 'invalid foreignCom', - userReceiverIdentifier: recipUser.gradidoID, - creationDate: creationDate.toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('revertSendCoins with wrong communityReceiverIdentifier')], + errors: [new GraphQLError('revertSendCoins with wrong recipientCommunityUuid')], }), ) }) @@ -300,25 +267,29 @@ describe('SendCoinsResolver', () => { describe('unknown recipient user', () => { it('throws an error', async () => { jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = 'invalid recipient' + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) expect( await mutate({ mutation: revertSendCoinsMutation, - variables: { - communityReceiverIdentifier: foreignCom.communityUuid, - userReceiverIdentifier: 'invalid recipient', - creationDate: creationDate.toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }), ).toEqual( expect.objectContaining({ errors: [ new GraphQLError( - 'revertSendCoins with unknown userReceiverIdentifier in the community=', + 'revertSendCoins with unknown recipientUserIdentifier in the community=', ), ], }), @@ -329,19 +300,23 @@ describe('SendCoinsResolver', () => { describe('valid X-Com-TX reverted', () => { it('throws an error', async () => { jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) expect( await mutate({ mutation: revertSendCoinsMutation, - variables: { - communityReceiverIdentifier: foreignCom.communityUuid, - userReceiverIdentifier: recipUser.gradidoID, - creationDate: creationDate.toISOString(), - amount: 100, - memo: 'X-Com-TX memo', - communitySenderIdentifier: homeCom.communityUuid, - userSenderIdentifier: sendUser.gradidoID, - userSenderName: fullName(sendUser.firstName, sendUser.lastName), - }, + variables: { args }, }), ).toEqual( expect.objectContaining({ @@ -353,4 +328,249 @@ describe('SendCoinsResolver', () => { }) }) }) + + describe('settleSendCoins', () => { + const creationDate = new Date() + + beforeEach(async () => { + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { args }, + }) + }) + + describe('unknown recipient community', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + args.recipientCommunityUuid = 'invalid foreignCom' + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + expect( + await mutate({ + mutation: settleSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('settleSendCoins with wrong recipientCommunityUuid')], + }), + ) + }) + }) + + describe('unknown recipient user', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = 'invalid recipient' + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + expect( + await mutate({ + mutation: settleSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'settleSendCoins with unknown recipientUserIdentifier in the community=', + ), + ], + }), + ) + }) + }) + + describe('valid X-Com-TX settled', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + expect( + await mutate({ + mutation: settleSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + data: { + settleSendCoins: true, + }, + }), + ) + }) + }) + }) + + describe('revertSettledSendCoins', () => { + const creationDate = new Date() + + beforeEach(async () => { + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { args }, + }) + await mutate({ + mutation: settleSendCoinsMutation, + variables: { args }, + }) + }) + + describe('unknown recipient community', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + args.recipientCommunityUuid = 'invalid foreignCom' + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + expect( + await mutate({ + mutation: revertSettledSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('revertSettledSendCoins with wrong recipientCommunityUuid')], + }), + ) + }) + }) + + describe('unknown recipient user', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = 'invalid recipient' + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + expect( + await mutate({ + mutation: revertSettledSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'revertSettledSendCoins with unknown recipientUserIdentifier in the community=', + ), + ], + }), + ) + }) + }) + + describe('valid X-Com-TX settled', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.gradidoID + args.creationDate = creationDate.toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + expect( + await mutate({ + mutation: revertSettledSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + data: { + revertSettledSendCoins: true, + }, + }), + ) + }) + }) + }) }) + +async function newEmailContact(email: string, userId: number): Promise { + const emailContact = new DbUserContact() + emailContact.email = email + emailContact.userId = userId + emailContact.type = 'EMAIL' + emailContact.emailChecked = false + emailContact.emailOptInTypeId = 1 + emailContact.emailVerificationCode = '1' + userId + return emailContact +} diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 11222a3ce..d942c48f3 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -1,78 +1,106 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Args, Mutation, Resolver } from 'type-graphql' +import { Arg, Args, Mutation, Resolver } from 'type-graphql' import { federationLogger as logger } from '@/server/logger' import { Community as DbCommunity } from '@entity/Community' import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' import { SendCoinsArgs } from '../model/SendCoinsArgs' -import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' import { PendingTransactionState } from '../enum/PendingTransactionState' import { TransactionTypeId } from '../enum/TransactionTypeId' -import { calculateRecipientBalance } from '@/graphql/util/calculateRecipientBalance' -import Decimal from 'decimal.js-light' +import { calculateRecipientBalance } from '../util/calculateRecipientBalance' import { fullName } from '@/graphql/util/fullName' +import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction' +// import { checkTradingLevel } from '@/graphql/util/checkTradingLevel' +import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction' +import { findUserByIdentifier } from '@/graphql/util/findUserByIdentifier' +import { SendCoinsResult } from '../model/SendCoinsResult' +import Decimal from 'decimal.js-light' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars export class SendCoinsResolver { - @Mutation(() => String) + @Mutation(() => SendCoinsResult) async voteForSendCoins( - @Args() - { - communityReceiverIdentifier, - userReceiverIdentifier, - creationDate, - amount, - memo, - communitySenderIdentifier, - userSenderIdentifier, - userSenderName, - }: SendCoinsArgs, - ): Promise { - logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) - let result: string | null = null + @Arg('data') + args: SendCoinsArgs, + ): Promise { + logger.debug( + `voteForSendCoins() via apiVersion=1_0 ...`, + args.recipientCommunityUuid, + args.recipientUserIdentifier, + args.creationDate, + args.amount.toString(), + args.memo, + args.senderCommunityUuid, + args.senderUserUuid, + args.senderUserName, + ) + const result = new SendCoinsResult() // first check if receiver community is correct const homeCom = await DbCommunity.findOneBy({ - communityUuid: communityReceiverIdentifier, + communityUuid: args.recipientCommunityUuid, }) if (!homeCom) { throw new LogError( - `voteForSendCoins with wrong communityReceiverIdentifier`, - communityReceiverIdentifier, + `voteForSendCoins with wrong recipientCommunityUuid`, + args.recipientCommunityUuid, ) } - // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) - if (!receiverUser) { + let receiverUser + try { + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + } catch (err) { + logger.error('Error in findUserByIdentifier:', err) throw new LogError( - `voteForSendCoins with unknown userReceiverIdentifier in the community=`, + `voteForSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, ) } + const openSenderPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW }, + ], + }) + const openReceiverPendingTx = await DbPendingTransaction.count({ + where: [ + { userGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW }, + { linkedUserGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW }, + ], + }) + if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) { + throw new LogError( + `There exist still ongoing 'Pending-Transactions' for the involved users on receiver-side!`, + ) + } + try { - const txDate = new Date(creationDate) - const receiveBalance = await calculateRecipientBalance(receiverUser.id, amount, txDate) + const txDate = new Date(args.creationDate) + const receiveBalance = await calculateRecipientBalance(receiverUser.id, args.amount, txDate) const pendingTx = DbPendingTransaction.create() - pendingTx.amount = amount - pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0) + pendingTx.amount = args.amount + pendingTx.balance = receiveBalance ? receiveBalance.balance : args.amount pendingTx.balanceDate = txDate pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null pendingTx.creationDate = new Date() - pendingTx.linkedUserCommunityUuid = communitySenderIdentifier - pendingTx.linkedUserGradidoID = userSenderIdentifier - pendingTx.linkedUserName = userSenderName - pendingTx.memo = memo + pendingTx.linkedUserCommunityUuid = args.senderCommunityUuid + pendingTx.linkedUserGradidoID = args.senderUserUuid + pendingTx.linkedUserName = args.senderUserName + pendingTx.memo = args.memo pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null pendingTx.state = PendingTransactionState.NEW pendingTx.typeId = TransactionTypeId.RECEIVE pendingTx.userId = receiverUser.id - pendingTx.userCommunityUuid = communityReceiverIdentifier - pendingTx.userGradidoID = userReceiverIdentifier + pendingTx.userCommunityUuid = args.recipientCommunityUuid + pendingTx.userGradidoID = receiverUser.gradidoID pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) await DbPendingTransaction.insert(pendingTx) - result = pendingTx.userName + result.vote = true + result.recipName = pendingTx.userName + result.recipGradidoID = pendingTx.userGradidoID logger.debug(`voteForSendCoins()-1_0... successfull`) } catch (err) { throw new LogError(`Error in voteForSendCoins: `, err) @@ -82,49 +110,43 @@ export class SendCoinsResolver { @Mutation(() => Boolean) async revertSendCoins( - @Args() - { - communityReceiverIdentifier, - userReceiverIdentifier, - creationDate, - amount, - memo, - communitySenderIdentifier, - userSenderIdentifier, - userSenderName, - }: SendCoinsArgs, + @Arg('data') + args: SendCoinsArgs, ): Promise { logger.debug(`revertSendCoins() via apiVersion=1_0 ...`) // first check if receiver community is correct const homeCom = await DbCommunity.findOneBy({ - communityUuid: communityReceiverIdentifier, + communityUuid: args.recipientCommunityUuid, }) if (!homeCom) { throw new LogError( - `revertSendCoins with wrong communityReceiverIdentifier`, - communityReceiverIdentifier, + `revertSendCoins with wrong recipientCommunityUuid`, + args.recipientCommunityUuid, ) } - // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) - if (!receiverUser) { + let receiverUser + try { + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + } catch (err) { + logger.error('Error in findUserByIdentifier:', err) throw new LogError( - `revertSendCoins with unknown userReceiverIdentifier in the community=`, + `revertSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, ) } try { const pendingTx = await DbPendingTransaction.findOneBy({ - userCommunityUuid: communityReceiverIdentifier, - userGradidoID: userReceiverIdentifier, + userCommunityUuid: args.recipientCommunityUuid, + userGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW, typeId: TransactionTypeId.RECEIVE, - balanceDate: new Date(creationDate), - linkedUserCommunityUuid: communitySenderIdentifier, - linkedUserGradidoID: userSenderIdentifier, + balanceDate: new Date(args.creationDate), + linkedUserCommunityUuid: args.senderCommunityUuid, + linkedUserGradidoID: args.senderUserUuid, }) logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx) - if (pendingTx && pendingTx.amount.toString() === amount.toString()) { + if (pendingTx && pendingTx.amount.toString() === args.amount.toString()) { logger.debug('XCom: revertSendCoins matching pendingTX for remove...') try { await pendingTx.remove() @@ -135,21 +157,21 @@ export class SendCoinsResolver { } else { logger.debug( 'XCom: revertSendCoins NOT matching pendingTX for remove:', - pendingTx?.amount, - amount, + pendingTx?.amount.toString(), + args.amount.toString(), ) throw new LogError( `Can't find in revertSendCoins the pending receiver TX for args=`, - communityReceiverIdentifier, - userReceiverIdentifier, + args.recipientCommunityUuid, + args.recipientUserIdentifier, PendingTransactionState.NEW, TransactionTypeId.RECEIVE, - creationDate, - amount, - memo, - communitySenderIdentifier, - userSenderIdentifier, - userSenderName, + args.creationDate, + args.amount, + args.memo, + args.senderCommunityUuid, + args.senderUserUuid, + args.senderUserName, ) } logger.debug(`revertSendCoins()-1_0... successfull`) @@ -158,4 +180,146 @@ export class SendCoinsResolver { throw new LogError(`Error in revertSendCoins: `, err) } } + + @Mutation(() => Boolean) + async settleSendCoins( + @Arg('data') + args: SendCoinsArgs, + ): Promise { + logger.debug( + `settleSendCoins() via apiVersion=1_0 ...userCommunityUuid=${ + args.recipientCommunityUuid + }, userGradidoID=${args.recipientUserIdentifier}, balanceDate=${ + args.creationDate + },amount=${args.amount.valueOf()}, memo=${args.memo}, linkedUserCommunityUuid = ${ + args.senderCommunityUuid + }, userSenderIdentifier=${args.senderUserUuid}, userSenderName=${args.senderUserName}`, + ) + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: args.recipientCommunityUuid, + }) + if (!homeCom) { + throw new LogError( + `settleSendCoins with wrong recipientCommunityUuid`, + args.recipientCommunityUuid, + ) + } + let receiverUser + try { + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + } catch (err) { + logger.error('Error in findUserByIdentifier:', err) + throw new LogError( + `settleSendCoins with unknown recipientUserIdentifier in the community=`, + homeCom.name, + ) + } + const pendingTx = await DbPendingTransaction.findOneBy({ + userCommunityUuid: args.recipientCommunityUuid, + userGradidoID: receiverUser.gradidoID, + state: PendingTransactionState.NEW, + typeId: TransactionTypeId.RECEIVE, + balanceDate: new Date(args.creationDate), + linkedUserCommunityUuid: args.senderCommunityUuid, + linkedUserGradidoID: args.senderUserUuid, + }) + logger.debug('XCom: settleSendCoins found pendingTX=', pendingTx?.toString()) + if ( + pendingTx && + pendingTx.amount.toString() === args.amount.toString() && + pendingTx.memo === args.memo + ) { + logger.debug('XCom: settleSendCoins matching pendingTX for settlement...') + + await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx) + logger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successfull`) + return true + } else { + logger.debug('XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...') + throw new LogError( + `Can't find in settlePendingReceiveTransaction the pending receiver TX for args=`, + args.recipientCommunityUuid, + args.recipientUserIdentifier, + PendingTransactionState.NEW, + TransactionTypeId.RECEIVE, + args.creationDate, + args.amount, + args.memo, + args.senderCommunityUuid, + args.senderUserUuid, + args.senderUserName, + ) + } + } + + @Mutation(() => Boolean) + async revertSettledSendCoins( + @Arg('data') + args: SendCoinsArgs, + ): Promise { + logger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`) + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: args.recipientCommunityUuid, + }) + if (!homeCom) { + throw new LogError( + `revertSettledSendCoins with wrong recipientCommunityUuid`, + args.recipientCommunityUuid, + ) + } + let receiverUser + try { + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + } catch (err) { + logger.error('Error in findUserByIdentifier:', err) + throw new LogError( + `revertSettledSendCoins with unknown recipientUserIdentifier in the community=`, + homeCom.name, + ) + } + const pendingTx = await DbPendingTransaction.findOneBy({ + userCommunityUuid: args.recipientCommunityUuid, + userGradidoID: args.recipientUserIdentifier, + state: PendingTransactionState.SETTLED, + typeId: TransactionTypeId.RECEIVE, + balanceDate: new Date(args.creationDate), + linkedUserCommunityUuid: args.senderCommunityUuid, + linkedUserGradidoID: args.senderUserUuid, + }) + logger.debug('XCom: revertSettledSendCoins found pendingTX=', pendingTx) + if ( + pendingTx && + pendingTx.amount.toString() === args.amount.toString() && + pendingTx.memo === args.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=`, + args.recipientCommunityUuid, + args.recipientUserIdentifier, + PendingTransactionState.SETTLED, + TransactionTypeId.RECEIVE, + args.creationDate, + args.amount, + args.memo, + args.senderCommunityUuid, + args.senderUserUuid, + args.senderUserName, + ) + } + logger.debug(`revertSendCoins()-1_0... successfull`) + return true + } } diff --git a/federation/src/graphql/util/calculateRecipientBalance.ts b/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts similarity index 76% rename from federation/src/graphql/util/calculateRecipientBalance.ts rename to federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts index 2a9c2aa1c..1b296f845 100644 --- a/federation/src/graphql/util/calculateRecipientBalance.ts +++ b/federation/src/graphql/api/1_0/util/calculateRecipientBalance.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 calculateRecipientBalance( userId: number, 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..4b0f989ba --- /dev/null +++ b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts @@ -0,0 +1,158 @@ +/* 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) + logger.debug(`LastTransaction vs PendingTransaction`) + logger.debug(`balance:`, lastTransaction?.balance.toString(), pendingTx.balance.toString()) + logger.debug( + `balanceDate:`, + lastTransaction?.balanceDate.toISOString(), + pendingTx.balanceDate.toISOString(), + ) + logger.debug(`GradidoID:`, lastTransaction?.userGradidoID, pendingTx.userGradidoID) + logger.debug(`Name:`, lastTransaction?.userName, pendingTx.userName) + logger.debug(`amount:`, lastTransaction?.amount.toString(), pendingTx.amount.toString()) + logger.debug(`memo:`, lastTransaction?.memo, pendingTx.memo) + logger.debug( + `linkedUserGradidoID:`, + lastTransaction?.linkedUserGradidoID, + pendingTx.linkedUserGradidoID, + ) + logger.debug(`linkedUserName:`, lastTransaction?.linkedUserName, pendingTx.linkedUserName) + // now the last Tx must be the equivalant to the pendingTX + if ( + lastTransaction && + lastTransaction.balance.comparedTo(pendingTx.balance) === 0 && + lastTransaction.balanceDate.toISOString() === pendingTx.balanceDate.toISOString() && + lastTransaction.userGradidoID === pendingTx.userGradidoID && + lastTransaction.userName === pendingTx.userName && + lastTransaction.amount.comparedTo(pendingTx.amount) === 0 && + 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!`, + lastTransaction, + 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 new file mode 100644 index 000000000..e0e600be9 --- /dev/null +++ b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts @@ -0,0 +1,145 @@ +/* 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' +import { calculateRecipientBalance } from './calculateRecipientBalance' +import Decimal from 'decimal.js-light' + +export async function settlePendingReceiveTransaction( + 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: settlePendingReceiveTransaction:', 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 }, + { 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(receiverUser.id) + + if (lastTransaction !== null && 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 transactionReceive = new dbTransaction() + transactionReceive.typeId = pendingTx.typeId + transactionReceive.memo = pendingTx.memo + transactionReceive.userId = pendingTx.userId + transactionReceive.userCommunityUuid = pendingTx.userCommunityUuid + transactionReceive.userGradidoID = pendingTx.userGradidoID + transactionReceive.userName = pendingTx.userName + transactionReceive.linkedUserId = pendingTx.linkedUserId + transactionReceive.linkedUserCommunityUuid = pendingTx.linkedUserCommunityUuid + transactionReceive.linkedUserGradidoID = pendingTx.linkedUserGradidoID + transactionReceive.linkedUserName = pendingTx.linkedUserName + transactionReceive.amount = pendingTx.amount + const receiveBalance = await calculateRecipientBalance( + receiverUser.id, + pendingTx.amount, + pendingTx.balanceDate, + ) + transactionReceive.balance = receiveBalance ? receiveBalance.balance : pendingTx.amount + transactionReceive.balanceDate = pendingTx.balanceDate + transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) + transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null + transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null + transactionReceive.linkedTransactionId = pendingTx.linkedTransactionId + await queryRunner.manager.insert(dbTransaction, transactionReceive) + logger.debug(`receive 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 recipient 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: 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/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) diff --git a/federation/src/graphql/util/checkTradingLevel.ts b/federation/src/graphql/util/checkTradingLevel.ts new file mode 100644 index 000000000..9f5463a0d --- /dev/null +++ b/federation/src/graphql/util/checkTradingLevel.ts @@ -0,0 +1,25 @@ +import CONFIG from '@/config' +import { Community as DbCommunity } from '@entity/Community' +import { Decimal } from 'decimal.js-light' +import { federationLogger as logger } from '@/server/logger' + +export async function checkTradingLevel(homeCom: DbCommunity, amount: Decimal): Promise { + const tradingLevel = CONFIG.FEDERATION_TRADING_LEVEL + if (homeCom.url !== tradingLevel.RECEIVER_COMMUNITY_URL) { + logger.warn( + `X-Com: tradingLevel allows to receive coins only with url ${tradingLevel.RECEIVER_COMMUNITY_URL}`, + ) + return false + } + if (!tradingLevel.SEND_COINS) { + logger.warn(`X-Com: tradingLevel disable general x-com sendcoin actions!`) + return false + } + if (new Decimal(tradingLevel.AMOUNT) < amount) { + logger.warn( + `X-Com: tradingLevel only allows to receive coins lower than amount of ${tradingLevel.AMOUNT}`, + ) + return false + } + return true +} diff --git a/federation/src/graphql/util/findUserByIdentifier.ts b/federation/src/graphql/util/findUserByIdentifier.ts new file mode 100644 index 000000000..96c9eb458 --- /dev/null +++ b/federation/src/graphql/util/findUserByIdentifier.ts @@ -0,0 +1,42 @@ +import { User as DbUser } from '@entity/User' +import { UserContact as DbUserContact } from '@entity/UserContact' +import { validate, version } from 'uuid' + +import { LogError } from '@/server/LogError' + +import { VALID_ALIAS_REGEX } from './validateAlias' + +export const findUserByIdentifier = async (identifier: string): Promise => { + let user: DbUser | null + if (validate(identifier) && version(identifier) === 4) { + user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] }) + if (!user) { + throw new LogError('No user found to given identifier', identifier) + } + } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { + const userContact = await DbUserContact.findOne({ + where: { + email: identifier, + emailChecked: true, + }, + relations: ['user'], + }) + if (!userContact) { + throw new LogError('No user with this credentials', identifier) + } + if (!userContact.user) { + throw new LogError('No user to given contact', identifier) + } + user = userContact.user + user.emailContact = userContact + } else if (VALID_ALIAS_REGEX.exec(identifier)) { + user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] }) + if (!user) { + throw new LogError('No user found to given identifier', identifier) + } + } else { + throw new LogError('Unknown identifier type', identifier) + } + + return user +} diff --git a/federation/src/graphql/util/validateAlias.ts b/federation/src/graphql/util/validateAlias.ts new file mode 100644 index 000000000..cd2d8fe1f --- /dev/null +++ b/federation/src/graphql/util/validateAlias.ts @@ -0,0 +1,39 @@ +import { Raw } from '@dbTools/typeorm' +import { User as DbUser } from '@entity/User' + +import { LogError } from '@/server/LogError' + +export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ + +const RESERVED_ALIAS = [ + 'admin', + 'email', + 'gast', + 'gdd', + 'gradido', + 'guest', + 'home', + 'root', + 'support', + 'temp', + 'tmp', + 'tmp', + 'user', + 'usr', + 'var', +] + +export const validateAlias = async (alias: string): Promise => { + if (alias.length < 3) throw new LogError('Given alias is too short', alias) + if (alias.length > 20) throw new LogError('Given alias is too long', alias) + if (!alias.match(VALID_ALIAS_REGEX)) throw new LogError('Invalid characters in alias', alias) + if (RESERVED_ALIAS.includes(alias.toLowerCase())) + throw new LogError('Alias is not allowed', alias) + const aliasInUse = await DbUser.find({ + where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, + }) + if (aliasInUse.length !== 0) { + throw new LogError('Alias already in use', alias) + } + return true +} diff --git a/federation/src/index.ts b/federation/src/index.ts index 997edb7aa..bdc66c87a 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import createServer from './server/createServer' +import { createServer } from './server/createServer' // config -import CONFIG from './config' +import { CONFIG } from './config' async function main() { // eslint-disable-next-line no-console diff --git a/federation/src/server/createServer.ts b/federation/src/server/createServer.ts index a47b6da7f..b79847254 100644 --- a/federation/src/server/createServer.ts +++ b/federation/src/server/createServer.ts @@ -13,7 +13,7 @@ import cors from './cors' import plugins from './plugins' // config -import CONFIG from '@/config' +import { CONFIG } from '@/config' // graphql import schema from '@/graphql/schema' @@ -33,7 +33,7 @@ import { Logger } from 'log4js' type ServerDef = { apollo: ApolloServer; app: Express; con: Connection } -const createServer = async ( +export const createServer = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any // context: any = serverContext, logger: Logger = apolloLogger, diff --git a/federation/test/extensions.ts b/federation/test/extensions.ts new file mode 100644 index 000000000..262a9bcdb --- /dev/null +++ b/federation/test/extensions.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { Decimal } from 'decimal.js-light' + +expect.extend({ + decimalEqual(received, value) { + const pass = new Decimal(value).equals(received.toString()) + if (pass) { + return { + message: () => `expected ${received} to not equal ${value}`, + pass: true, + } + } else { + return { + message: () => `expected ${received} to equal ${value}`, + pass: false, + } + } + }, +}) + +interface CustomMatchers { + decimalEqual(value: number): R +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Expect extends CustomMatchers {} + interface Matchers extends CustomMatchers {} + interface InverseAsymmetricMatchers extends CustomMatchers {} + } +} diff --git a/federation/test/helpers.ts b/federation/test/helpers.ts index 542906f49..3b05edf4d 100644 --- a/federation/test/helpers.ts +++ b/federation/test/helpers.ts @@ -4,11 +4,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ - import { entities } from '@entity/index' import { createTestClient } from 'apollo-server-testing' -import createServer from '@/server/createServer' +import { createServer } from '@/server/createServer' import { logger } from './testSetup' diff --git a/federation/tsconfig.json b/federation/tsconfig.json index 2326786ac..50ce9d0c7 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..87bd7f0d4 100644 --- a/federation/yarn.lock +++ b/federation/yarn.lock @@ -1057,6 +1057,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/ws@^7.0.0": version "7.4.7" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" @@ -1529,6 +1534,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" @@ -5437,16 +5447,16 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@8.3.2, uuid@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + uuid@^3.1.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.0.0: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"