From aaf02910315947c2dde0f8842aae8e6b4f1772b7 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Fri, 25 Aug 2023 18:24:09 +0200 Subject: [PATCH 1/3] 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 2/3] 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 ae43155ca7870aed53e873457f06e21677c28bba Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Thu, 7 Sep 2023 00:19:17 +0200 Subject: [PATCH 3/3] 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,