From ec9c78dbeff5a24ce936e7e71e20fd0898dbd3c4 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Wed, 23 Aug 2023 00:58:31 +0200 Subject: [PATCH 01/15] first draft of graphql client SendCoins --- .../federation/client/1_0/SendCoinsClient.ts | 69 +++++++++++++++++++ .../client/1_0/model/SendCoinsArgs.ts | 26 +++++++ .../client/1_0/query/revertSendCoins.ts | 25 +++++++ .../client/1_0/query/voteForSendCoins.ts | 25 +++++++ .../federation/client/1_1/SendCoinsClient.ts | 5 ++ .../client/SendCoinsClientFactory.ts | 62 +++++++++++++++++ 6 files changed, 212 insertions(+) create mode 100644 backend/src/federation/client/1_0/SendCoinsClient.ts create mode 100644 backend/src/federation/client/1_0/model/SendCoinsArgs.ts create mode 100644 backend/src/federation/client/1_0/query/revertSendCoins.ts create mode 100644 backend/src/federation/client/1_0/query/voteForSendCoins.ts create mode 100644 backend/src/federation/client/1_1/SendCoinsClient.ts create mode 100644 backend/src/federation/client/SendCoinsClientFactory.ts diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts new file mode 100644 index 000000000..0362d9c27 --- /dev/null +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -0,0 +1,69 @@ +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { GraphQLClient } from 'graphql-request' + +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +import { SendCoinsArgs } from './model/SendCoinsArgs' +import { revertSendCoins } from './query/revertSendCoins' +import { voteForSendCoins } from './query/voteForSendCoins' + +// eslint-disable-next-line camelcase +export class SendCoinsClient { + dbCom: DbFederatedCommunity + endpoint: string + client: GraphQLClient + + constructor(dbCom: DbFederatedCommunity) { + this.dbCom = dbCom + this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${ + dbCom.apiVersion + }/` + this.client = new GraphQLClient(this.endpoint, { + method: 'GET', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } + + voteForSendCoins = async (args: SendCoinsArgs): Promise => { + logger.debug('X-Com: voteForSendCoins against endpoint', this.endpoint) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(voteForSendCoins, { args }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.voteForSendCoins?.vote) { + logger.warn('X-Com: voteForSendCoins without response data from endpoint', this.endpoint) + return false + } + logger.debug( + 'X-Com: voteForSendCoins successful from endpoint', + this.endpoint, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + data.voteForSendCoins.vote, + ) + return true + } catch (err) { + throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err) + } + } + + revertSendCoins = async (args: SendCoinsArgs): Promise => { + logger.debug(`X-Com: revertSendCoins against endpoint='${this.endpoint}'...`) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(revertSendCoins, { args }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.revertSendCoins?.acknowledged) { + logger.warn('X-Com: revertSendCoins without response data from endpoint', this.endpoint) + return false + } + logger.debug(`X-Com: revertSendCoins successful from endpoint=${this.endpoint}`) + return true + } catch (err) { + throw new LogError(`X-Com: revertSendCoins 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 new file mode 100644 index 000000000..2ba743368 --- /dev/null +++ b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts @@ -0,0 +1,26 @@ +import { Decimal } from 'decimal.js-light' +import { ArgsType, Field, Int } from 'type-graphql' + +@ArgsType() +export class SendCoinsArgs { + @Field(() => String) + communityReceiverIdentifier: string + + @Field(() => String) + userReceiverIdentifier: string + + @Field(() => Decimal) + amount: Decimal + + @Field(() => String) + memo: string + + @Field(() => String) + communitySenderIdentifier: string + + @Field(() => String) + userSenderIdentifier: string + + @Field(() => String) + userSenderName: string +} diff --git a/backend/src/federation/client/1_0/query/revertSendCoins.ts b/backend/src/federation/client/1_0/query/revertSendCoins.ts new file mode 100644 index 000000000..fd74feef1 --- /dev/null +++ b/backend/src/federation/client/1_0/query/revertSendCoins.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-request' + +export const revertSendCoins = gql` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $amount: Decimal! + $memo: String! + $communitySenderIdentifier: String! + $userSenderIdentifier: String! + $userSenderName: String! + ) { + revertSendCoins( + communityReceiverIdentifier: $communityReceiverIdentifier + userReceiverIdentifier: $userReceiverIdentifier + amount: $amount + memo: $memo + communitySenderIdentifier: $communitySenderIdentifier + userSenderIdentifier: $userSenderIdentifier + userSenderName: $userSenderName + ) { + acknowledged + } + } +` diff --git a/backend/src/federation/client/1_0/query/voteForSendCoins.ts b/backend/src/federation/client/1_0/query/voteForSendCoins.ts new file mode 100644 index 000000000..4f4dd892a --- /dev/null +++ b/backend/src/federation/client/1_0/query/voteForSendCoins.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-request' + +export const voteForSendCoins = gql` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $amount: Decimal! + $memo: String! + $communitySenderIdentifier: String! + $userSenderIdentifier: String! + $userSenderName: String! + ) { + voteForSendCoins( + communityReceiverIdentifier: $communityReceiverIdentifier + userReceiverIdentifier: $userReceiverIdentifier + amount: $amount + memo: $memo + communitySenderIdentifier: $communitySenderIdentifier + userSenderIdentifier: $userSenderIdentifier + userSenderName: $userSenderName + ) { + vote + } + } +` diff --git a/backend/src/federation/client/1_1/SendCoinsClient.ts b/backend/src/federation/client/1_1/SendCoinsClient.ts new file mode 100644 index 000000000..586f8561e --- /dev/null +++ b/backend/src/federation/client/1_1/SendCoinsClient.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line camelcase +import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' + +// eslint-disable-next-line camelcase +export class SendCoinsClient extends V1_0_SendCoinsClient {} diff --git a/backend/src/federation/client/SendCoinsClientFactory.ts b/backend/src/federation/client/SendCoinsClientFactory.ts new file mode 100644 index 000000000..2c7b90f01 --- /dev/null +++ b/backend/src/federation/client/SendCoinsClientFactory.ts @@ -0,0 +1,62 @@ +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' + +// eslint-disable-next-line camelcase +import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' +// eslint-disable-next-line camelcase +import { SendCoinsClient as V1_1_SendCoinsClient } from '@/federation/client/1_1/SendCoinsClient' +import { ApiVersionType } from '@/federation/enum/apiVersionType' + +// eslint-disable-next-line camelcase +type SendCoinsClient = V1_0_SendCoinsClient | V1_1_SendCoinsClient + +interface SendCoinsClientInstance { + id: number + // eslint-disable-next-line no-use-before-define + client: SendCoinsClient +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class SendCoinsClientFactory { + private static instanceArray: SendCoinsClientInstance[] = [] + + /** + * The Singleton's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + private static createSendCoinsClient = (dbCom: DbFederatedCommunity) => { + switch (dbCom.apiVersion) { + case ApiVersionType.V1_0: + return new V1_0_SendCoinsClient(dbCom) + case ApiVersionType.V1_1: + return new V1_1_SendCoinsClient(dbCom) + default: + return null + } + } + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the Singleton class while keeping + * just one instance of each subclass around. + */ + public static getInstance(dbCom: DbFederatedCommunity): SendCoinsClient | null { + const instance = SendCoinsClientFactory.instanceArray.find( + (instance) => instance.id === dbCom.id, + ) + if (instance) { + return instance.client + } + const client = SendCoinsClientFactory.createSendCoinsClient(dbCom) + if (client) { + SendCoinsClientFactory.instanceArray.push({ + id: dbCom.id, + client, + } as SendCoinsClientInstance) + } + return client + } +} From 43bd594dc37d3ce25ef8ced01ded8e806c10dfd5 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Wed, 23 Aug 2023 01:19:19 +0200 Subject: [PATCH 02/15] commit wip --- .../federation/client/1_0/SendCoinsClient.ts | 18 +++++++++++++ .../client/1_0/query/commitSendCoins.ts | 25 +++++++++++++++++++ database/entity/PendingTransaction.ts | 2 +- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 backend/src/federation/client/1_0/query/commitSendCoins.ts diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index 0362d9c27..0264d8b70 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -5,6 +5,7 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' +import { commitSendCoins } from './query/commitSendCoins' import { revertSendCoins } from './query/revertSendCoins' import { voteForSendCoins } from './query/voteForSendCoins' @@ -66,4 +67,21 @@ export class SendCoinsClient { throw new LogError(`X-Com: revertSendCoins failed for endpoint=${this.endpoint}`, err) } } + + commitSendCoins = async (args: SendCoinsArgs): Promise => { + logger.debug(`X-Com: commitSendCoins against endpoint='${this.endpoint}'...`) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(commitSendCoins, { args }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.commitSendCoins?.acknowledged) { + logger.warn('X-Com: commitSendCoins without response data from endpoint', this.endpoint) + return false + } + logger.debug(`X-Com: commitSendCoins successful from endpoint=${this.endpoint}`) + return true + } catch (err) { + throw new LogError(`X-Com: commitSendCoins failed for endpoint=${this.endpoint}`, err) + } + } } diff --git a/backend/src/federation/client/1_0/query/commitSendCoins.ts b/backend/src/federation/client/1_0/query/commitSendCoins.ts new file mode 100644 index 000000000..eea934d00 --- /dev/null +++ b/backend/src/federation/client/1_0/query/commitSendCoins.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-request' + +export const commitSendCoins = gql` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $amount: Decimal! + $memo: String! + $communitySenderIdentifier: String! + $userSenderIdentifier: String! + $userSenderName: String! + ) { + commitSendCoins( + communityReceiverIdentifier: $communityReceiverIdentifier + userReceiverIdentifier: $userReceiverIdentifier + amount: $amount + memo: $memo + communitySenderIdentifier: $communitySenderIdentifier + userSenderIdentifier: $userSenderIdentifier + userSenderName: $userSenderName + ) { + acknowledged + } + } +` diff --git a/database/entity/PendingTransaction.ts b/database/entity/PendingTransaction.ts index 5ae28c2cd..dbd6f0c74 100644 --- a/database/entity/PendingTransaction.ts +++ b/database/entity/PendingTransaction.ts @@ -1 +1 @@ -export { Transaction } from './0071-add-pending_transactions-table/PendingTransaction' +export { PendingTransaction } from './0071-add-pending_transactions-table/PendingTransaction' From 52160e19d68ba806a320a048490c83f0de4e8403 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Wed, 23 Aug 2023 01:30:33 +0200 Subject: [PATCH 03/15] shift revert- and commitSendCoins to next isssue --- .../federation/client/1_0/SendCoinsClient.ts | 4 +-- .../client/1_0/query/commitSendCoins.ts | 25 ------------------- .../client/1_0/query/revertSendCoins.ts | 25 ------------------- 3 files changed, 2 insertions(+), 52 deletions(-) delete mode 100644 backend/src/federation/client/1_0/query/commitSendCoins.ts delete mode 100644 backend/src/federation/client/1_0/query/revertSendCoins.ts diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index 0264d8b70..cc0cd7ca4 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -5,8 +5,6 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' -import { commitSendCoins } from './query/commitSendCoins' -import { revertSendCoins } from './query/revertSendCoins' import { voteForSendCoins } from './query/voteForSendCoins' // eslint-disable-next-line camelcase @@ -51,6 +49,7 @@ export class SendCoinsClient { } } + /* revertSendCoins = async (args: SendCoinsArgs): Promise => { logger.debug(`X-Com: revertSendCoins against endpoint='${this.endpoint}'...`) try { @@ -84,4 +83,5 @@ export class SendCoinsClient { throw new LogError(`X-Com: commitSendCoins failed for endpoint=${this.endpoint}`, err) } } + */ } diff --git a/backend/src/federation/client/1_0/query/commitSendCoins.ts b/backend/src/federation/client/1_0/query/commitSendCoins.ts deleted file mode 100644 index eea934d00..000000000 --- a/backend/src/federation/client/1_0/query/commitSendCoins.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { gql } from 'graphql-request' - -export const commitSendCoins = gql` - mutation ( - $communityReceiverIdentifier: String! - $userReceiverIdentifier: String! - $amount: Decimal! - $memo: String! - $communitySenderIdentifier: String! - $userSenderIdentifier: String! - $userSenderName: String! - ) { - commitSendCoins( - communityReceiverIdentifier: $communityReceiverIdentifier - userReceiverIdentifier: $userReceiverIdentifier - amount: $amount - memo: $memo - communitySenderIdentifier: $communitySenderIdentifier - userSenderIdentifier: $userSenderIdentifier - userSenderName: $userSenderName - ) { - acknowledged - } - } -` diff --git a/backend/src/federation/client/1_0/query/revertSendCoins.ts b/backend/src/federation/client/1_0/query/revertSendCoins.ts deleted file mode 100644 index fd74feef1..000000000 --- a/backend/src/federation/client/1_0/query/revertSendCoins.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { gql } from 'graphql-request' - -export const revertSendCoins = gql` - mutation ( - $communityReceiverIdentifier: String! - $userReceiverIdentifier: String! - $amount: Decimal! - $memo: String! - $communitySenderIdentifier: String! - $userSenderIdentifier: String! - $userSenderName: String! - ) { - revertSendCoins( - communityReceiverIdentifier: $communityReceiverIdentifier - userReceiverIdentifier: $userReceiverIdentifier - amount: $amount - memo: $memo - communitySenderIdentifier: $communitySenderIdentifier - userSenderIdentifier: $userSenderIdentifier - userSenderName: $userSenderName - ) { - acknowledged - } - } -` From 53a6f1fec68f19d855149d3f5db6d1a2830351d6 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Thu, 24 Aug 2023 01:47:21 +0200 Subject: [PATCH 04/15] 1st implementation of voteForSendCoins --- .../client/1_0/model/SendCoinsArgs.ts | 5 +- federation/src/config/index.ts | 4 +- .../api/1_0/enum/PendingTransactionState.ts | 14 ++++ .../graphql/api/1_0/enum/TransactionTypeId.ts | 15 +++++ federation/src/graphql/api/1_0/model/Decay.ts | 41 ++++++++++++ .../graphql/api/1_0/model/SendCoinsArgs.ts | 29 ++++++++ .../api/1_0/resolver/SendCoinsResolver.ts | 66 +++++++++++++++++++ .../graphql/util/calculateRecepientBalance.ts | 20 ++++++ federation/src/graphql/util/decay.test.ts | 42 ++++++++++++ federation/src/graphql/util/decay.ts | 65 ++++++++++++++++++ federation/src/graphql/util/fullName.ts | 2 + .../src/graphql/util/getLastTransaction.ts | 12 ++++ federation/src/server/LogError.ts | 10 +++ 13 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 federation/src/graphql/api/1_0/enum/PendingTransactionState.ts create mode 100644 federation/src/graphql/api/1_0/enum/TransactionTypeId.ts create mode 100644 federation/src/graphql/api/1_0/model/Decay.ts create mode 100644 federation/src/graphql/api/1_0/model/SendCoinsArgs.ts create mode 100644 federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts create mode 100644 federation/src/graphql/util/calculateRecepientBalance.ts create mode 100644 federation/src/graphql/util/decay.test.ts create mode 100644 federation/src/graphql/util/decay.ts create mode 100644 federation/src/graphql/util/fullName.ts create mode 100644 federation/src/graphql/util/getLastTransaction.ts create mode 100644 federation/src/server/LogError.ts diff --git a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts index 2ba743368..3d15c04b1 100644 --- a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts +++ b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts @@ -1,5 +1,5 @@ import { Decimal } from 'decimal.js-light' -import { ArgsType, Field, Int } from 'type-graphql' +import { ArgsType, Field } from 'type-graphql' @ArgsType() export class SendCoinsArgs { @@ -9,6 +9,9 @@ export class SendCoinsArgs { @Field(() => String) userReceiverIdentifier: string + @Field(() => Date) + creationDate: Date + @Field(() => Decimal) amount: Decimal diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 29458d006..aceb15e98 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -12,13 +12,13 @@ Decimal.set({ const constants = { DB_VERSION: '0071-add-pending_transactions-table', - // DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 + 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 LOG_LEVEL: process.env.LOG_LEVEL || 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v1.2023-01-09', + EXPECTED: 'v2.2023-08-24', CURRENT: '', }, } diff --git a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts new file mode 100644 index 000000000..6a614be96 --- /dev/null +++ b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from 'type-graphql' + +export enum PendingTransactionState { + NEW = 1, + WAIT_ON_PENDING = 2, + PENDING = 3, + WAIT_ON_CONFIRM = 4, + CONFIRMED = 5, +} + +registerEnumType(PendingTransactionState, { + name: 'PendingTransactionState', // this one is mandatory + description: 'State of the PendingTransaction', // this one is optional +}) diff --git a/federation/src/graphql/api/1_0/enum/TransactionTypeId.ts b/federation/src/graphql/api/1_0/enum/TransactionTypeId.ts new file mode 100644 index 000000000..a7e39eebc --- /dev/null +++ b/federation/src/graphql/api/1_0/enum/TransactionTypeId.ts @@ -0,0 +1,15 @@ +import { registerEnumType } from 'type-graphql' + +export enum TransactionTypeId { + CREATION = 1, + SEND = 2, + RECEIVE = 3, + // This is a virtual property, never occurring on the database + DECAY = 4, + LINK_SUMMARY = 5, +} + +registerEnumType(TransactionTypeId, { + name: 'TransactionTypeId', // this one is mandatory + description: 'Type of the transaction', // this one is optional +}) diff --git a/federation/src/graphql/api/1_0/model/Decay.ts b/federation/src/graphql/api/1_0/model/Decay.ts new file mode 100644 index 000000000..0b710c234 --- /dev/null +++ b/federation/src/graphql/api/1_0/model/Decay.ts @@ -0,0 +1,41 @@ +import { Decimal } from 'decimal.js-light' +import { ObjectType, Field, Int } from 'type-graphql' + +interface DecayInterface { + balance: Decimal + decay: Decimal + roundedDecay: Decimal + start: Date | null + end: Date | null + duration: number | null +} + +@ObjectType() +export class Decay { + constructor({ balance, decay, roundedDecay, start, end, duration }: DecayInterface) { + this.balance = balance + this.decay = decay + this.roundedDecay = roundedDecay + this.start = start + this.end = end + this.duration = duration + } + + @Field(() => Decimal) + balance: Decimal + + @Field(() => Decimal) + decay: Decimal + + @Field(() => Decimal) + roundedDecay: Decimal + + @Field(() => Date, { nullable: true }) + start: Date | null + + @Field(() => Date, { nullable: true }) + end: Date | null + + @Field(() => Int, { nullable: true }) + duration: number | null +} diff --git a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts new file mode 100644 index 000000000..3d15c04b1 --- /dev/null +++ b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts @@ -0,0 +1,29 @@ +import { Decimal } from 'decimal.js-light' +import { ArgsType, Field } from 'type-graphql' + +@ArgsType() +export class SendCoinsArgs { + @Field(() => String) + communityReceiverIdentifier: string + + @Field(() => String) + userReceiverIdentifier: string + + @Field(() => Date) + creationDate: Date + + @Field(() => Decimal) + amount: Decimal + + @Field(() => String) + memo: string + + @Field(() => String) + communitySenderIdentifier: string + + @Field(() => String) + userSenderIdentifier: string + + @Field(() => String) + userSenderName: string +} diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts new file mode 100644 index 000000000..06247a425 --- /dev/null +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -0,0 +1,66 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Mutation, Query, 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 { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance' +import Decimal from 'decimal.js-light' +import { fullName } from '@/graphql/util/fullName' + +@Resolver() +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export class SendCoinsResolver { + @Mutation(() => Boolean) + async voteForSendCoins(args: SendCoinsArgs): Promise { + logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) + try { + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: args.communityReceiverIdentifier, + }) + if (!homeCom) { + throw new LogError(`voteForSendCoins with wrong communityReceiverIdentifier`) + } + // second check if receiver user exists in this community + const receiverUser = await DbUser.findOneBy({ gradidoID: args.userReceiverIdentifier }) + if (!receiverUser) { + throw new LogError( + `voteForSendCoins with unknown userReceiverIdentifier in the community=`, + homeCom.name, + ) + } + const receiveBalance = await calculateRecepientBalance( + receiverUser.id, + args.amount, + args.creationDate, + ) + const pendingTx = DbPendingTransaction.create() + pendingTx.amount = args.amount + pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0) + pendingTx.balanceDate = args.creationDate + pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) + pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null + pendingTx.linkedUserCommunityUuid = args.communitySenderIdentifier + pendingTx.linkedUserGradidoID = args.userSenderIdentifier + pendingTx.linkedUserName = args.userSenderName + pendingTx.memo = args.memo + pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null + pendingTx.state = PendingTransactionState.NEW + pendingTx.typeId = TransactionTypeId.RECEIVE + pendingTx.userCommunityUuid = args.communityReceiverIdentifier + pendingTx.userGradidoID = args.userReceiverIdentifier + pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) + + await DbPendingTransaction.insert(pendingTx) + logger.debug(`voteForSendCoins()-1_0... successfull`) + } catch (err) { + throw new LogError(`Error in voteForSendCoins with args=`, args) + } + return true + } +} diff --git a/federation/src/graphql/util/calculateRecepientBalance.ts b/federation/src/graphql/util/calculateRecepientBalance.ts new file mode 100644 index 000000000..56914afba --- /dev/null +++ b/federation/src/graphql/util/calculateRecepientBalance.ts @@ -0,0 +1,20 @@ +import { Decimal } from 'decimal.js-light' + +import { getLastTransaction } from './getLastTransaction' +import { calculateDecay } from './decay' +import { Decay } from '../api/1_0/model/Decay' + +export async function calculateRecepientBalance( + userId: number, + amount: Decimal, + time: Date, +): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { + const lastTransaction = await getLastTransaction(userId) + if (!lastTransaction) return null + + const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + + const balance = decay.balance.add(amount.toString()) + + return { balance, lastTransactionId: lastTransaction.id, decay } +} diff --git a/federation/src/graphql/util/decay.test.ts b/federation/src/graphql/util/decay.test.ts new file mode 100644 index 000000000..1d4ebab3b --- /dev/null +++ b/federation/src/graphql/util/decay.test.ts @@ -0,0 +1,42 @@ +import { Decimal } from 'decimal.js-light' + +import { decayFormula, calculateDecay } from './decay' + +describe('utils/decay', () => { + describe('decayFormula', () => { + it('has base 0.99999997802044727', () => { + const amount = new Decimal(1.0) + const seconds = 1 + // TODO: toString() was required, we could not compare two decimals + expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012') + }) + it('has correct backward calculation', () => { + const amount = new Decimal(1.0) + const seconds = -1 + expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444') + }) + // we get pretty close, but not exact here, skipping + // eslint-disable-next-line jest/no-disabled-tests + it.skip('has correct forward calculation', () => { + const amount = new Decimal(1.0).div( + new Decimal('0.99999997803504048973201202316767079413460520837376'), + ) + const seconds = 1 + expect(decayFormula(amount, seconds).toString()).toBe('1.0') + }) + }) + it('has base 0.99999997802044727', () => { + const now = new Date() + now.setSeconds(1) + const oneSecondAgo = new Date(now.getTime()) + oneSecondAgo.setSeconds(0) + expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe( + '0.999999978035040489732012', + ) + }) + + it('returns input amount when from and to is the same', () => { + const now = new Date() + expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100') + }) +}) diff --git a/federation/src/graphql/util/decay.ts b/federation/src/graphql/util/decay.ts new file mode 100644 index 000000000..f195075ff --- /dev/null +++ b/federation/src/graphql/util/decay.ts @@ -0,0 +1,65 @@ +import { Decimal } from 'decimal.js-light' + +import { LogError } from '@/server/LogError' +import CONFIG from '@/config' +import { Decay } from '../api/1_0/model/Decay' + +// TODO: externalize all those definitions and functions into an external decay library + +function decayFormula(value: Decimal, seconds: number): Decimal { + // TODO why do we need to convert this here to a stting to work properly? + return value.mul( + new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(), + ) +} + +function calculateDecay( + amount: Decimal, + from: Date, + to: Date, + startBlock: Date = CONFIG.DECAY_START_TIME, +): Decay { + const fromMs = from.getTime() + const toMs = to.getTime() + const startBlockMs = startBlock.getTime() + + if (toMs < fromMs) { + throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid') + } + + // Initialize with no decay + const decay: Decay = { + balance: amount, + decay: new Decimal(0), + roundedDecay: new Decimal(0), + start: null, + end: null, + duration: null, + } + + // decay started after end date; no decay + if (startBlockMs > toMs) { + return decay + } + // decay started before start date; decay for full duration + if (startBlockMs < fromMs) { + decay.start = from + decay.duration = (toMs - fromMs) / 1000 + } + // decay started between start and end date; decay from decay start till end date + else { + decay.start = startBlock + decay.duration = (toMs - startBlockMs) / 1000 + } + + decay.end = to + decay.balance = decayFormula(amount, decay.duration) + decay.decay = decay.balance.minus(amount) + decay.roundedDecay = amount + .toDecimalPlaces(2, Decimal.ROUND_DOWN) + .minus(decay.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN).toString()) + .mul(-1) + return decay +} + +export { decayFormula, calculateDecay } diff --git a/federation/src/graphql/util/fullName.ts b/federation/src/graphql/util/fullName.ts new file mode 100644 index 000000000..7473f5ed0 --- /dev/null +++ b/federation/src/graphql/util/fullName.ts @@ -0,0 +1,2 @@ +export const fullName = (firstName: string, lastName: string): string => + [firstName, lastName].filter(Boolean).join(' ') diff --git a/federation/src/graphql/util/getLastTransaction.ts b/federation/src/graphql/util/getLastTransaction.ts new file mode 100644 index 000000000..0d7747088 --- /dev/null +++ b/federation/src/graphql/util/getLastTransaction.ts @@ -0,0 +1,12 @@ +import { Transaction as DbTransaction } from '@entity/Transaction' + +export const getLastTransaction = async ( + userId: number, + relations?: string[], +): Promise => { + return DbTransaction.findOne({ + where: { userId }, + order: { balanceDate: 'DESC', id: 'DESC' }, + relations, + }) +} diff --git a/federation/src/server/LogError.ts b/federation/src/server/LogError.ts new file mode 100644 index 000000000..b29e83dc8 --- /dev/null +++ b/federation/src/server/LogError.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { federationLogger as logger } from './logger' + +export class LogError extends Error { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(msg: string, ...details: any[]) { + super(msg) + logger.error(msg, ...details) + } +} From 7d8b97b8611b559a4745fe9b8505486812379c29 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Thu, 24 Aug 2023 21:27:50 +0200 Subject: [PATCH 05/15] correct voteSendCoins graphql schema --- .../federation/client/1_0/SendCoinsClient.ts | 2 +- .../client/1_0/query/voteForSendCoins.ts | 6 +-- .../api/1_0/resolver/SendCoinsResolver.ts | 49 ++++++++++++------- .../scalar/{Decimal.ts.unused => Decimal.ts} | 5 +- federation/src/graphql/schema.ts | 6 +-- 5 files changed, 40 insertions(+), 28 deletions(-) rename federation/src/graphql/scalar/{Decimal.ts.unused => Decimal.ts} (74%) diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index cc0cd7ca4..bc6d9c58d 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -33,7 +33,7 @@ export class SendCoinsClient { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(voteForSendCoins, { args }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.voteForSendCoins?.vote) { + if (!data?.voteForSendCoins) { logger.warn('X-Com: voteForSendCoins without response data from endpoint', this.endpoint) return false } diff --git a/backend/src/federation/client/1_0/query/voteForSendCoins.ts b/backend/src/federation/client/1_0/query/voteForSendCoins.ts index 4f4dd892a..9cdae73f3 100644 --- a/backend/src/federation/client/1_0/query/voteForSendCoins.ts +++ b/backend/src/federation/client/1_0/query/voteForSendCoins.ts @@ -4,6 +4,7 @@ export const voteForSendCoins = gql` mutation ( $communityReceiverIdentifier: String! $userReceiverIdentifier: String! + $creationDate: Date! $amount: Decimal! $memo: String! $communitySenderIdentifier: String! @@ -13,13 +14,12 @@ export const voteForSendCoins = gql` voteForSendCoins( communityReceiverIdentifier: $communityReceiverIdentifier userReceiverIdentifier: $userReceiverIdentifier + creationDate: $creationDate amount: $amount memo: $memo communitySenderIdentifier: $communitySenderIdentifier userSenderIdentifier: $userSenderIdentifier userSenderName: $userSenderName - ) { - vote - } + ) } ` diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 06247a425..dd8958e9f 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Mutation, Query, Resolver } from 'type-graphql' +import { Args, Mutation, Query, 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' @@ -16,50 +16,61 @@ import { fullName } from '@/graphql/util/fullName' // eslint-disable-next-line @typescript-eslint/no-unused-vars export class SendCoinsResolver { @Mutation(() => Boolean) - async voteForSendCoins(args: SendCoinsArgs): Promise { + async voteForSendCoins( + @Args() + { + communityReceiverIdentifier, + userReceiverIdentifier, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + }: SendCoinsArgs, + ): Promise { logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) try { // first check if receiver community is correct const homeCom = await DbCommunity.findOneBy({ - communityUuid: args.communityReceiverIdentifier, + communityUuid: communityReceiverIdentifier, }) if (!homeCom) { - throw new LogError(`voteForSendCoins with wrong communityReceiverIdentifier`) + throw new LogError( + `voteForSendCoins with wrong communityReceiverIdentifier`, + communityReceiverIdentifier, + ) } // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneBy({ gradidoID: args.userReceiverIdentifier }) + const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) if (!receiverUser) { throw new LogError( `voteForSendCoins with unknown userReceiverIdentifier in the community=`, homeCom.name, ) } - const receiveBalance = await calculateRecepientBalance( - receiverUser.id, - args.amount, - args.creationDate, - ) + const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, creationDate) const pendingTx = DbPendingTransaction.create() - pendingTx.amount = args.amount + pendingTx.amount = amount pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0) - pendingTx.balanceDate = args.creationDate + pendingTx.balanceDate = creationDate pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null - pendingTx.linkedUserCommunityUuid = args.communitySenderIdentifier - pendingTx.linkedUserGradidoID = args.userSenderIdentifier - pendingTx.linkedUserName = args.userSenderName - pendingTx.memo = args.memo + pendingTx.linkedUserCommunityUuid = communitySenderIdentifier + pendingTx.linkedUserGradidoID = userSenderIdentifier + pendingTx.linkedUserName = userSenderName + pendingTx.memo = memo pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null pendingTx.state = PendingTransactionState.NEW pendingTx.typeId = TransactionTypeId.RECEIVE - pendingTx.userCommunityUuid = args.communityReceiverIdentifier - pendingTx.userGradidoID = args.userReceiverIdentifier + pendingTx.userCommunityUuid = communityReceiverIdentifier + pendingTx.userGradidoID = userReceiverIdentifier pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) await DbPendingTransaction.insert(pendingTx) logger.debug(`voteForSendCoins()-1_0... successfull`) } catch (err) { - throw new LogError(`Error in voteForSendCoins with args=`, args) + throw new LogError(`Error in voteForSendCoins: `, err) } return true } diff --git a/federation/src/graphql/scalar/Decimal.ts.unused b/federation/src/graphql/scalar/Decimal.ts similarity index 74% rename from federation/src/graphql/scalar/Decimal.ts.unused rename to federation/src/graphql/scalar/Decimal.ts index da5a99e0c..96804bdfa 100644 --- a/federation/src/graphql/scalar/Decimal.ts.unused +++ b/federation/src/graphql/scalar/Decimal.ts @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { Decimal } from 'decimal.js-light' import { GraphQLScalarType, Kind } from 'graphql' -import Decimal from 'decimal.js-light' -export default new GraphQLScalarType({ +export const DecimalScalar = new GraphQLScalarType({ name: 'Decimal', description: 'The `Decimal` scalar type to represent currency values', diff --git a/federation/src/graphql/schema.ts b/federation/src/graphql/schema.ts index 015ea124f..0951c1000 100644 --- a/federation/src/graphql/schema.ts +++ b/federation/src/graphql/schema.ts @@ -2,15 +2,15 @@ import { GraphQLSchema } from 'graphql' import { buildSchema } from 'type-graphql' // import isAuthorized from './directive/isAuthorized' -// import DecimalScalar from './scalar/Decimal' -// import Decimal from 'decimal.js-light' +import { DecimalScalar } from './scalar/Decimal' +import { Decimal } from 'decimal.js-light' import { getApiResolvers } from './api/schema' const schema = async (): Promise => { return await buildSchema({ resolvers: [getApiResolvers()], // authChecker: isAuthorized, - // scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], + scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], }) } From 48af8a7a90e35c81a25c174125468440bf6f7949 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Fri, 25 Aug 2023 03:21:18 +0200 Subject: [PATCH 06/15] first draft of write senderPendingTX --- backend/src/config/index.ts | 5 +- .../federation/client/1_0/SendCoinsClient.ts | 19 +++-- .../client/1_0/model/SendCoinsResult.ts | 17 ++++ .../resolver/util/processXComSendCoins.ts | 80 +++++++++++++++++++ backend/src/util/calculateSenderBalance.ts | 21 +++++ .../graphql/api/1_0/model/SendCoinsResult.ts | 17 ++++ .../api/1_0/resolver/SendCoinsResolver.ts | 9 ++- 7 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 backend/src/federation/client/1_0/model/SendCoinsResult.ts create mode 100644 backend/src/graphql/resolver/util/processXComSendCoins.ts create mode 100644 backend/src/util/calculateSenderBalance.ts create mode 100644 federation/src/graphql/api/1_0/model/SendCoinsResult.ts diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 744f1d3cc..9fbf66507 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -19,7 +19,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v18.2023-07-10', + EXPECTED: 'v19.2023-08-25', CURRENT: '', }, } @@ -124,6 +124,9 @@ if ( const federation = { FEDERATION_VALIDATE_COMMUNITY_TIMER: Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000, + // default value for community-uuid is equal uuid of stage-3 + FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID: + process.env.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID ?? '56a55482-909e-46a4-bfa2-cd025e894ebc', } export const CONFIG = { diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index bc6d9c58d..e15e13100 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -6,6 +6,7 @@ import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' import { voteForSendCoins } from './query/voteForSendCoins' +import { SendCoinsResult } from './model/SendCoinsResult' // eslint-disable-next-line camelcase export class SendCoinsClient { @@ -27,23 +28,25 @@ export class SendCoinsClient { }) } - voteForSendCoins = async (args: SendCoinsArgs): Promise => { + voteForSendCoins = async (args: SendCoinsArgs): Promise => { logger.debug('X-Com: voteForSendCoins against endpoint', this.endpoint) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(voteForSendCoins, { args }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.voteForSendCoins) { - logger.warn('X-Com: voteForSendCoins without response data from endpoint', this.endpoint) - return false + if (!data?.voteForSendCoins?.SendCoinsResult.vote) { + logger.warn( + 'X-Com: voteForSendCoins failed with: ', + data?.voteForSendCoins?.SendCoinsResult, + ) + return null } logger.debug( - 'X-Com: voteForSendCoins successful from endpoint', - this.endpoint, + 'X-Com: voteForSendCoins successful with result=', // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - data.voteForSendCoins.vote, + data.voteForSendCoins.SendCoinsResult, ) - return true + return data.voteForSendCoins.SendCoinsResult } catch (err) { throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err) } diff --git a/backend/src/federation/client/1_0/model/SendCoinsResult.ts b/backend/src/federation/client/1_0/model/SendCoinsResult.ts new file mode 100644 index 000000000..1897410cc --- /dev/null +++ b/backend/src/federation/client/1_0/model/SendCoinsResult.ts @@ -0,0 +1,17 @@ +import { ArgsType, Field } from 'type-graphql' + +@ArgsType() +export class SendCoinsResult { + constructor() { + this.vote = false + } + + @Field(() => Boolean) + vote: boolean + + @Field(() => String) + receiverFirstName: string + + @Field(() => String) + receiverLastName: string +} diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts new file mode 100644 index 000000000..957522f65 --- /dev/null +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -0,0 +1,80 @@ +import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' +import { User as dbUser } from '@entity/User' +import { Decimal } from 'decimal.js-light' + +import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' +// eslint-disable-next-line camelcase +import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' +import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory' +import { backendLogger as logger } from '@/server/logger' +import { CONFIG } from '@/config' +import { fullName } from '@/util/utilities' +import { calculateSenderBalance } from '@/util/calculateSenderBalance' +import { LogError } from '@/server/LogError' + + +export async function processXComSendCoins( + receiverFCom: DbFederatedCommunity, + senderFCom: DbFederatedCommunity, + receiverCom: DbCommunity, + senderCom: DbCommunity, + creationDate: Date, + amount: Decimal, + memo: string, + sender: dbUser, + recipient: dbUser, +): Promise { + try { + const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate) + if (!senderBalance) { + throw new LogError('User has not enough GDD or amount is < 0', senderBalance) + } + + const client = SendCoinsClientFactory.getInstance(receiverFCom) + // eslint-disable-next-line camelcase + if (client instanceof V1_0_SendCoinsClient) { + const args = new SendCoinsArgs() + args.communityReceiverIdentifier = receiverCom.communityUuid + ? receiverCom.communityUuid + : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID + args.userReceiverIdentifier = recipient.gradidoID + args.creationDate = creationDate + 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) + const result = await client.voteForSendCoins(args) + if(result) { + const pendingTx = DbPendingTransaction.create() + pendingTx.amount = amount.mul(-1) + pendingTx.balance = senderBalance ? senderBalance.balance : new Decimal(0) + pendingTx.balanceDate = creationDate + pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0) + pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null + pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid + ? receiverCom.communityUuid + : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID + pendingTx.linkedUserGradidoID = recipient.gradidoID + pendingTx.linkedUserName = userSenderName + pendingTx.memo = memo + pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null + pendingTx.state = PendingTransactionState.NEW + pendingTx.typeId = TransactionTypeId.RECEIVE + pendingTx.userCommunityUuid = communityReceiverIdentifier + pendingTx.userGradidoID = userReceiverIdentifier + pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) + + await DbPendingTransaction.insert(pendingTx) + logger.debug(`voteForSendCoins()-1_0... successfull`) + } + } + } catch (err) { + logger.error(`Error:`, err) + } + return true +} diff --git a/backend/src/util/calculateSenderBalance.ts b/backend/src/util/calculateSenderBalance.ts new file mode 100644 index 000000000..89e417d35 --- /dev/null +++ b/backend/src/util/calculateSenderBalance.ts @@ -0,0 +1,21 @@ +import { Decimal } from 'decimal.js-light' + +import { Decay } from '@model/Decay' + +import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' + +import { calculateDecay } from './decay' + +export async function calculateSenderBalance( + userId: number, + amount: Decimal, + time: Date, +): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { + const lastTransaction = await getLastTransaction(userId) + if (!lastTransaction) return null + + const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + + const balance = decay.balance.add(amount.toString()) + return { balance, lastTransactionId: lastTransaction.id, decay } +} 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..1897410cc --- /dev/null +++ b/federation/src/graphql/api/1_0/model/SendCoinsResult.ts @@ -0,0 +1,17 @@ +import { ArgsType, Field } from 'type-graphql' + +@ArgsType() +export class SendCoinsResult { + constructor() { + this.vote = false + } + + @Field(() => Boolean) + vote: boolean + + @Field(() => String) + receiverFirstName: string + + @Field(() => String) + receiverLastName: string +} diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index dd8958e9f..73c6e077c 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -11,6 +11,7 @@ import { TransactionTypeId } from '../enum/TransactionTypeId' import { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance' import Decimal from 'decimal.js-light' import { fullName } from '@/graphql/util/fullName' +import { SendCoinsResult } from '../model/SendCoinsResult' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -28,7 +29,8 @@ export class SendCoinsResolver { userSenderIdentifier, userSenderName, }: SendCoinsArgs, - ): Promise { + ): Promise { + const result = new SendCoinsResult() logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) try { // first check if receiver community is correct @@ -68,10 +70,13 @@ export class SendCoinsResolver { pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) await DbPendingTransaction.insert(pendingTx) + result.vote = true + result.receiverFirstName = receiverUser.firstName + result.receiverLastName = receiverUser.lastName logger.debug(`voteForSendCoins()-1_0... successfull`) } catch (err) { throw new LogError(`Error in voteForSendCoins: `, err) } - return true + return result } } From a8eaa6f35328c06e21e4acb77349cdf0e4f904b8 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Fri, 25 Aug 2023 16:50:02 +0200 Subject: [PATCH 07/15] rework writing the pendingTX on both sides --- .../federation/client/1_0/SendCoinsClient.ts | 15 ++--- .../resolver/util/processXComSendCoins.ts | 61 +++++++++++-------- .../graphql/api/1_0/model/SendCoinsResult.ts | 17 ------ .../api/1_0/resolver/SendCoinsResolver.ts | 9 +-- 4 files changed, 46 insertions(+), 56 deletions(-) delete mode 100644 federation/src/graphql/api/1_0/model/SendCoinsResult.ts diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index e15e13100..f599dbafd 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -6,7 +6,6 @@ import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' import { voteForSendCoins } from './query/voteForSendCoins' -import { SendCoinsResult } from './model/SendCoinsResult' // eslint-disable-next-line camelcase export class SendCoinsClient { @@ -28,25 +27,27 @@ export class SendCoinsClient { }) } - voteForSendCoins = async (args: SendCoinsArgs): Promise => { + voteForSendCoins = async (args: SendCoinsArgs): Promise => { logger.debug('X-Com: voteForSendCoins against endpoint', this.endpoint) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(voteForSendCoins, { args }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.voteForSendCoins?.SendCoinsResult.vote) { + if (!data?.voteForSendCoins?.voteForSendCoins) { logger.warn( 'X-Com: voteForSendCoins failed with: ', - data?.voteForSendCoins?.SendCoinsResult, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + data.voteForSendCoins.voteForSendCoins, ) - return null + return } logger.debug( 'X-Com: voteForSendCoins successful with result=', // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - data.voteForSendCoins.SendCoinsResult, + data.voteForSendCoins, ) - return data.voteForSendCoins.SendCoinsResult + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return data.voteForSendCoins.voteForSendCoins } catch (err) { throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err) } diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 957522f65..1cd854c60 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -4,16 +4,17 @@ import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTrans import { User as dbUser } from '@entity/User' import { Decimal } from 'decimal.js-light' +import { CONFIG } from '@/config' import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' // eslint-disable-next-line camelcase import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory' -import { backendLogger as logger } from '@/server/logger' -import { CONFIG } from '@/config' -import { fullName } from '@/util/utilities' -import { calculateSenderBalance } from '@/util/calculateSenderBalance' +import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState' +import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { LogError } from '@/server/LogError' - +import { backendLogger as logger } from '@/server/logger' +import { calculateSenderBalance } from '@/util/calculateSenderBalance' +import { fullName } from '@/util/utilities' export async function processXComSendCoins( receiverFCom: DbFederatedCommunity, @@ -27,6 +28,7 @@ export async function processXComSendCoins( recipient: dbUser, ): Promise { try { + // first calculate the sender balance and check if the transaction is allowed const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate) if (!senderBalance) { throw new LogError('User has not enough GDD or amount is < 0', senderBalance) @@ -48,28 +50,35 @@ export async function processXComSendCoins( : 'homeCom-UUID' args.userSenderIdentifier = sender.gradidoID args.userSenderName = fullName(sender.firstName, sender.lastName) - const result = await client.voteForSendCoins(args) - if(result) { - const pendingTx = DbPendingTransaction.create() - pendingTx.amount = amount.mul(-1) - pendingTx.balance = senderBalance ? senderBalance.balance : new Decimal(0) - pendingTx.balanceDate = creationDate - pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0) - pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null - pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid - ? receiverCom.communityUuid - : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID - pendingTx.linkedUserGradidoID = recipient.gradidoID - pendingTx.linkedUserName = userSenderName - pendingTx.memo = memo - pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null - pendingTx.state = PendingTransactionState.NEW - pendingTx.typeId = TransactionTypeId.RECEIVE - pendingTx.userCommunityUuid = communityReceiverIdentifier - pendingTx.userGradidoID = userReceiverIdentifier - pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) + const recipientName = await client.voteForSendCoins(args) + if (recipientName) { + // writing the pending transaction on receiver-side was successfull, so now write the sender side + try { + const pendingTx = DbPendingTransaction.create() + pendingTx.amount = amount.mul(-1) + pendingTx.balance = senderBalance ? senderBalance.balance : new Decimal(0) + pendingTx.balanceDate = creationDate + pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0) + pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null + pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid + ? receiverCom.communityUuid + : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID + pendingTx.linkedUserGradidoID = recipient.gradidoID + pendingTx.linkedUserName = recipientName + pendingTx.memo = memo + pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null + pendingTx.state = PendingTransactionState.NEW + pendingTx.typeId = TransactionTypeId.SEND + if (senderCom.communityUuid) pendingTx.userCommunityUuid = senderCom.communityUuid + pendingTx.userGradidoID = sender.gradidoID + pendingTx.userName = fullName(sender.firstName, sender.lastName) - await DbPendingTransaction.insert(pendingTx) + await DbPendingTransaction.insert(pendingTx) + } catch (err) { + logger.error(`Error in writing sender pending transaction: `, err) + // revert the existing pending transaction on receiver side + // TODO in the issue #3186 + } logger.debug(`voteForSendCoins()-1_0... successfull`) } } diff --git a/federation/src/graphql/api/1_0/model/SendCoinsResult.ts b/federation/src/graphql/api/1_0/model/SendCoinsResult.ts deleted file mode 100644 index 1897410cc..000000000 --- a/federation/src/graphql/api/1_0/model/SendCoinsResult.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ArgsType, Field } from 'type-graphql' - -@ArgsType() -export class SendCoinsResult { - constructor() { - this.vote = false - } - - @Field(() => Boolean) - vote: boolean - - @Field(() => String) - receiverFirstName: string - - @Field(() => String) - receiverLastName: string -} diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 73c6e077c..ba23ae530 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -11,7 +11,6 @@ import { TransactionTypeId } from '../enum/TransactionTypeId' import { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance' import Decimal from 'decimal.js-light' import { fullName } from '@/graphql/util/fullName' -import { SendCoinsResult } from '../model/SendCoinsResult' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -29,9 +28,9 @@ export class SendCoinsResolver { userSenderIdentifier, userSenderName, }: SendCoinsArgs, - ): Promise { - const result = new SendCoinsResult() + ): Promise { logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) + let result: string | null = null try { // first check if receiver community is correct const homeCom = await DbCommunity.findOneBy({ @@ -70,9 +69,7 @@ export class SendCoinsResolver { pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName) await DbPendingTransaction.insert(pendingTx) - result.vote = true - result.receiverFirstName = receiverUser.firstName - result.receiverLastName = receiverUser.lastName + result = pendingTx.userName logger.debug(`voteForSendCoins()-1_0... successfull`) } catch (err) { throw new LogError(`Error in voteForSendCoins: `, err) From aaf02910315947c2dde0f8842aae8e6b4f1772b7 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Fri, 25 Aug 2023 18:24:09 +0200 Subject: [PATCH 08/15] first draft for revertSendCoins endpoints --- backend/src/config/index.ts | 2 + .../federation/client/1_0/SendCoinsClient.ts | 12 +-- .../client/1_0/query/revertSendCoins.ts | 25 ++++++ .../resolver/util/processXComSendCoins.ts | 9 ++- .../api/1_0/resolver/SendCoinsResolver.ts | 77 ++++++++++++++++++- 5 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 backend/src/federation/client/1_0/query/revertSendCoins.ts diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 9fbf66507..26e759c47 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -127,6 +127,8 @@ const federation = { // default value for community-uuid is equal uuid of stage-3 FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID: process.env.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID ?? '56a55482-909e-46a4-bfa2-cd025e894ebc', + FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS: + process.env.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS ?? 3, } export const CONFIG = { diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index f599dbafd..c57ca4823 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -5,6 +5,7 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { SendCoinsArgs } from './model/SendCoinsArgs' +import { revertSendCoins } from './query/revertSendCoins' import { voteForSendCoins } from './query/voteForSendCoins' // eslint-disable-next-line camelcase @@ -28,7 +29,7 @@ export class SendCoinsClient { } voteForSendCoins = async (args: SendCoinsArgs): Promise => { - logger.debug('X-Com: voteForSendCoins against endpoint', this.endpoint) + logger.debug('X-Com: voteForSendCoins against endpoint=', this.endpoint) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(voteForSendCoins, { args }) @@ -53,24 +54,25 @@ export class SendCoinsClient { } } - /* revertSendCoins = async (args: SendCoinsArgs): Promise => { - logger.debug(`X-Com: revertSendCoins against endpoint='${this.endpoint}'...`) + logger.debug('X-Com: revertSendCoins against endpoint=', this.endpoint) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data } = await this.client.rawRequest(revertSendCoins, { args }) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!data?.revertSendCoins?.acknowledged) { + if (!data?.revertSendCoins?.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}`) return true } catch (err) { - throw new LogError(`X-Com: revertSendCoins failed for endpoint=${this.endpoint}`, err) + logger.error(`X-Com: revertSendCoins failed for endpoint=${this.endpoint}`, err) + return false } } + /* commitSendCoins = async (args: SendCoinsArgs): Promise => { logger.debug(`X-Com: commitSendCoins against endpoint='${this.endpoint}'...`) try { diff --git a/backend/src/federation/client/1_0/query/revertSendCoins.ts b/backend/src/federation/client/1_0/query/revertSendCoins.ts new file mode 100644 index 000000000..63c3cff63 --- /dev/null +++ b/backend/src/federation/client/1_0/query/revertSendCoins.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-request' + +export const revertSendCoins = gql` + mutation ( + $communityReceiverIdentifier: String! + $userReceiverIdentifier: String! + $creationDate: Date! + $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 + ) + } +` diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 1cd854c60..59b0246d4 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -77,7 +77,14 @@ export async function processXComSendCoins( } catch (err) { logger.error(`Error in writing sender pending transaction: `, err) // revert the existing pending transaction on receiver side - // TODO in the issue #3186 + let revertCount = 0 + do { + if (await client.revertSendCoins(args)) { + logger.debug('revertSendCoins()-1_0... successfull') + throw new LogError('Error in writing sender pending transaction: `, err') + } + } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) + throw new LogError('Error in reverting receiver pending transaction even after retries') } logger.debug(`voteForSendCoins()-1_0... successfull`) } diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index ba23ae530..d03f1e4f0 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -15,7 +15,7 @@ import { fullName } from '@/graphql/util/fullName' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars export class SendCoinsResolver { - @Mutation(() => Boolean) + @Mutation(() => String) async voteForSendCoins( @Args() { @@ -76,4 +76,79 @@ export class SendCoinsResolver { } return result } + + @Mutation(() => Boolean) + async revertSendCoins( + @Args() + { + communityReceiverIdentifier, + userReceiverIdentifier, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + }: SendCoinsArgs, + ): Promise { + logger.debug(`revertSendCoins() via apiVersion=1_0 ...`) + try { + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: communityReceiverIdentifier, + }) + if (!homeCom) { + throw new LogError( + `revertSendCoins with wrong communityReceiverIdentifier`, + communityReceiverIdentifier, + ) + } + // second check if receiver user exists in this community + const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) + if (!receiverUser) { + throw new LogError( + `revertSendCoins with unknown userReceiverIdentifier in the community=`, + homeCom.name, + ) + } + const pendingTx = await DbPendingTransaction.findOneBy({ + userCommunityUuid: communityReceiverIdentifier, + userGradidoID: userReceiverIdentifier, + state: PendingTransactionState.NEW, + typeId: TransactionTypeId.RECEIVE, + balanceDate: creationDate, + linkedUserCommunityUuid: communitySenderIdentifier, + linkedUserGradidoID: userSenderIdentifier, + }) + logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx) + if (pendingTx && pendingTx.amount === amount) { + logger.debug('XCom: revertSendCoins matching pendingTX for remove...') + try { + await pendingTx.remove() + logger.debug('XCom: revertSendCoins pendingTX for remove successfully') + } catch (err) { + throw new LogError('Error in revertSendCoins on removing pendingTx of receiver: ', err) + } + } else { + logger.debug('XCom: revertSendCoins NOT matching pendingTX for remove...') + throw new LogError( + `Can't find in revertSendCoins the pending receiver TX for args=`, + communityReceiverIdentifier, + userReceiverIdentifier, + PendingTransactionState.NEW, + TransactionTypeId.RECEIVE, + creationDate, + amount, + memo, + communitySenderIdentifier, + userSenderIdentifier, + userSenderName, + ) + } + logger.debug(`revertSendCoins()-1_0... successfull`) + return true + } catch (err) { + throw new LogError(`Error in revertSendCoins: `, err) + } + } } From 1016dd071a813e992e35ad752369a65f0ae46c79 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Fri, 25 Aug 2023 18:51:04 +0200 Subject: [PATCH 09/15] split XCom-sendCoins processing in pending and committing --- .../resolver/util/processXComSendCoins.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 59b0246d4..107f65cde 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -16,9 +16,8 @@ import { backendLogger as logger } from '@/server/logger' import { calculateSenderBalance } from '@/util/calculateSenderBalance' import { fullName } from '@/util/utilities' -export async function processXComSendCoins( +export async function processXComPendingSendCoins( receiverFCom: DbFederatedCommunity, - senderFCom: DbFederatedCommunity, receiverCom: DbCommunity, senderCom: DbCommunity, creationDate: Date, @@ -28,11 +27,23 @@ export async function processXComSendCoins( recipient: dbUser, ): Promise { try { + logger.debug( + `XCom: processXComPendingSendCoins...`, + receiverFCom, + receiverCom, + senderCom, + creationDate, + amount, + memo, + sender, + recipient, + ) // first calculate the sender balance and check if the transaction is allowed const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate) if (!senderBalance) { throw new LogError('User has not enough GDD or amount is < 0', senderBalance) } + logger.debug(`X-Com: calculated senderBalance = `, senderBalance) const client = SendCoinsClientFactory.getInstance(receiverFCom) // eslint-disable-next-line camelcase @@ -50,7 +61,9 @@ export async function processXComSendCoins( : 'homeCom-UUID' args.userSenderIdentifier = sender.gradidoID args.userSenderName = 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) { // writing the pending transaction on receiver-side was successfull, so now write the sender side try { @@ -72,19 +85,26 @@ export async function processXComSendCoins( if (senderCom.communityUuid) pendingTx.userCommunityUuid = senderCom.communityUuid pendingTx.userGradidoID = sender.gradidoID pendingTx.userName = fullName(sender.firstName, sender.lastName) + logger.debug(`X-Com: initialized sender pendingTX=`, pendingTx) await DbPendingTransaction.insert(pendingTx) + logger.debug(`X-Com: sender pendingTx successfully inserted...`) } 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 revertSendCoins of receiver`) do { if (await client.revertSendCoins(args)) { - logger.debug('revertSendCoins()-1_0... successfull') + logger.debug(`revertSendCoins()-1_0... successfull after revertCount=`, revertCount) + // treat revertingSendCoins as an error of the whole sendCoins-process throw new LogError('Error in writing sender pending transaction: `, err') } } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) - throw new LogError('Error in reverting receiver pending transaction even after retries') + throw new LogError( + `Error in reverting receiver pending transaction even after revertCount=`, + revertCount, + ) } logger.debug(`voteForSendCoins()-1_0... successfull`) } From 27aa1ff4f054fe4d2f5d067644cf3ffc43349656 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 5 Sep 2023 16:27:40 +0200 Subject: [PATCH 10/15] add voteForsendCoins tests --- federation/src/config/index.ts | 4 +- .../graphql/api/1_0/model/SendCoinsArgs.ts | 4 +- .../1_0/resolver/SendCoinsResolver.test.ts | 193 ++++++++++++++++++ .../api/1_0/resolver/SendCoinsResolver.ts | 44 ++-- federation/test/helpers.test.ts | 7 + federation/test/helpers.ts | 63 ++++++ 6 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts create mode 100644 federation/test/helpers.test.ts create mode 100644 federation/test/helpers.ts diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index aceb15e98..74a53ed1b 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -1,14 +1,12 @@ // ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) +import { Decimal } from 'decimal.js-light' import dotenv from 'dotenv' dotenv.config() -/* -import Decimal from 'decimal.js-light' Decimal.set({ precision: 25, rounding: Decimal.ROUND_HALF_UP, }) -*/ const constants = { DB_VERSION: '0071-add-pending_transactions-table', diff --git a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts index 3d15c04b1..545aab822 100644 --- a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts +++ b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts @@ -9,8 +9,8 @@ export class SendCoinsArgs { @Field(() => String) userReceiverIdentifier: string - @Field(() => Date) - creationDate: Date + @Field(() => String) + creationDate: string @Field(() => Decimal) amount: Decimal diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts new file mode 100644 index 000000000..a9ea7e068 --- /dev/null +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts @@ -0,0 +1,193 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +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 { 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' + +let mutate: ApolloServerTestClient['mutate'], con: Connection +// let query: ApolloServerTestClient['query'] + +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +CONFIG.FEDERATION_API = '1_0' + +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() +}) + +afterAll(async () => { + // await cleanDB() + await con.destroy() +}) + +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 + ) + } +` + + 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() + 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), + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('voteForSendCoins with wrong communityReceiverIdentifier')], + }), + ) + }) + }) + + describe('unknown recipient user', () => { + it('throws an error', async () => { + jest.clearAllMocks() + 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), + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'voteForSendCoins with unknown userReceiverIdentifier in the community=', + ), + ], + }), + ) + }) + }) + + describe('valid X-Com-TX voted', () => { + it('throws an error', async () => { + jest.clearAllMocks() + 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), + }, + }), + ).toEqual( + expect.objectContaining({ + data: { + voteForSendCoins: 'recipUser-FirstName recipUser-LastName', + }, + }), + ) + }) + }) + }) +}) diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index ba23ae530..4af6c005b 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -15,7 +15,7 @@ import { fullName } from '@/graphql/util/fullName' @Resolver() // eslint-disable-next-line @typescript-eslint/no-unused-vars export class SendCoinsResolver { - @Mutation(() => Boolean) + @Mutation(() => String) async voteForSendCoins( @Args() { @@ -31,30 +31,31 @@ export class SendCoinsResolver { ): Promise { logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`) let result: string | null = null + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: communityReceiverIdentifier, + }) + if (!homeCom) { + throw new LogError( + `voteForSendCoins with wrong communityReceiverIdentifier`, + communityReceiverIdentifier, + ) + } + // second check if receiver user exists in this community + const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) + if (!receiverUser) { + throw new LogError( + `voteForSendCoins with unknown userReceiverIdentifier in the community=`, + homeCom.name, + ) + } try { - // first check if receiver community is correct - const homeCom = await DbCommunity.findOneBy({ - communityUuid: communityReceiverIdentifier, - }) - if (!homeCom) { - throw new LogError( - `voteForSendCoins with wrong communityReceiverIdentifier`, - communityReceiverIdentifier, - ) - } - // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) - if (!receiverUser) { - throw new LogError( - `voteForSendCoins with unknown userReceiverIdentifier in the community=`, - homeCom.name, - ) - } - const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, creationDate) + const txDate = new Date(creationDate) + const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, txDate) const pendingTx = DbPendingTransaction.create() pendingTx.amount = amount pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0) - pendingTx.balanceDate = creationDate + pendingTx.balanceDate = txDate pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null pendingTx.linkedUserCommunityUuid = communitySenderIdentifier @@ -64,6 +65,7 @@ export class SendCoinsResolver { 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.userName = fullName(receiverUser.firstName, receiverUser.lastName) diff --git a/federation/test/helpers.test.ts b/federation/test/helpers.test.ts new file mode 100644 index 000000000..69d8f3fa4 --- /dev/null +++ b/federation/test/helpers.test.ts @@ -0,0 +1,7 @@ +import { contributionDateFormatter } from '@test/helpers' + +describe('contributionDateFormatter', () => { + it('formats the date correctly', () => { + expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024') + }) +}) diff --git a/federation/test/helpers.ts b/federation/test/helpers.ts new file mode 100644 index 000000000..542906f49 --- /dev/null +++ b/federation/test/helpers.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* 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 { logger } from './testSetup' + +export const headerPushMock = jest.fn((t) => { + context.token = t.value +}) + +const context = { + token: '', + setHeaders: { + push: headerPushMock, + forEach: jest.fn(), + }, + clientTimezoneOffset: 0, +} + +export const cleanDB = async () => { + // this only works as long we do not have foreign key constraints + for (const entity of entities) { + await resetEntity(entity) + } +} + +export const testEnvironment = async (testLogger = logger) => { + const server = await createServer(testLogger) // context, testLogger, testI18n) + const con = server.con + const testClient = createTestClient(server.apollo) + const mutate = testClient.mutate + const query = testClient.query + return { mutate, query, con } +} + +export const resetEntity = async (entity: any) => { + const items = await entity.find({ withDeleted: true }) + if (items.length > 0) { + const ids = items.map((e: any) => e.id) + await entity.delete(ids) + } +} + +export const resetToken = () => { + context.token = '' +} + +// format date string as it comes from the frontend for the contribution date +export const contributionDateFormatter = (date: Date): string => { + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}` +} + +export const setClientTimezoneOffset = (offset: number): void => { + context.clientTimezoneOffset = offset +} From 83ebd1f38f615f6cd1323f56e4ad05c213af4a83 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 5 Sep 2023 16:54:00 +0200 Subject: [PATCH 11/15] reduce backend-coverage from 89 to 87 temporary till X-Com-SendCoins is complete --- backend/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index c9fbd6e81..838bad6a5 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: 89, + lines: 87, }, }, setupFiles: ['/test/testSetup.ts'], From 4b73be8721beaabd050b79d96addda6844dd796e Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 5 Sep 2023 17:00:42 +0200 Subject: [PATCH 12/15] reduce coverage again to 86 --- backend/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index 838bad6a5..1529fad55 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: 87, + lines: 86, }, }, setupFiles: ['/test/testSetup.ts'], From 589e4f4878b4e66de5ee68a5c23268a0b32e0212 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 5 Sep 2023 18:38:53 +0200 Subject: [PATCH 13/15] change creationDate in sendCoinArgs to String --- backend/src/federation/client/1_0/model/SendCoinsArgs.ts | 4 ++-- backend/src/federation/client/1_0/query/voteForSendCoins.ts | 2 +- backend/src/graphql/resolver/util/processXComSendCoins.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts index 3d15c04b1..545aab822 100644 --- a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts +++ b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts @@ -9,8 +9,8 @@ export class SendCoinsArgs { @Field(() => String) userReceiverIdentifier: string - @Field(() => Date) - creationDate: Date + @Field(() => String) + creationDate: string @Field(() => Decimal) amount: Decimal diff --git a/backend/src/federation/client/1_0/query/voteForSendCoins.ts b/backend/src/federation/client/1_0/query/voteForSendCoins.ts index 9cdae73f3..f0f75198f 100644 --- a/backend/src/federation/client/1_0/query/voteForSendCoins.ts +++ b/backend/src/federation/client/1_0/query/voteForSendCoins.ts @@ -4,7 +4,7 @@ export const voteForSendCoins = gql` mutation ( $communityReceiverIdentifier: String! $userReceiverIdentifier: String! - $creationDate: Date! + $creationDate: String! $amount: Decimal! $memo: String! $communitySenderIdentifier: String! diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 1cd854c60..2b3d650ee 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -42,7 +42,7 @@ export async function processXComSendCoins( ? receiverCom.communityUuid : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID args.userReceiverIdentifier = recipient.gradidoID - args.creationDate = creationDate + args.creationDate = creationDate.toISOString() args.amount = amount args.memo = memo args.communitySenderIdentifier = senderCom.communityUuid From d1f36a682ab9457c59f75314c1b5a60cc3891b13 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Wed, 6 Sep 2023 01:13:30 +0200 Subject: [PATCH 14/15] rework PR-comment --- .../src/graphql/api/1_0/resolver/SendCoinsResolver.ts | 6 +++--- ...lateRecepientBalance.ts => calculateRecipientBalance.ts} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename federation/src/graphql/util/{calculateRecepientBalance.ts => calculateRecipientBalance.ts} (92%) diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 4af6c005b..8916b5f10 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Args, Mutation, Query, Resolver } from 'type-graphql' +import { 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' @@ -8,7 +8,7 @@ import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' import { PendingTransactionState } from '../enum/PendingTransactionState' import { TransactionTypeId } from '../enum/TransactionTypeId' -import { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance' +import { calculateRecipientBalance } from '@/graphql/util/calculateRecipientBalance' import Decimal from 'decimal.js-light' import { fullName } from '@/graphql/util/fullName' @@ -51,7 +51,7 @@ export class SendCoinsResolver { } try { const txDate = new Date(creationDate) - const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, txDate) + const receiveBalance = await calculateRecipientBalance(receiverUser.id, amount, txDate) const pendingTx = DbPendingTransaction.create() pendingTx.amount = amount pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0) diff --git a/federation/src/graphql/util/calculateRecepientBalance.ts b/federation/src/graphql/util/calculateRecipientBalance.ts similarity index 92% rename from federation/src/graphql/util/calculateRecepientBalance.ts rename to federation/src/graphql/util/calculateRecipientBalance.ts index 56914afba..2a9c2aa1c 100644 --- a/federation/src/graphql/util/calculateRecepientBalance.ts +++ b/federation/src/graphql/util/calculateRecipientBalance.ts @@ -4,7 +4,7 @@ import { getLastTransaction } from './getLastTransaction' import { calculateDecay } from './decay' import { Decay } from '../api/1_0/model/Decay' -export async function calculateRecepientBalance( +export async function calculateRecipientBalance( userId: number, amount: Decimal, time: Date, From ae43155ca7870aed53e873457f06e21677c28bba Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Thu, 7 Sep 2023 00:19:17 +0200 Subject: [PATCH 15/15] add tests for revertSendCoins --- .../client/1_0/query/revertSendCoins.ts | 2 +- .../1_0/resolver/SendCoinsResolver.test.ts | 163 ++++++++++++++++++ .../api/1_0/resolver/SendCoinsResolver.ts | 47 ++--- 3 files changed, 190 insertions(+), 22 deletions(-) diff --git a/backend/src/federation/client/1_0/query/revertSendCoins.ts b/backend/src/federation/client/1_0/query/revertSendCoins.ts index 63c3cff63..881107cb4 100644 --- a/backend/src/federation/client/1_0/query/revertSendCoins.ts +++ b/backend/src/federation/client/1_0/query/revertSendCoins.ts @@ -4,7 +4,7 @@ export const revertSendCoins = gql` mutation ( $communityReceiverIdentifier: String! $userReceiverIdentifier: String! - $creationDate: Date! + $creationDate: String! $amount: Decimal! $memo: String! $communitySenderIdentifier: String! 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 a9ea7e068..7b580b240 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts @@ -65,6 +65,29 @@ describe('SendCoinsResolver', () => { ) } ` + 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 + ) + } +` describe('voteForSendCoins', () => { let homeCom: DbCommunity @@ -190,4 +213,144 @@ 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) + + 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), + }, + }) + }) + + describe('unknown recipient community', () => { + it('throws an error', async () => { + jest.clearAllMocks() + 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), + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('revertSendCoins with wrong communityReceiverIdentifier')], + }), + ) + }) + }) + + describe('unknown recipient user', () => { + it('throws an error', async () => { + jest.clearAllMocks() + 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), + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'revertSendCoins with unknown userReceiverIdentifier in the community=', + ), + ], + }), + ) + }) + }) + + describe('valid X-Com-TX reverted', () => { + it('throws an error', async () => { + jest.clearAllMocks() + 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), + }, + }), + ).toEqual( + expect.objectContaining({ + data: { + revertSendCoins: true, + }, + }), + ) + }) + }) + }) }) diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 7058c4b49..11222a3ce 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -58,6 +58,7 @@ export class SendCoinsResolver { 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 @@ -94,36 +95,36 @@ export class SendCoinsResolver { }: SendCoinsArgs, ): Promise { logger.debug(`revertSendCoins() via apiVersion=1_0 ...`) + // first check if receiver community is correct + const homeCom = await DbCommunity.findOneBy({ + communityUuid: communityReceiverIdentifier, + }) + if (!homeCom) { + throw new LogError( + `revertSendCoins with wrong communityReceiverIdentifier`, + communityReceiverIdentifier, + ) + } + // second check if receiver user exists in this community + const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) + if (!receiverUser) { + throw new LogError( + `revertSendCoins with unknown userReceiverIdentifier in the community=`, + homeCom.name, + ) + } try { - // first check if receiver community is correct - const homeCom = await DbCommunity.findOneBy({ - communityUuid: communityReceiverIdentifier, - }) - if (!homeCom) { - throw new LogError( - `revertSendCoins with wrong communityReceiverIdentifier`, - communityReceiverIdentifier, - ) - } - // second check if receiver user exists in this community - const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier }) - if (!receiverUser) { - throw new LogError( - `revertSendCoins with unknown userReceiverIdentifier in the community=`, - homeCom.name, - ) - } const pendingTx = await DbPendingTransaction.findOneBy({ userCommunityUuid: communityReceiverIdentifier, userGradidoID: userReceiverIdentifier, state: PendingTransactionState.NEW, typeId: TransactionTypeId.RECEIVE, - balanceDate: creationDate, + balanceDate: new Date(creationDate), linkedUserCommunityUuid: communitySenderIdentifier, linkedUserGradidoID: userSenderIdentifier, }) logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx) - if (pendingTx && pendingTx.amount === amount) { + if (pendingTx && pendingTx.amount.toString() === amount.toString()) { logger.debug('XCom: revertSendCoins matching pendingTX for remove...') try { await pendingTx.remove() @@ -132,7 +133,11 @@ export class SendCoinsResolver { throw new LogError('Error in revertSendCoins on removing pendingTx of receiver: ', err) } } else { - logger.debug('XCom: revertSendCoins NOT matching pendingTX for remove...') + logger.debug( + 'XCom: revertSendCoins NOT matching pendingTX for remove:', + pendingTx?.amount, + amount, + ) throw new LogError( `Can't find in revertSendCoins the pending receiver TX for args=`, communityReceiverIdentifier,