diff --git a/backend/.env.org b/backend/.env.org index a0082aab1..2267fdc50 100644 --- a/backend/.env.org +++ b/backend/.env.org @@ -63,7 +63,6 @@ LOG_LEVEL=INFO # Federation FEDERATION_VALIDATE_COMMUNITY_TIMER=60000 -FEDERATION_XCOM_SENDCOINS_ENABLED=false # GMS # GMS_ACTIVE=true diff --git a/backend/.env.template b/backend/.env.template index 6c3867978..9b0bc7df2 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -63,7 +63,6 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET # Federation FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER -FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED # GMS GMS_ACTIVE=$GMS_ACTIVE diff --git a/backend/src/apis/dltConnector/model/TransactionDraft.ts b/backend/src/apis/dltConnector/model/TransactionDraft.ts index 1342a3f8f..86f46a43b 100755 --- a/backend/src/apis/dltConnector/model/TransactionDraft.ts +++ b/backend/src/apis/dltConnector/model/TransactionDraft.ts @@ -59,21 +59,20 @@ export class TransactionDraft { static createTransfer(sendingUser: DbUser, receivingUser: DbUser, amount: string, memo: string, createdAt: Date): TransactionDraft | null { if (!sendingUser.community || !receivingUser.community) { - logger.warn(`missing community for user ${sendingUser.id} and/or ${receivingUser.id}`) - return null + throw new Error(`missing community for user ${sendingUser.id} and/or ${receivingUser.id}`) } - if (sendingUser.community.hieroTopicId && receivingUser.community.hieroTopicId) { - const draft = new TransactionDraft() - draft.user = new AccountIdentifier(sendingUser.community.hieroTopicId, new CommunityAccountIdentifier(sendingUser.gradidoID)) - draft.linkedUser = new AccountIdentifier(receivingUser.community.hieroTopicId, new CommunityAccountIdentifier(receivingUser.gradidoID)) - draft.type = TransactionType.GRADIDO_TRANSFER - draft.createdAt = createdAt.toISOString() - draft.amount = amount - draft.memo = memo - return draft - } else { - logger.warn(`missing topicId for community ${community.id}`) + const senderUserTopic = sendingUser.community.hieroTopicId + const receiverUserTopic = receivingUser.community.hieroTopicId + if (!senderUserTopic || !receiverUserTopic) { + throw new Error(`missing topicId for community ${sendingUser.community.id} and/or ${receivingUser.community.id}`) } - return null + const draft = new TransactionDraft() + draft.user = new AccountIdentifier(senderUserTopic, new CommunityAccountIdentifier(sendingUser.gradidoID)) + draft.linkedUser = new AccountIdentifier(receiverUserTopic, new CommunityAccountIdentifier(receivingUser.gradidoID)) + draft.type = TransactionType.GRADIDO_TRANSFER + draft.createdAt = createdAt.toISOString() + draft.amount = amount + draft.memo = memo + return draft } } \ No newline at end of file diff --git a/backend/src/apis/gms/GmsClient.ts b/backend/src/apis/gms/GmsClient.ts index bb4fce2e7..7ed1df89d 100644 --- a/backend/src/apis/gms/GmsClient.ts +++ b/backend/src/apis/gms/GmsClient.ts @@ -4,7 +4,7 @@ import { httpAgent, httpsAgent } from '@/apis/ConnectionAgents' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from 'core' import { getLogger } from 'log4js' import { GmsUser } from './model/GmsUser' diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 9d61c2da4..aa0a7dbf2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -99,19 +99,9 @@ const webhook = { process.env.APP_SECRET = server.JWT_SECRET const federation = { - FEDERATION_BACKEND_SEND_ON_API: process.env.FEDERATION_BACKEND_SEND_ON_API ?? '1_0', - // ?? operator don't work here as expected FEDERATION_VALIDATE_COMMUNITY_TIMER: Number( process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER ?? 60000, ), - FEDERATION_XCOM_SENDCOINS_ENABLED: - process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' || false, - // 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: parseInt( - process.env.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS ?? '3', - ), } const gms = { diff --git a/backend/src/config/schema.ts b/backend/src/config/schema.ts index f305a7488..144608992 100644 --- a/backend/src/config/schema.ts +++ b/backend/src/config/schema.ts @@ -209,39 +209,13 @@ export const schema = Joi.object({ .description('Time in minutes before a new code can be requested') .required(), - FEDERATION_BACKEND_SEND_ON_API: Joi.string() - .pattern(/^\d+_\d+$/) - .default('1_0') - .description('API Version of sending requests to another communities, e.g., "1_0"') - .required(), - - FEDERATION_VALIDATE_COMMUNITY_TIMER: Joi.number() + FEDERATION_VALIDATE_COMMUNITY_TIMER: Joi.number() .integer() .min(1000) .default(60000) .description('Timer interval in milliseconds for community validation') .required(), - FEDERATION_XCOM_SENDCOINS_ENABLED: Joi.boolean() - .default(false) - .description('Enable or disable the federation send coins feature') - .optional(), - - FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID: Joi.string() - .uuid() - .default('56a55482-909e-46a4-bfa2-cd025e894ebc') - .description( - 'UUID of the receiver community for federation cross-community transactions if the receiver is unknown', - ) - .required(), - - FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS: Joi.number() - .integer() - .min(0) - .default(3) - .description('Maximum number of retries for reverting send coins transactions') - .required(), - GMS_CREATE_USER_THROW_ERRORS: Joi.boolean() .default(false) .when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() }) diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 642f87387..aac726002 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,7 +1,7 @@ import { Decimal } from 'decimal.js-light' import { CONFIG } from '@/config' -import { decimalSeparatorByLanguage } from '@/util/utilities' +import { decimalSeparatorByLanguage } from 'core' import { sendEmailTranslated } from './sendEmailTranslated' diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index d8946eb47..ad2d91469 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -1,10 +1,10 @@ import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database' import { validate as validateUUID, version as versionUUID } from 'uuid' import { randombytes_random } from 'sodium-native' -import { CONFIG } from '@/config' +import { CONFIG as CONFIG_CORE } from 'core' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from 'core' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared' @@ -27,7 +27,7 @@ export async function startCommunityAuthentication( methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!)) const homeFedComA = await DbFederatedCommunity.findOneByOrFail({ foreign: false, - apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API, + apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, }) methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA)) const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) diff --git a/backend/src/federation/client/1_0/AuthenticationClient.ts b/backend/src/federation/client/1_0/AuthenticationClient.ts index b504c0030..5d897093e 100644 --- a/backend/src/federation/client/1_0/AuthenticationClient.ts +++ b/backend/src/federation/client/1_0/AuthenticationClient.ts @@ -1,11 +1,11 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from 'core' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { EncryptedTransferArgs } from 'core' import { getLogger } from 'log4js' -import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs' import { openConnection } from './query/openConnection' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.AuthenticationClient`) diff --git a/backend/src/federation/client/1_0/DisbursementClient.ts b/backend/src/federation/client/1_0/DisbursementClient.ts new file mode 100644 index 000000000..8ca859070 --- /dev/null +++ b/backend/src/federation/client/1_0/DisbursementClient.ts @@ -0,0 +1,47 @@ +import { FederatedCommunity as DbFederatedCommunity } from 'database' +import { GraphQLClient } from 'graphql-request' + +import { ensureUrlEndsWithSlash } from 'core' +import { getLogger } from 'log4js' + +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { EncryptedTransferArgs } from 'core' +import { processDisburseJwtOnSenderCommunity as processDisburseJwtOnSenderCommunityQuery } from './query/processDisburseJwtOnSenderCommunity' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.DisbursementClient`) + +export class DisbursementClient { + dbCom: DbFederatedCommunity + endpoint: string + client: GraphQLClient + + constructor(dbCom: DbFederatedCommunity) { + this.dbCom = dbCom + this.endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion).concat('/') + this.client = new GraphQLClient(this.endpoint, { + method: 'POST', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } + + async sendDisburseJwtToSenderCommunity(args: EncryptedTransferArgs): Promise { + logger.debug('sendDisburseJwtToSenderCommunity against endpoint=', this.endpoint) + try { + const { data } = await this.client.rawRequest<{ processDisburseJwtOnSenderCommunity: string }>(processDisburseJwtOnSenderCommunityQuery, { args }) + const response = data?.processDisburseJwtOnSenderCommunity + if (response) { + logger.debug('received response:', response) + return response + } + } catch (err) { + const errmsg = `sendDisburseJwtToSenderCommunity failed for endpoint=${this.endpoint}, err=${err}` + logger.error(errmsg) + throw new Error(errmsg) + } + return null + } + +} diff --git a/backend/src/federation/client/1_0/FederationClient.ts b/backend/src/federation/client/1_0/FederationClient.ts index df1140f9c..c437ad085 100644 --- a/backend/src/federation/client/1_0/FederationClient.ts +++ b/backend/src/federation/client/1_0/FederationClient.ts @@ -4,7 +4,7 @@ import { GraphQLClient } from 'graphql-request' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getPublicCommunityInfo } from '@/federation/client/1_0/query/getPublicCommunityInfo' import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from 'core' import { getLogger } from 'log4js' import { PublicCommunityInfoLoggingView } from './logging/PublicCommunityInfoLogging.view' diff --git a/backend/src/federation/client/1_0/query/processDisburseJwtOnSenderCommunity.ts b/backend/src/federation/client/1_0/query/processDisburseJwtOnSenderCommunity.ts new file mode 100644 index 000000000..e2d0f49d9 --- /dev/null +++ b/backend/src/federation/client/1_0/query/processDisburseJwtOnSenderCommunity.ts @@ -0,0 +1,7 @@ +import { gql } from 'graphql-request' + +export const processDisburseJwtOnSenderCommunity = gql` + mutation ($args: EncryptedTransferArgs!) { + processDisburseJwtOnSenderCommunity(data: $args) + } +` diff --git a/backend/src/federation/client/1_1/DisbursementClient.ts b/backend/src/federation/client/1_1/DisbursementClient.ts new file mode 100644 index 000000000..411befe19 --- /dev/null +++ b/backend/src/federation/client/1_1/DisbursementClient.ts @@ -0,0 +1,3 @@ +import { DisbursementClient as V1_0_DisbursementClient } from '@/federation/client/1_0/DisbursementClient' + +export class DisbursementClient extends V1_0_DisbursementClient {} diff --git a/backend/src/federation/client/1_1/SendCoinsClient.ts b/backend/src/federation/client/1_1/SendCoinsClient.ts deleted file mode 100644 index cc804d0dc..000000000 --- a/backend/src/federation/client/1_1/SendCoinsClient.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' - -export class SendCoinsClient extends V1_0_SendCoinsClient {} diff --git a/backend/src/federation/client/AuthenticationClientFactory.ts b/backend/src/federation/client/AuthenticationClientFactory.ts index 0ae0cd1b8..344753a48 100644 --- a/backend/src/federation/client/AuthenticationClientFactory.ts +++ b/backend/src/federation/client/AuthenticationClientFactory.ts @@ -3,7 +3,7 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' import { AuthenticationClient as V1_1_AuthenticationClient } from '@/federation/client/1_1/AuthenticationClient' -import { ApiVersionType } from '@/federation/enum/apiVersionType' +import { ApiVersionType } from 'core' type AuthenticationClient = V1_0_AuthenticationClient | V1_1_AuthenticationClient diff --git a/backend/src/federation/client/DisbursementClientFactory.ts b/backend/src/federation/client/DisbursementClientFactory.ts new file mode 100644 index 000000000..01c911e92 --- /dev/null +++ b/backend/src/federation/client/DisbursementClientFactory.ts @@ -0,0 +1,58 @@ +import { FederatedCommunity as DbFederatedCommunity } from 'database' + +import { DisbursementClient as V1_0_DisbursementClient } from '@/federation/client/1_0/DisbursementClient' +import { DisbursementClient as V1_1_DisbursementClient } from '@/federation/client/1_1/DisbursementClient' +import { ApiVersionType } from 'core' + +type DisbursementClient = V1_0_DisbursementClient | V1_1_DisbursementClient + +interface DisbursementClientInstance { + id: number + + client: DisbursementClient +} + +export class DisbursementClientFactory { + private static instanceArray: DisbursementClientInstance[] = [] + + /** + * The Singleton's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + + private constructor() {} + + private static createDisbursementClient = (dbCom: DbFederatedCommunity) => { + switch (dbCom.apiVersion) { + case ApiVersionType.V1_0: + return new V1_0_DisbursementClient(dbCom) + case ApiVersionType.V1_1: + return new V1_1_DisbursementClient(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): DisbursementClient | null { + const instance = DisbursementClientFactory.instanceArray.find( + (instance) => instance.id === dbCom.id, + ) + if (instance) { + return instance.client + } + const client = DisbursementClientFactory.createDisbursementClient(dbCom) + if (client) { + DisbursementClientFactory.instanceArray.push({ + id: dbCom.id, + client, + } as DisbursementClientInstance) + } + return client + } +} diff --git a/backend/src/federation/client/FederationClientFactory.ts b/backend/src/federation/client/FederationClientFactory.ts index 3660934f4..926c3c180 100644 --- a/backend/src/federation/client/FederationClientFactory.ts +++ b/backend/src/federation/client/FederationClientFactory.ts @@ -1,10 +1,8 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' - import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient' -import { ApiVersionType } from '@/federation/enum/apiVersionType' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ApiVersionType, ensureUrlEndsWithSlash } from 'core' type FederationClient = V1_0_FederationClient | V1_1_FederationClient diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 478261386..36c9985c9 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -15,7 +15,7 @@ import { createKeyPair } from 'shared' import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' -import { ApiVersionType } from './enum/apiVersionType' +import { ApiVersionType } from 'core' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`) diff --git a/backend/src/graphql/model/FederatedCommunity.ts b/backend/src/graphql/model/FederatedCommunity.ts index 7029013eb..cee430dba 100644 --- a/backend/src/graphql/model/FederatedCommunity.ts +++ b/backend/src/graphql/model/FederatedCommunity.ts @@ -1,7 +1,7 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { Field, Int, ObjectType } from 'type-graphql' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from 'core' @ObjectType() export class FederatedCommunity { diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index dc0849a53..0560cef54 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -2,9 +2,8 @@ import { Transaction as dbTransaction } from 'database' import { Decimal } from 'decimal.js-light' import { Field, Int, ObjectType } from 'type-graphql' -import { TransactionTypeId } from '@enum/TransactionTypeId' +import { Decay, TransactionTypeId } from 'core' -import { Decay } from './Decay' import { User } from './User' @ObjectType() diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index f7388c027..0e88382ff 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -7,14 +7,14 @@ import { Balance } from '@model/Balance' import { RIGHTS } from '@/auth/RIGHTS' import { BalanceLoggingView } from '@/logging/BalanceLogging.view' -import { DecayLoggingView } from '@/logging/DecayLogging.view' import { Context, getUser } from '@/server/context' +import { DecayLoggingView } from 'core' import { calculateDecay } from 'shared' -import { getLogger } from 'log4js' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLastTransaction } from 'database' +import { getLogger } from 'log4js' import { GdtResolver } from './GdtResolver' -import { getLastTransaction } from './util/getLastTransaction' import { transactionLinkSummary } from './util/transactionLinkSummary' @Resolver() diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 14dfa614f..6dee20592 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -50,7 +50,7 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { stephenHawking } from '@/seeds/users/stephen-hawking' -import { getFirstDayOfPreviousNMonth } from '@/util/utilities' +import { getFirstDayOfPreviousNMonth } from 'core' import { getLogger } from 'config-schema/test/testSetup' import { getLogger as originalGetLogger } from 'log4js' diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 34ea8b1da..7c31209e0 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -17,11 +17,11 @@ import { Paginated } from '@arg/Paginated' import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs' import { ContributionStatus } from '@enum/ContributionStatus' import { ContributionType } from '@enum/ContributionType' -import { TransactionTypeId } from '@enum/TransactionTypeId' import { AdminUpdateContribution } from '@model/AdminUpdateContribution' import { Contribution, ContributionListResult } from '@model/Contribution' import { OpenCreation } from '@model/OpenCreation' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' +import { TransactionTypeId } from 'core' import { RIGHTS } from '@/auth/RIGHTS' import { @@ -43,9 +43,9 @@ import { import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context' import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' -import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { TRANSACTIONS_LOCK } from 'database' +import { fullName } from 'core' import { calculateDecay, Decay } from 'shared' -import { fullName } from '@/util/utilities' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { ContributionMessageType } from '@enum/ContributionMessageType' @@ -59,8 +59,7 @@ import { import { getOpenCreations, getUserCreation, validateContribution } from './util/creations' import { extractGraphQLFields } from './util/extractGraphQLFields' import { findContributions } from './util/findContributions' -import { getLastTransaction } from './util/getLastTransaction' -import { InterruptiveSleepManager, TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/util/InterruptiveSleepManager' +import { getLastTransaction } from 'database' import { contributionTransaction } from '@/apis/dltConnector' const db = AppDatabase.getInstance() diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 575a522b2..d7c2fc713 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -32,7 +32,7 @@ import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { transactionLinks } from '@/seeds/transactionLink/index' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' -import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { TRANSACTIONS_LOCK } from 'database' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getLogger } from 'config-schema/test/testSetup' @@ -43,7 +43,7 @@ const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`) jest.mock('@/password/EncryptorUtils') // mock semaphore to allow use fake timers -jest.mock('@/util/TRANSACTIONS_LOCK') +jest.mock('database/src/util/TRANSACTIONS_LOCK') TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn()) let mutate: ApolloServerTestClient['mutate'] diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index d8fab512a..624038c20 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -6,22 +6,20 @@ import { TransactionLinkFilters } from '@arg/TransactionLinkFilters' import { ContributionCycleType } from '@enum/ContributionCycleType' import { ContributionStatus } from '@enum/ContributionStatus' import { ContributionType } from '@enum/ContributionType' -import { TransactionTypeId } from '@enum/TransactionTypeId' import { Community } from '@model/Community' import { ContributionLink } from '@model/ContributionLink' -import { Decay } from '@model/Decay' import { RedeemJwtLink } from '@model/RedeemJwtLink' import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink' import { User } from '@model/User' import { QueryLinkResult } from '@union/QueryLinkResult' +import { Decay, interpretEncryptedTransferArgs, TransactionTypeId } from 'core' import { - AppDatabase, - Contribution as DbContribution, - ContributionLink as DbContributionLink, - Transaction as DbTransaction, + AppDatabase, Community as DbCommunity, Contribution as DbContribution, + ContributionLink as DbContributionLink, FederatedCommunity as DbFederatedCommunity, Transaction as DbTransaction, TransactionLink as DbTransactionLink, User as DbUser, - getHomeCommunity, + findTransactionLinkByCode, + getHomeCommunity } from 'database' import { Decimal } from 'decimal.js-light' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' @@ -39,24 +37,29 @@ import { InterruptiveSleepManager, TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, } from '@/util/InterruptiveSleepManager' -import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' -import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK' -import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' -import { calculateDecay, decode, DisburseJwtPayloadType, encode, RedeemJwtPayloadType, verify } from 'shared' +import { fullName } from 'core' +import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database' +import { calculateDecay, decode, DisburseJwtPayloadType, encode, encryptAndSign, EncryptedJWEJwtPayloadType, RedeemJwtPayloadType, verify } from 'shared' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { DisbursementClient as V1_0_DisbursementClient } from '@/federation/client/1_0/DisbursementClient' +import { DisbursementClientFactory } from '@/federation/client/DisbursementClientFactory' +import { EncryptedTransferArgs } from 'core' +import { getLastTransaction } from 'database' import { getLogger, Logger } from 'log4js' +import { randombytes_random } from 'sodium-native' import { executeTransaction } from './TransactionResolver' import { getAuthenticatedCommunities, + getCommunityByPublicKey, getCommunityByUuid, } from './util/communities' import { getUserCreation, validateContribution } from './util/creations' -import { getLastTransaction } from './util/getLastTransaction' import { transactionLinkList } from './util/transactionLinkList' +import { SignedTransferPayloadType } from 'shared' -const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver`) +const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver.${method}`) // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { @@ -151,9 +154,9 @@ export class TransactionLinkResolver { @Authorized([RIGHTS.QUERY_TRANSACTION_LINK]) @Query(() => QueryLinkResult) async queryTransactionLink(@Arg('code') code: string): Promise { - const logger = createLogger() - logger.addContext('code', code.substring(0, 6)) - logger.debug('TransactionLinkResolver.queryTransactionLink...') + const methodLogger = createLogger('queryTransactionLink') + methodLogger.addContext('handshakeID', randombytes_random().toString()) + methodLogger.debug('queryTransactionLink...') if (code.match(/^CL-/)) { const contributionLink = await DbContributionLink.findOneOrFail({ where: { code: code.replace('CL-', '') }, @@ -164,17 +167,14 @@ export class TransactionLinkResolver { let txLinkFound = false let dbTransactionLink!: DbTransactionLink try { - dbTransactionLink = await DbTransactionLink.findOneOrFail({ - where: { code }, - withDeleted: true, - }) + dbTransactionLink = await findTransactionLinkByCode(code) txLinkFound = true } catch (_err) { txLinkFound = false } // normal redeem code if (txLinkFound) { - logger.debug( + methodLogger.debug( 'TransactionLinkResolver.queryTransactionLink... normal redeem code found=', txLinkFound, ) @@ -189,7 +189,7 @@ export class TransactionLinkResolver { return new TransactionLink(dbTransactionLink, new User(user), redeemedBy, communities) } else { // redeem jwt-token - return await this.queryRedeemJwtLink(code, logger) + return await this.queryRedeemJwtLink(code, methodLogger) } } } @@ -200,8 +200,8 @@ export class TransactionLinkResolver { @Arg('code', () => String) code: string, @Ctx() context: Context, ): Promise { - const logger = createLogger() - logger.addContext('code', code.substring(0, 6)) + const methodLogger = createLogger('redeemTransactionLink') + methodLogger.addContext('code', code.substring(0, 6)) const clientTimezoneOffset = getClientTimezoneOffset(context) // const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } }) const user = getUser(context) @@ -209,7 +209,7 @@ export class TransactionLinkResolver { // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() try { - logger.info('redeem contribution link...') + methodLogger.info('redeem contribution link...') const now = new Date() const queryRunner = db.getDataSource().createQueryRunner() await queryRunner.connect() @@ -224,7 +224,7 @@ export class TransactionLinkResolver { if (!contributionLink) { throw new LogError('No contribution link found to given code', code) } - logger.info('...contribution link found with id', contributionLink.id) + methodLogger.info('...contribution link found with id', contributionLink.id) if (new Date(contributionLink.validFrom).getTime() > now.getTime()) { throw new LogError('Contribution link is not valid yet', contributionLink.validFrom) } @@ -281,7 +281,7 @@ export class TransactionLinkResolver { } const creations = await getUserCreation(user.id, clientTimezoneOffset) - logger.info('open creations', creations) + methodLogger.info('open creations', creations) validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset) const contribution = new DbContribution() contribution.userId = user.id @@ -387,7 +387,7 @@ export class TransactionLinkResolver { transactionLink.memo, linkedUser, user, - logger, + methodLogger, transactionLink, ) await EVENT_TRANSACTION_LINK_REDEEM( @@ -419,9 +419,9 @@ export class TransactionLinkResolver { @Arg('alias', { nullable: true }) alias?: string, @Arg('validUntil', { nullable: true }) validUntil?: string, ): Promise { - const logger = createLogger() - logger.addContext('code', code.substring(0, 6)) - logger.debug('TransactionLinkResolver.queryRedeemJwt... args=', { + const methodLogger = createLogger('createRedeemJwt') + methodLogger.addContext('code', code.substring(0, 6)) + methodLogger.debug('args=', { gradidoId, senderCommunityUuid, senderCommunityName, @@ -433,27 +433,65 @@ export class TransactionLinkResolver { alias, validUntil, }) + try { + const redeemJwtPayloadType = new RedeemJwtPayloadType( + senderCommunityUuid, + gradidoId, + alias ?? firstName ?? '', + code, + amount, + memo, + validUntil ?? '', + ) + // encode/sign the jwt with the private key of the sender/home community + const senderCom = await getCommunityByUuid(senderCommunityUuid) + if (!senderCom) { + throw new LogError('Sender community not found') + } + if (!senderCom.privateJwtKey) { + throw new LogError('Sender community privateJwtKey is not set') + } + const recipientCom = await getCommunityByUuid(recipientCommunityUuid) + if (!recipientCom) { + throw new LogError('Recipient community not found') + } + if (!recipientCom.publicJwtKey) { + throw new LogError('Recipient community publicJwtKey is not set') + } + const redeemJwt = await encryptAndSign(redeemJwtPayloadType, senderCom.privateJwtKey!, recipientCom.publicJwtKey!) + if (!redeemJwt) { + throw new LogError('Redeem JWT was not created successfully') + } + // prepare the args for the client invocation + const args = new EncryptedTransferArgs() + args.publicKey = senderCom.publicKey.toString('hex') + args.jwt = redeemJwt + args.handshakeID = randombytes_random().toString() + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('successfully created RedeemJWT-Response with args:', args) + } + const signedTransferPayload = new SignedTransferPayloadType( + args.publicKey, + args.jwt, + args.handshakeID, + ) + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('successfully created RedeemJWT-Response with signedTransferPayload:', signedTransferPayload) + } + const signedTransferJwt = await encode(signedTransferPayload, senderCom.privateJwtKey!) + if (!signedTransferJwt) { + throw new LogError('SignedTransfer JWT was not created successfully') + } + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('successfully created RedeemJWT-Response with signedTransferJwt:', signedTransferJwt) + } - const redeemJwtPayloadType = new RedeemJwtPayloadType( - senderCommunityUuid, - gradidoId, - alias ?? firstName ?? '', - code, - amount, - memo, - validUntil ?? '', - ) - // TODO:encode/sign the jwt normally with the private key of the sender/home community, but interims with uuid - const homeCom = await getHomeCommunity() - if (!homeCom) { - throw new LogError('Home community not found') + return signedTransferJwt + } catch (e) { + const errmsg = `Error on creating Redeem JWT: error=${e}` + methodLogger.error(errmsg) + throw new LogError(errmsg) } - if (!homeCom.communityUuid) { - throw new LogError('Home community UUID is not set') - } - const redeemJwt = await encode(redeemJwtPayloadType, homeCom.communityUuid) - // TODO: encrypt the payload with the public key of the target community - return redeemJwt } @Authorized([RIGHTS.DISBURSE_TRANSACTION_LINK]) @@ -472,40 +510,80 @@ export class TransactionLinkResolver { @Arg('validUntil', { nullable: true }) validUntil?: string, @Arg('recipientAlias', { nullable: true }) recipientAlias?: string, ): Promise { - const logger = createLogger() - logger.addContext('code', code.substring(0, 6)) - logger.debug('TransactionLinkResolver.disburseTransactionLink... args=', { - senderGradidoId, - senderCommunityUuid, - recipientCommunityUuid, - recipientCommunityName, - recipientGradidoId, - recipientFirstName, - code, - amount, - memo, - validUntil, + const handshakeID = randombytes_random().toString() + const methodLogger = createLogger(`disburseTransactionLink`) + methodLogger.addContext('handshakeID', handshakeID) + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('args=', { + senderGradidoId, + senderCommunityUuid, + recipientCommunityUuid, + recipientCommunityName, + recipientGradidoId, + recipientFirstName, + code, + amount, + memo, + validUntil, recipientAlias, - }) - const disburseJwt = await this.createDisburseJwt( - senderCommunityUuid, - senderGradidoId, - recipientCommunityUuid, - recipientCommunityName, - recipientGradidoId, - recipientFirstName, - code, - amount, - memo, - validUntil ?? '', - recipientAlias ?? '', - ) - try { - logger.debug('TransactionLinkResolver.disburseTransactionLink... disburseJwt=', disburseJwt) - // now send the disburseJwt to the sender community to invoke a x-community-tx to disbures the redeemLink - // await sendDisburseJwtToSenderCommunity(context, disburseJwt) - } catch (e) { - throw new LogError('Disburse JWT was not sent successfully', e) + }) + } + const senderCom = await getCommunityByUuid(senderCommunityUuid) + if (!senderCom) { + const errmsg = `Sender community not found with uuid=${senderCommunityUuid}` + methodLogger.error(errmsg) + throw new LogError(errmsg) + } + const senderFedCom = await DbFederatedCommunity.findOneBy({ publicKey: senderCom.publicKey }) + if (!senderFedCom) { + const errmsg = `Sender federated community not found with publicKey=${senderCom.publicKey}` + methodLogger.error(errmsg) + throw new LogError(errmsg) + } + const recipientCom = await getCommunityByUuid(recipientCommunityUuid) + if (!recipientCom) { + const errmsg = `Recipient community not found with uuid=${recipientCommunityUuid}` + methodLogger.error(errmsg) + throw new LogError(errmsg) + } + const client = DisbursementClientFactory.getInstance(senderFedCom) + if (client instanceof V1_0_DisbursementClient) { + const disburseJwtPayload = new DisburseJwtPayloadType( + handshakeID, + senderCommunityUuid, + senderGradidoId, + recipientCommunityUuid, + recipientCommunityName, + recipientGradidoId, + recipientFirstName, + code, + amount, + memo, + validUntil!, + recipientAlias!, + ) + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('disburseJwtPayload=', disburseJwtPayload) + } + const jws = await encryptAndSign(disburseJwtPayload, recipientCom.privateJwtKey!, senderCom.publicJwtKey!) + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('jws=', jws) + } + const args = new EncryptedTransferArgs() + args.publicKey = recipientCom.publicKey.toString('hex') + args.jwt = jws + args.handshakeID = handshakeID + try { + // now send the disburseJwt to the sender community to invoke a x-community-tx to disbures the redeemLink + const result = await client.sendDisburseJwtToSenderCommunity(args) + if(methodLogger.isDebugEnabled()) { + methodLogger.debug('Disburse JWT was sent successfully with result=', result) + } + } catch (e) { + const errmsg = `Disburse JWT was not sent successfully with error=${e}` + methodLogger.error(errmsg) + throw new Error(errmsg) + } } return true } @@ -546,98 +624,77 @@ export class TransactionLinkResolver { } async queryRedeemJwtLink(code: string, logger: Logger): Promise { - logger.debug('TransactionLinkResolver.queryRedeemJwtLink... redeem jwt-token found') - // decode token first to get the senderCommunityUuid as input for verify token - const decodedPayload = decode(code) - logger.debug('TransactionLinkResolver.queryRedeemJwtLink... decodedPayload=', decodedPayload) - if ( - decodedPayload != null && - decodedPayload.tokentype === RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE - ) { - const redeemJwtPayload = new RedeemJwtPayloadType( - decodedPayload.sendercommunityuuid as string, - decodedPayload.sendergradidoid as string, - decodedPayload.sendername as string, - decodedPayload.redeemcode as string, - decodedPayload.amount as string, - decodedPayload.memo as string, - decodedPayload.validuntil as string, - ) - logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... redeemJwtPayload=', - redeemJwtPayload, - ) - const senderCom = await getCommunityByUuid(redeemJwtPayload.sendercommunityuuid) + logger.debug('queryRedeemJwtLink... redeem jwt-token found') + + // decode token first to get the EncryptedTransferArgs with the senderCommunity.publicKey as input to verify token + const decodedPayload = decode(code) as SignedTransferPayloadType + logger.debug('queryRedeemJwtLink... decodedPayload=', decodedPayload) + logger.debug('switch logger-context to received token-handshakeID:' + decodedPayload.handshakeID) + logger.addContext('handshakeID', decodedPayload.handshakeID) + if(decodedPayload !== null && decodedPayload.tokentype === SignedTransferPayloadType.SIGNED_TRANSFER_TYPE) { + const signedTransferPayload = new SignedTransferPayloadType( + decodedPayload.publicKey, + decodedPayload.jwt, + decodedPayload.handshakeID) + logger.debug('queryRedeemJwtLink... signedTransferPayload=', signedTransferPayload) + const senderCom = await getCommunityByPublicKey(Buffer.from(signedTransferPayload.publicKey, 'hex')) if (!senderCom) { - throw new LogError('Sender community not found:', redeemJwtPayload.sendercommunityuuid) + const errmsg = `Sender community not found with publicKey=${signedTransferPayload.publicKey}` + logger.error(errmsg) + throw new Error(errmsg) } - logger.debug('TransactionLinkResolver.queryRedeemJwtLink... senderCom=', senderCom) - if (!senderCom.communityUuid) { - throw new LogError('Sender community UUID is not set') - } - // now with the sender community UUID the jwt token can be verified - const verifiedJwtPayload = await verify('handshakeID', code, senderCom.communityUuid) - logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... nach verify verifiedJwtPayload=', - verifiedJwtPayload, - ) + logger.debug('queryRedeemJwtLink... senderCom=', senderCom) + const jweVerifyResult = await verify(signedTransferPayload.handshakeID, signedTransferPayload.jwt, senderCom.publicJwtKey!) + logger.debug('queryRedeemJwtLink... jweVerifyResult=', jweVerifyResult) let verifiedRedeemJwtPayload: RedeemJwtPayloadType | null = null - if (verifiedJwtPayload !== null) { - if (verifiedJwtPayload.exp !== undefined) { - const expDate = new Date(verifiedJwtPayload.exp * 1000) + if (jweVerifyResult === null) { + const errmsg = `Error on verify transferred redeem token with publicKey=${signedTransferPayload.publicKey}` + logger.error(errmsg) + throw new Error(errmsg) + } else { + const encryptedTransferArgs = new EncryptedTransferArgs() + encryptedTransferArgs.publicKey = signedTransferPayload.publicKey + encryptedTransferArgs.jwt = signedTransferPayload.jwt + encryptedTransferArgs.handshakeID = signedTransferPayload.handshakeID + + verifiedRedeemJwtPayload = await interpretEncryptedTransferArgs(encryptedTransferArgs) as RedeemJwtPayloadType + if(logger.isDebugEnabled()) { + logger.debug(`queryRedeemJwtLink() ...`, verifiedRedeemJwtPayload) + } + if (!verifiedRedeemJwtPayload) { + const errmsg = `invalid authentication payload of requesting community with publicKey` + signedTransferPayload.publicKey + logger.error(errmsg) + throw new Error(errmsg) + } + if (verifiedRedeemJwtPayload.tokentype !== RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE) { + const errmsg = `Wrong tokentype in redeem JWT: type=` + verifiedRedeemJwtPayload.tokentype + ' vs expected ' + RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE + logger.error(errmsg) + throw new Error(errmsg) + } + if(senderCom?.communityUuid !== verifiedRedeemJwtPayload.sendercommunityuuid) { + const errmsg = `Mismatch of sender community UUID in redeem JWT against transfer JWT: uuid=` + senderCom.communityUuid + ' vs ' + verifiedRedeemJwtPayload.sendercommunityuuid + logger.error(errmsg) + throw new Error(errmsg) + } + if (verifiedRedeemJwtPayload.exp !== undefined) { + const expDate = new Date(verifiedRedeemJwtPayload.exp * 1000) logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... expDate, exp =', + 'queryRedeemJwtLink... expDate, exp =', expDate, - verifiedJwtPayload.exp, + verifiedRedeemJwtPayload.exp, ) if (expDate < new Date()) { - throw new LogError('Redeem JWT-Token expired! jwtPayload.exp=', expDate) + const errmsg = `Redeem JWT-Token expired! jwtPayload.exp=${expDate}` + logger.error(errmsg) + throw new Error(errmsg) } } - if (verifiedJwtPayload.tokentype === RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE) { - logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... verifiedJwtPayload.tokentype=', - verifiedJwtPayload.tokentype, - ) - verifiedRedeemJwtPayload = new RedeemJwtPayloadType( - verifiedJwtPayload.sendercommunityuuid as string, - verifiedJwtPayload.sendergradidoid as string, - verifiedJwtPayload.sendername as string, - verifiedJwtPayload.redeemcode as string, - verifiedJwtPayload.amount as string, - verifiedJwtPayload.memo as string, - verifiedJwtPayload.validuntil as string, - ) - logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... nach verify verifiedRedeemJwtPayload=', - verifiedRedeemJwtPayload, - ) - } - } - if (verifiedRedeemJwtPayload === null) { - logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... verifiedRedeemJwtPayload===null', - ) - verifiedRedeemJwtPayload = new RedeemJwtPayloadType( - decodedPayload.sendercommunityuuid as string, - decodedPayload.sendergradidoid as string, - decodedPayload.sendername as string, - decodedPayload.redeemcode as string, - decodedPayload.amount as string, - decodedPayload.memo as string, - decodedPayload.validuntil as string, - ) - } else { - // TODO: as long as the verification fails, fallback to simply decoded payload - verifiedRedeemJwtPayload = redeemJwtPayload - logger.debug( - 'TransactionLinkResolver.queryRedeemJwtLink... fallback to decode verifiedRedeemJwtPayload=', - verifiedRedeemJwtPayload, - ) } const homeCommunity = await getHomeCommunity() if (!homeCommunity) { - throw new LogError('Home community not found') + const errmsg = `Home community not found` + logger.error(errmsg) + throw new Error(errmsg) } const recipientCommunity = new Community(homeCommunity) const senderCommunity = new Community(senderCom) @@ -653,59 +710,9 @@ export class TransactionLinkResolver { logger.debug('TransactionLinkResolver.queryRedeemJwtLink... redeemJwtLink=', redeemJwtLink) return redeemJwtLink } else { - throw new LogError( - 'Redeem with wrong type of JWT-Token or expired! decodedPayload=', - decodedPayload, - ) + const errmsg = `transfer of redeem JWT with wrong envelope! code=${code}` + logger.error(errmsg) + throw new Error(errmsg) } } - - async createDisburseJwt( - senderCommunityUuid: string, - senderGradidoId: string, - recipientCommunityUuid: string, - recipientCommunityName: string, - recipientGradidoId: string, - recipientFirstName: string, - code: string, - amount: string, - memo: string, - validUntil: string, - recipientAlias: string, - ): Promise { - const logger = createLogger() - logger.addContext('code', code.substring(0, 6)) - logger.debug('TransactionLinkResolver.createDisburseJwt... args=', { - senderCommunityUuid, - senderGradidoId, - recipientCommunityUuid, - recipientCommunityName, - recipientGradidoId, - recipientFirstName, - code, - amount, - memo, - validUntil, - recipientAlias, - }) - - const disburseJwtPayloadType = new DisburseJwtPayloadType( - senderCommunityUuid, - senderGradidoId, - recipientCommunityUuid, - recipientCommunityName, - recipientGradidoId, - recipientFirstName, - code, - amount, - memo, - validUntil, - recipientAlias, - ) - // TODO:encode/sign the jwt normally with the private key of the recipient community, but interims with uuid - const disburseJwt = await encode(disburseJwtPayloadType, recipientCommunityUuid) - logger.debug('TransactionLinkResolver.createDisburseJwt... disburseJwt=', disburseJwt) - // TODO: encrypt the payload with the public key of the target/sender community - return disburseJwt - } } diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index ff786681b..7fc4630f8 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -13,12 +13,12 @@ import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' -import { CONFIG } from '@/config' +// import { CONFIG } from '@/config' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { EventType } from '@/event/Events' -import { SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' -import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' -import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' +// import { V1_0_SendCoinsClient } from 'core' +// import { SendCoinsArgs } from 'core' +// import { SendCoinsResult } from 'core' import { userFactory } from '@/seeds/factory/user' import { confirmContribution, diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 33d34d928..79b6eff5f 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -3,7 +3,6 @@ import { countOpenPendingTransactions, Community as DbCommunity, DltTransaction as DbDltTransaction, - PendingTransaction as DbPendingTransaction, Transaction as dbTransaction, TransactionLink as dbTransactionLink, User as dbUser, @@ -16,11 +15,10 @@ import { In, IsNull } from 'typeorm' import { Paginated } from '@arg/Paginated' import { TransactionSendArgs } from '@arg/TransactionSendArgs' import { Order } from '@enum/Order' -import { PendingTransactionState, SendCoinsResponseJwtPayloadType } from 'shared' -import { TransactionTypeId } from '@enum/TransactionTypeId' import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' import { User } from '@model/User' +import { processXComCompleteTransaction, TransactionTypeId } from 'core' import { RIGHTS } from '@/auth/RIGHTS' import { CONFIG } from '@/config' @@ -29,31 +27,21 @@ import { sendTransactionReceivedEmail, } from '@/emails/sendEmailVariants' import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Events' -import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' import { LogError } from '@/server/LogError' import { Context, getUser } from '@/server/context' -import { - InterruptiveSleepManager, - TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, -} from '@/util/InterruptiveSleepManager' -import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { communityUser } from '@/util/communityUser' -import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' import { virtualDecayTransaction, virtualLinkTransaction } from '@/util/virtualTransactions' +import { fullName } from 'core' +import { TRANSACTIONS_LOCK } from 'database' -import { Logger, getLogger } from 'log4js' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { getLastTransaction } from 'database' +import { getLogger, Logger } from 'log4js' import { BalanceResolver } from './BalanceResolver' import { GdtResolver } from './GdtResolver' -import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities' -import { getLastTransaction } from './util/getLastTransaction' +import { getCommunityName, isHomeCommunity } from './util/communities' import { getTransactionList } from './util/getTransactionList' -import { - processXComCommittingSendCoins, - processXComPendingSendCoins, -} from './util/processXComSendCoins' -import { storeForeignUser } from './util/storeForeignUser' import { transactionLinkSummary } from './util/transactionLinkSummary' import { transferTransaction } from '@/apis/dltConnector' @@ -482,78 +470,14 @@ export class TransactionResolver { await executeTransaction(amount, memo, senderUser, recipientUser, logger) logger.info('successful executeTransaction') } else { - // processing a x-community sendCoins - logger.info('X-Com: processing a x-community transaction...') - if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { - throw new LogError('X-Community sendCoins disabled per configuration!') - } - const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier) - logger.debug('recipient community: ', recipCom?.id) - if (recipCom === null) { - throw new LogError( - `no recipient community found for identifier: ${recipientCommunityIdentifier}`, - ) - } - if (recipCom !== null && recipCom.authenticatedAt === null) { - throw new LogError('recipient community is connected, but still not authenticated yet!') - } - let pendingResult: SendCoinsResponseJwtPayloadType | null = null - let committingResult: SendCoinsResult - const creationDate = new Date() - - try { - pendingResult = await processXComPendingSendCoins( - recipCom, - homeCom, - creationDate, - amount, - memo, - senderUser, - recipientIdentifier, - ) - logger.debug('processXComPendingSendCoins result: ', pendingResult) - if (pendingResult && pendingResult.vote && pendingResult.recipGradidoID) { - logger.debug('vor processXComCommittingSendCoins... ') - committingResult = await processXComCommittingSendCoins( - recipCom, - homeCom, - creationDate, - amount, - memo, - senderUser, - pendingResult, - ) - logger.debug('processXComCommittingSendCoins result: ', committingResult) - if (!committingResult.vote) { - logger.fatal('FATAL ERROR: on processXComCommittingSendCoins for', committingResult) - throw new LogError( - 'FATAL ERROR: on processXComCommittingSendCoins with ', - recipientCommunityIdentifier, - recipientIdentifier, - amount.toString(), - memo, - ) - } - // after successful x-com-tx store the recipient as foreign user - logger.debug('store recipient as foreign user...') - if (await storeForeignUser(recipCom, committingResult)) { - logger.info( - 'X-Com: new foreign user inserted successfully...', - recipCom.communityUuid, - committingResult.recipGradidoID, - ) - } - } - } catch (err) { - throw new LogError( - 'ERROR: on processXComCommittingSendCoins with ', - recipientCommunityIdentifier, - recipientIdentifier, - amount.toString(), - memo, - err, - ) - } + await processXComCompleteTransaction( + senderUser.communityUuid, + senderUser.gradidoID, + recipientCommunityIdentifier, + recipientIdentifier, + amount.valueOf(), + memo, + ) } return true } diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index bf2cb1f59..64817bd3e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -65,7 +65,7 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' -import { objectValuesToArray } from '@/util/utilities' +import { objectValuesToArray } from 'core' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { getLogger } from 'config-schema/test/testSetup' diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 385811dba..3e1e1a3c3 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -1,7 +1,6 @@ import { AppDatabase, ContributionLink as DbContributionLink, - DltTransaction as DbDltTransaction, TransactionLink as DbTransactionLink, User as DbUser, UserContact as DbUserContact, @@ -86,7 +85,7 @@ import { Context, getClientTimezoneOffset, getUser } from '@/server/context' import { communityDbUser } from '@/util/communityUser' import { hasElopageBuys } from '@/util/hasElopageBuys' import { durationInMinutesFromDates, getTimeDurationObject, printTimeDuration } from '@/util/time' -import { delay } from '@/util/utilities' +import { delay } from 'core' import random from 'random-bigint' import { randombytes_random } from 'sodium-native' diff --git a/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts b/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts index 74593f1c7..dfa775bde 100644 --- a/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts +++ b/backend/src/graphql/resolver/util/authenticateGmsUserPlayground.ts @@ -4,7 +4,7 @@ import { verifyAuthToken } from '@/apis/gms/GmsClient' import { CONFIG } from '@/config' import { GmsUserAuthenticationResult } from '@/graphql/model/GmsUserAuthenticationResult' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from 'core' import { getLogger } from 'log4js' const logger = getLogger( diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index ee7a0f96d..d1288a1ea 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -78,6 +78,11 @@ export async function getCommunityByUuid(communityUuid: string): Promise { + return await DbCommunity.findOne({ + where: [{ publicKey: publicKey }], + }) +} export async function getAuthenticatedCommunities(): Promise { const dbCommunities: DbCommunity[] = await DbCommunity.find({ diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index a0c930579..486e7cc99 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -6,7 +6,7 @@ import { OpenCreation } from '@model/OpenCreation' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '@/graphql/resolver/const/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LogError } from '@/server/LogError' -import { getFirstDayOfPreviousNMonth } from '@/util/utilities' +import { getFirstDayOfPreviousNMonth } from 'core' import { AppDatabase } from 'database' import { getLogger } from 'log4js' diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts new file mode 100644 index 000000000..769730544 --- /dev/null +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts @@ -0,0 +1,799 @@ +import { ApolloServerTestClient } from 'apollo-server-testing' +import { Community, DltTransaction, Transaction } from 'database' +import { Decimal } from 'decimal.js-light' +// import { GraphQLClient } from 'graphql-request' +// import { Response } from 'graphql-request/dist/types' +import { GraphQLClient } from 'graphql-request' +import { Response } from 'graphql-request/dist/types' +import { DataSource } from 'typeorm' +import { v4 as uuidv4 } from 'uuid' + +import { cleanDB, testEnvironment } from '@test/helpers' +import { i18n as localization } from '@test/testSetup' + +import { CONFIG } from '@/config' +import { TransactionTypeId } from 'core' +import { creations } from '@/seeds/creation' +import { creationFactory } from '@/seeds/factory/creation' +import { userFactory } from '@/seeds/factory/user' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { peterLustig } from '@/seeds/users/peter-lustig' +import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' +import { getLogger } from 'config-schema/test/testSetup' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' + +import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector' + +jest.mock('@/password/EncryptorUtils') + +const logger = getLogger( + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`, +) + +/* +// Mock the GraphQLClient +jest.mock('graphql-request', () => { + const originalModule = jest.requireActual('graphql-request') + + let testCursor = 0 + + return { + __esModule: true, + ...originalModule, + GraphQLClient: jest.fn().mockImplementation((url: string) => { + if (url === 'invalid') { + throw new Error('invalid url') + } + return { + // why not using mockResolvedValueOnce or mockReturnValueOnce? + // I have tried, but it didn't work and return every time the first value + request: jest.fn().mockImplementation(() => { + testCursor++ + if (testCursor === 4) { + return Promise.resolve( + // invalid, is 33 Bytes long as binary + { + transmitTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A', + }, + }, + ) + } else if (testCursor === 5) { + throw Error('Connection error') + } else { + return Promise.resolve( + // valid, is 32 Bytes long as binary + { + transmitTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + }, + }, + ) + } + }), + } + }), + } +}) +let mutate: ApolloServerTestClient['mutate'], + query: ApolloServerTestClient['query'], + con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} +*/ + +async function createHomeCommunity(): Promise { + const homeCommunity = Community.create() + homeCommunity.foreign = false + homeCommunity.communityUuid = uuidv4() + homeCommunity.url = 'localhost' + homeCommunity.publicKey = Buffer.from('0x6e6a6c6d6feffe', 'hex') + await Community.save(homeCommunity) + return homeCommunity +} + +async function createTxCREATION1(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(1000) + tx.balance = new Decimal(100) + tx.balanceDate = new Date('01.01.2023 00:00:00') + tx.memo = 'txCREATION1' + tx.typeId = TransactionTypeId.CREATION + tx.userGradidoID = 'txCREATION1.userGradidoID' + tx.userId = 1 + tx.userName = 'txCREATION 1' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('01.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('01.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxCREATION2(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(1000) + tx.balance = new Decimal(200) + tx.balanceDate = new Date('02.01.2023 00:00:00') + tx.memo = 'txCREATION2' + tx.typeId = TransactionTypeId.CREATION + tx.userGradidoID = 'txCREATION2.userGradidoID' + tx.userId = 2 + tx.userName = 'txCREATION 2' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('02.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('02.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxCREATION3(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(1000) + tx.balance = new Decimal(300) + tx.balanceDate = new Date('03.01.2023 00:00:00') + tx.memo = 'txCREATION3' + tx.typeId = TransactionTypeId.CREATION + tx.userGradidoID = 'txCREATION3.userGradidoID' + tx.userId = 3 + tx.userName = 'txCREATION 3' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('03.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('03.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxSend1ToReceive2(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(100) + tx.balance = new Decimal(1000) + tx.balanceDate = new Date('11.01.2023 00:00:00') + tx.memo = 'txSEND1 to txRECEIVE2' + tx.typeId = TransactionTypeId.SEND + tx.userGradidoID = 'txSEND1.userGradidoID' + tx.userId = 1 + tx.userName = 'txSEND 1' + tx.linkedUserGradidoID = 'txRECEIVE2.linkedUserGradidoID' + tx.linkedUserId = 2 + tx.linkedUserName = 'txRECEIVE 2' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('11.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a1' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('11.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxReceive2FromSend1(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(100) + tx.balance = new Decimal(1300) + tx.balanceDate = new Date('11.01.2023 00:00:00') + tx.memo = 'txSEND1 to txRECEIVE2' + tx.typeId = TransactionTypeId.RECEIVE + tx.userGradidoID = 'txRECEIVE2.linkedUserGradidoID' + tx.userId = 2 + tx.userName = 'txRECEIVE 2' + tx.linkedUserGradidoID = 'txSEND1.userGradidoID' + tx.linkedUserId = 1 + tx.linkedUserName = 'txSEND 1' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('11.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b2' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('11.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +/* +async function createTxSend2ToReceive3(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(200) + tx.balance = new Decimal(1100) + tx.balanceDate = new Date('23.01.2023 00:00:00') + tx.memo = 'txSEND2 to txRECEIVE3' + tx.typeId = TransactionTypeId.SEND + tx.userGradidoID = 'txSEND2.userGradidoID' + tx.userId = 2 + tx.userName = 'txSEND 2' + tx.linkedUserGradidoID = 'txRECEIVE3.linkedUserGradidoID' + tx.linkedUserId = 3 + tx.linkedUserName = 'txRECEIVE 3' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('23.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a2' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('23.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxReceive3FromSend2(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(200) + tx.balance = new Decimal(1500) + tx.balanceDate = new Date('23.01.2023 00:00:00') + tx.memo = 'txSEND2 to txRECEIVE3' + tx.typeId = TransactionTypeId.RECEIVE + tx.userGradidoID = 'txRECEIVE3.linkedUserGradidoID' + tx.userId = 3 + tx.userName = 'txRECEIVE 3' + tx.linkedUserGradidoID = 'txSEND2.userGradidoID' + tx.linkedUserId = 2 + tx.linkedUserName = 'txSEND 2' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('23.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b3' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('23.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxSend3ToReceive1(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(300) + tx.balance = new Decimal(1200) + tx.balanceDate = new Date('31.01.2023 00:00:00') + tx.memo = 'txSEND3 to txRECEIVE1' + tx.typeId = TransactionTypeId.SEND + tx.userGradidoID = 'txSEND3.userGradidoID' + tx.userId = 3 + tx.userName = 'txSEND 3' + tx.linkedUserGradidoID = 'txRECEIVE1.linkedUserGradidoID' + tx.linkedUserId = 1 + tx.linkedUserName = 'txRECEIVE 1' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('31.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a3' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('31.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxReceive1FromSend3(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(300) + tx.balance = new Decimal(1300) + tx.balanceDate = new Date('31.01.2023 00:00:00') + tx.memo = 'txSEND3 to txRECEIVE1' + tx.typeId = TransactionTypeId.RECEIVE + tx.userGradidoID = 'txRECEIVE1.linkedUserGradidoID' + tx.userId = 1 + tx.userName = 'txRECEIVE 1' + tx.linkedUserGradidoID = 'txSEND3.userGradidoID' + tx.linkedUserId = 3 + tx.linkedUserName = 'txSEND 3' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('31.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b1' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('31.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} +*/ + +let con: DataSource +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: DataSource +} + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.destroy() +}) + +describe('create and send Transactions to DltConnector', () => { + let txCREATION1: Transaction + let txCREATION2: Transaction + let txCREATION3: Transaction + let txSEND1to2: Transaction + let txRECEIVE2From1: Transaction + // let txSEND2To3: Transaction + // let txRECEIVE3From2: Transaction + // let txSEND3To1: Transaction + // let txRECEIVE1From3: Transaction + + beforeEach(() => { + jest.clearAllMocks() + }) + + afterEach(async () => { + await cleanDB() + }) + + describe('with 3 creations but inactive dlt-connector', () => { + it('found 3 dlt-transactions', async () => { + txCREATION1 = await createTxCREATION1(false) + txCREATION2 = await createTxCREATION2(false) + txCREATION3 = await createTxCREATION3(false) + await createHomeCommunity() + + CONFIG.DLT_CONNECTOR = false + await sendTransactionsToDltConnector() + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + // Find the previous created transactions of sendCoin mutation + const transactions = await Transaction.find({ + // where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + + const dltTransactions = await DltTransaction.find({ + // where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[0].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[1].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[2].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + + expect(logger.info).nthCalledWith(2, 'sending to DltConnector currently not configured...') + }) + }) + + describe('with 3 creations and active dlt-connector', () => { + it('found 3 dlt-transactions', async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await userFactory(testEnv, raeuberHotzenplotz) + await userFactory(testEnv, bobBaumeister) + let count = 0 + for (const creation of creations) { + await creationFactory(testEnv, creation) + count++ + // we need only 3 for testing + if (count >= 3) { + break + } + } + await createHomeCommunity() + + CONFIG.DLT_CONNECTOR = true + + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + return { + data: { + sendTransaction: { succeed: true }, + }, + } as Response + }) + + await sendTransactionsToDltConnector() + + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + // Find the previous created transactions of sendCoin mutation + const transactions = await Transaction.find({ + // where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + + const dltTransactions = await DltTransaction.find({ + // where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[0].id, + messageId: 'sended', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[1].id, + messageId: 'sended', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[2].id, + messageId: 'sended', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + }) + }) + + describe('with 3 verified creations, 1 sendCoins and active dlt-connector', () => { + it('found 3 dlt-transactions', async () => { + txCREATION1 = await createTxCREATION1(true) + txCREATION2 = await createTxCREATION2(true) + txCREATION3 = await createTxCREATION3(true) + await createHomeCommunity() + + txSEND1to2 = await createTxSend1ToReceive2(false) + txRECEIVE2From1 = await createTxReceive2FromSend1(false) + + /* + txSEND2To3 = await createTxSend2ToReceive3() + txRECEIVE3From2 = await createTxReceive3FromSend2() + txSEND3To1 = await createTxSend3ToReceive1() + txRECEIVE1From3 = await createTxReceive1FromSend3() + */ + + CONFIG.DLT_CONNECTOR = true + + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + return { + data: { + sendTransaction: { succeed: true }, + }, + } as Response + }) + + await sendTransactionsToDltConnector() + + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + // Find the previous created transactions of sendCoin mutation + /* + const transactions = await Transaction.find({ + // where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + */ + + const dltTransactions = await DltTransaction.find({ + // where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: txCREATION1.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1', + verified: true, + createdAt: new Date('01.01.2023 00:00:10'), + verifiedAt: new Date('01.01.2023 00:01:10'), + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txCREATION2.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2', + verified: true, + createdAt: new Date('02.01.2023 00:00:10'), + verifiedAt: new Date('02.01.2023 00:01:10'), + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txCREATION3.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3', + verified: true, + createdAt: new Date('03.01.2023 00:00:10'), + verifiedAt: new Date('03.01.2023 00:01:10'), + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txSEND1to2.id, + messageId: 'sended', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txRECEIVE2From1.id, + messageId: 'sended', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + }) + /* + describe('with one Community of api 1_0 and not matching pubKey', () => { + beforeEach(async () => { + + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + + return { + data: { + getPublicKey: { + publicKey: 'somePubKey', + }, + }, + } as Response + }) + const variables1 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '1_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables1) + .orUpdate({ + + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + + jest.clearAllMocks() + // await validateCommunities() + }) + + it('logs one community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs not matching publicKeys', () => { + expect(logger.warn).toBeCalledWith( + 'Federation: received not matching publicKey:', + 'somePubKey', + expect.stringMatching('11111111111111111111111111111111'), + ) + }) + }) + describe('with one Community of api 1_0 and matching pubKey', () => { + beforeEach(async () => { + + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + + return { + data: { + getPublicKey: { + publicKey: '11111111111111111111111111111111', + }, + }, + } as Response + }) + const variables1 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '1_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables1) + .orUpdate({ + + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + // await validateCommunities() + }) + + it('logs one community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs community pubKey verified', () => { + expect(logger.info).toHaveBeenNthCalledWith( + 3, + 'Federation: verified community with', + 'http//localhost:5001/api/', + ) + }) + }) + describe('with two Communities of api 1_0 and 1_1', () => { + beforeEach(async () => { + jest.clearAllMocks() + + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + + return { + data: { + getPublicKey: { + publicKey: '11111111111111111111111111111111', + }, + }, + } as Response + }) + const variables2 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '1_1', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables2) + .orUpdate({ + + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + // await validateCommunities() + }) + it('logs two communities found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs requestGetPublicKey for community api 1_1 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_1/', + ) + }) + }) + describe('with three Communities of api 1_0, 1_1 and 2_0', () => { + let dbCom: DbFederatedCommunity + beforeEach(async () => { + const variables3 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '2_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables3) + .orUpdate({ + + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + dbCom = await DbFederatedCommunity.findOneOrFail({ + where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, + }) + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + // await validateCommunities() + }) + it('logs three community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs requestGetPublicKey for community api 1_1 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_1/', + ) + }) + it('logs unsupported api for community with api 2_0 ', () => { + expect(logger.warn).toBeCalledWith( + 'Federation: dbCom with unsupported apiVersion', + dbCom.endPoint, + '2_0', + ) + }) + }) + */ + }) +}) diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 1dd8a4529..1190e32aa 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -2,9 +2,9 @@ import { TransactionLink as dbTransactionLink } from 'database' import { Decimal } from 'decimal.js-light' import { validate, version } from 'uuid' -import { Decay } from '@model/Decay' +import { Decay } from 'core' -import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' +import { getLastTransaction } from 'database' import { transactionLinkSummary } from '@/graphql/resolver/util/transactionLinkSummary' import { calculateDecay } from 'shared' diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index dabaee264..a3bbbe042 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -2,7 +2,7 @@ import { Transaction as dbTransaction } from 'database' import { Decimal } from 'decimal.js-light' import { RemoveOptions, SaveOptions } from 'typeorm' -import { TransactionTypeId } from '@enum/TransactionTypeId' +import { TransactionTypeId } from 'core' import { Transaction } from '@model/Transaction' import { User } from '@model/User' diff --git a/bun.lock b/bun.lock index 532b20dc6..0a55639b6 100644 --- a/bun.lock +++ b/bun.lock @@ -184,14 +184,22 @@ "dependencies": { "database": "*", "esbuild": "^0.25.2", + "i18n": "^0.15.1", "jose": "^4.14.4", "log4js": "^6.9.1", "shared": "*", + "sodium-native": "^3.4.1", "zod": "^3.25.61", }, "devDependencies": { "@biomejs/biome": "2.0.0", + "@types/i18n": "^0.13.4", "@types/node": "^17.0.21", + "@types/sodium-native": "^2.3.5", + "config-schema": "*", + "decimal.js-light": "^2.5.1", + "graphql-request": "5.0.0", + "jest": "27.2.4", "type-graphql": "^1.1.1", "typescript": "^4.9.5", }, @@ -228,6 +236,7 @@ "@types/geojson": "^7946.0.13", "@types/jest": "27.0.2", "@types/node": "^18.7.14", + "await-semaphore": "^0.1.3", "crypto-random-bigint": "^2.1.1", "jest": "27.2.4", "ts-jest": "27.0.5", diff --git a/core/package.json b/core/package.json index a1f7a5603..9e745f948 100644 --- a/core/package.json +++ b/core/package.json @@ -24,20 +24,28 @@ "lint:fix": "biome check --error-on-warnings . --write", "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" }, - "devDependencies": { - "@biomejs/biome": "2.0.0", - "@types/node": "^17.0.21", - "type-graphql": "^1.1.1", - "typescript": "^4.9.5" - }, "dependencies": { "database": "*", "esbuild": "^0.25.2", + "i18n": "^0.15.1", "jose": "^4.14.4", "log4js": "^6.9.1", "shared": "*", + "sodium-native": "^3.4.1", "zod": "^3.25.61" }, + "devDependencies": { + "@biomejs/biome": "2.0.0", + "@types/i18n": "^0.13.4", + "@types/node": "^17.0.21", + "@types/sodium-native": "^2.3.5", + "config-schema": "*", + "decimal.js-light": "^2.5.1", + "graphql-request": "5.0.0", + "jest": "27.2.4", + "type-graphql": "^1.1.1", + "typescript": "^4.9.5" + }, "engines": { "node": ">=18" } diff --git a/core/src/config/index.ts b/core/src/config/index.ts new file mode 100644 index 000000000..31b8cf033 --- /dev/null +++ b/core/src/config/index.ts @@ -0,0 +1,27 @@ +// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) + +import { LogLevel, validate } from 'config-schema' +import dotenv from 'dotenv' + +import { schema } from './schema' + +dotenv.config() + + +const federation = { + FEDERATION_BACKEND_SEND_ON_API: process.env.FEDERATION_BACKEND_SEND_ON_API ?? '1_0', + FEDERATION_XCOM_SENDCOINS_ENABLED: + process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' || false, + // 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: parseInt( + process.env.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS ?? '3', + ), +} + + +export const CONFIG = { + ...federation, +} +validate(schema, CONFIG) diff --git a/core/src/config/schema.ts b/core/src/config/schema.ts new file mode 100644 index 000000000..dbf6478de --- /dev/null +++ b/core/src/config/schema.ts @@ -0,0 +1,29 @@ +import Joi from 'joi' + +export const schema = Joi.object({ + FEDERATION_BACKEND_SEND_ON_API: Joi.string() + .pattern(/^\d+_\d+$/) + .default('1_0') + .description('API Version of sending requests to another communities, e.g., "1_0"') + .required(), + + FEDERATION_XCOM_SENDCOINS_ENABLED: Joi.boolean() + .default(false) + .description('Enable or disable the federation send coins feature') + .optional(), + + FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID: Joi.string() + .uuid() + .default('56a55482-909e-46a4-bfa2-cd025e894ebc') + .description( + 'UUID of the receiver community for federation cross-community transactions if the receiver is unknown', + ) + .required(), + + FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS: Joi.number() + .integer() + .min(0) + .default(3) + .description('Maximum number of retries for reverting send coins transactions') + .required(), +}) diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/core/src/federation/client/1_0/SendCoinsClient.ts similarity index 87% rename from backend/src/federation/client/1_0/SendCoinsClient.ts rename to core/src/federation/client/1_0/SendCoinsClient.ts index 13c2b8697..a872c438d 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/core/src/federation/client/1_0/SendCoinsClient.ts @@ -1,16 +1,15 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { GraphQLClient } from 'graphql-request' -import { LogError } from '@/server/LogError' -import { ensureUrlEndsWithSlash } from '@/util/utilities' +import { ensureUrlEndsWithSlash } from '../../../util/utilities' import { getLogger } from 'log4js' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { LOG4JS_BASE_CATEGORY_NAME } from '../../../config/const' import { revertSendCoins as revertSendCoinsQuery } from './query/revertSendCoins' import { revertSettledSendCoins as revertSettledSendCoinsQuery } from './query/revertSettledSendCoins' import { settleSendCoins as settleSendCoinsQuery } from './query/settleSendCoins' import { voteForSendCoins as voteForSendCoinsQuery } from './query/voteForSendCoins' -import { EncryptedTransferArgs } from 'core' +import { EncryptedTransferArgs } from '../../../graphql/model/EncryptedTransferArgs' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.SendCoinsClient`) @@ -83,7 +82,9 @@ export class SendCoinsClient { logger.debug(`settleSendCoins successful from endpoint=${this.endpoint}`) return true } catch (err) { - throw new LogError(`settleSendCoins failed for endpoint=${this.endpoint}`, err) + const errmsg = `settleSendCoins failed for endpoint=${this.endpoint}, err=${err}` + logger.error(errmsg) + throw new Error(errmsg) } } @@ -103,7 +104,9 @@ export class SendCoinsClient { logger.debug(`revertSettledSendCoins successful from endpoint=${this.endpoint}`) return true } catch (err) { - throw new LogError(`revertSettledSendCoins failed for endpoint=${this.endpoint}`, err) + const errmsg = `revertSettledSendCoins failed for endpoint=${this.endpoint}, err=${err}` + logger.error(errmsg) + throw new Error(errmsg) } } } diff --git a/backend/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts b/core/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts similarity index 90% rename from backend/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts rename to core/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts index 9bd8fee69..11afa4346 100644 --- a/backend/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts +++ b/core/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts @@ -1,6 +1,6 @@ import { AbstractLoggingView } from 'database' -import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' +import { SendCoinsArgs } from '../model/SendCoinsArgs' export class SendCoinsArgsLoggingView extends AbstractLoggingView { public constructor(private self: SendCoinsArgs) { diff --git a/backend/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts b/core/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts similarity index 86% rename from backend/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts rename to core/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts index 2f2ec355e..1eb08c432 100644 --- a/backend/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts +++ b/core/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts @@ -1,6 +1,6 @@ import { AbstractLoggingView } from 'database' -import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' +import { SendCoinsResult } from '../model/SendCoinsResult' export class SendCoinsResultLoggingView extends AbstractLoggingView { public constructor(private self: SendCoinsResult) { diff --git a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts b/core/src/federation/client/1_0/model/SendCoinsArgs.ts similarity index 100% rename from backend/src/federation/client/1_0/model/SendCoinsArgs.ts rename to core/src/federation/client/1_0/model/SendCoinsArgs.ts diff --git a/backend/src/federation/client/1_0/model/SendCoinsResult.ts b/core/src/federation/client/1_0/model/SendCoinsResult.ts similarity index 100% rename from backend/src/federation/client/1_0/model/SendCoinsResult.ts rename to core/src/federation/client/1_0/model/SendCoinsResult.ts diff --git a/backend/src/federation/client/1_0/query/revertSendCoins.ts b/core/src/federation/client/1_0/query/revertSendCoins.ts similarity index 100% rename from backend/src/federation/client/1_0/query/revertSendCoins.ts rename to core/src/federation/client/1_0/query/revertSendCoins.ts diff --git a/backend/src/federation/client/1_0/query/revertSettledSendCoins.ts b/core/src/federation/client/1_0/query/revertSettledSendCoins.ts similarity index 100% rename from backend/src/federation/client/1_0/query/revertSettledSendCoins.ts rename to core/src/federation/client/1_0/query/revertSettledSendCoins.ts diff --git a/backend/src/federation/client/1_0/query/settleSendCoins.ts b/core/src/federation/client/1_0/query/settleSendCoins.ts similarity index 100% rename from backend/src/federation/client/1_0/query/settleSendCoins.ts rename to core/src/federation/client/1_0/query/settleSendCoins.ts diff --git a/backend/src/federation/client/1_0/query/voteForSendCoins.ts b/core/src/federation/client/1_0/query/voteForSendCoins.ts similarity index 58% rename from backend/src/federation/client/1_0/query/voteForSendCoins.ts rename to core/src/federation/client/1_0/query/voteForSendCoins.ts index 6fc9a11ae..9b987657c 100644 --- a/backend/src/federation/client/1_0/query/voteForSendCoins.ts +++ b/core/src/federation/client/1_0/query/voteForSendCoins.ts @@ -5,13 +5,4 @@ export const voteForSendCoins = gql` voteForSendCoins(data: $args) } ` -/* - { - vote - recipGradidoID - recipFirstName - recipLastName - recipAlias - } - } -*/ + diff --git a/core/src/federation/client/1_1/SendCoinsClient.ts b/core/src/federation/client/1_1/SendCoinsClient.ts new file mode 100644 index 000000000..b0eab7c0c --- /dev/null +++ b/core/src/federation/client/1_1/SendCoinsClient.ts @@ -0,0 +1,3 @@ +import { SendCoinsClient as V1_0_SendCoinsClient } from '../1_0/SendCoinsClient' + +export class SendCoinsClient extends V1_0_SendCoinsClient {} diff --git a/backend/src/federation/client/SendCoinsClientFactory.ts b/core/src/federation/client/SendCoinsClientFactory.ts similarity index 85% rename from backend/src/federation/client/SendCoinsClientFactory.ts rename to core/src/federation/client/SendCoinsClientFactory.ts index 2fd15f739..4a2772175 100644 --- a/backend/src/federation/client/SendCoinsClientFactory.ts +++ b/core/src/federation/client/SendCoinsClientFactory.ts @@ -1,9 +1,8 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' -import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' - -import { SendCoinsClient as V1_1_SendCoinsClient } from '@/federation/client/1_1/SendCoinsClient' -import { ApiVersionType } from '@/federation/enum/apiVersionType' +import { SendCoinsClient as V1_0_SendCoinsClient } from './1_0/SendCoinsClient' +import { SendCoinsClient as V1_1_SendCoinsClient } from './1_1/SendCoinsClient' +import { ApiVersionType } from '../enum/apiVersionType' type SendCoinsClient = V1_0_SendCoinsClient | V1_1_SendCoinsClient diff --git a/backend/src/federation/enum/apiVersionType.ts b/core/src/federation/enum/apiVersionType.ts similarity index 100% rename from backend/src/federation/enum/apiVersionType.ts rename to core/src/federation/enum/apiVersionType.ts diff --git a/backend/src/graphql/enum/TransactionTypeId.ts b/core/src/graphql/enum/TransactionTypeId.ts similarity index 100% rename from backend/src/graphql/enum/TransactionTypeId.ts rename to core/src/graphql/enum/TransactionTypeId.ts diff --git a/backend/src/logging/DecayLogging.view.ts b/core/src/graphql/logging/DecayLogging.view.ts similarity index 92% rename from backend/src/logging/DecayLogging.view.ts rename to core/src/graphql/logging/DecayLogging.view.ts index 538beecd5..2c485447a 100644 --- a/backend/src/logging/DecayLogging.view.ts +++ b/core/src/graphql/logging/DecayLogging.view.ts @@ -1,6 +1,6 @@ import { AbstractLoggingView } from 'database' -import { Decay } from '@/graphql/model/Decay' +import { Decay } from '../model/Decay' import type { Decay as DecayInterface } from 'shared' export class DecayLoggingView extends AbstractLoggingView { diff --git a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts index 0d522bff7..301f6da16 100644 --- a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts +++ b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts @@ -6,10 +6,10 @@ import { CommunityLoggingView, getHomeCommunity } from 'database' import { verifyAndDecrypt } from 'shared' import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs`) +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs`) export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise => { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs-method`) + const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs-method`) methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug('interpretEncryptedTransferArgs()... args:', args) // first find with args.publicKey the community 'requestingCom', which starts the request diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/core/src/graphql/logic/processXComSendCoins.ts similarity index 67% rename from backend/src/graphql/resolver/util/processXComSendCoins.ts rename to core/src/graphql/logic/processXComSendCoins.ts index 91eb61a1e..5b3d6584a 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/core/src/graphql/logic/processXComSendCoins.ts @@ -1,37 +1,160 @@ import { + CommunityLoggingView, + countOpenPendingTransactions, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, PendingTransaction as DbPendingTransaction, User as dbUser, + findTransactionLinkByCode, + findUserByIdentifier, + getCommunityByUuid, PendingTransactionLoggingView, - CommunityLoggingView, - UserLoggingView, - countOpenPendingTransactions, + UserLoggingView } from 'database' import { Decimal } from 'decimal.js-light' -import { CONFIG } from '@/config' +import { CONFIG as CONFIG_CORE } from '../../config' +import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' -import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient' -import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' -import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' -import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory' +import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient' +import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult' +import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory' +import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId' import { encryptAndSign, PendingTransactionState, SendCoinsJwtPayloadType, SendCoinsResponseJwtPayloadType, verifyAndDecrypt } from 'shared' -import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { LogError } from '@/server/LogError' -import { calculateSenderBalance } from '@/util/calculateSenderBalance' -import { fullName } from '@/util/utilities' +// import { LogError } from '@server/LogError' +import { calculateSenderBalance } from '../../util/calculateSenderBalance' +import { fullName } from '../../util/utilities' import { getLogger } from 'log4js' -import { settlePendingSenderTransaction } from './settlePendingSenderTransaction' -import { SendCoinsArgsLoggingView } from '@/federation/client/1_0/logging/SendCoinsArgsLogging.view' -import { SendCoinsResultLoggingView } from '@/federation/client/1_0/logging/SendCoinsResultLogging.view' -import { EncryptedTransferArgs } from 'core' +import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/SendCoinsResultLogging.view' +import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs' import { randombytes_random } from 'sodium-native' +import { settlePendingSenderTransaction } from './settlePendingSenderTransaction' +import { storeForeignUser } from './storeForeignUser' const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.processXComSendCoins.${method}`) +export async function processXComCompleteTransaction( + senderCommunityUuid: string, + senderGradidoId: string, + recipientCommunityUuid: string, + recipientGradidoId: string, + amount: string, + memo: string, + code?: string, + recipientFirstName?: string, + recipientAlias?: string, + creationDate?: Date, +): Promise { + const methodLogger = createLogger(`processXComCompleteTransaction`) + // processing a x-community sendCoins + methodLogger.info('processing a x-community transaction...') + if (!CONFIG_CORE.FEDERATION_XCOM_SENDCOINS_ENABLED) { + const errmsg = 'X-Community sendCoins disabled per configuration!' + methodLogger.error(errmsg) + throw new Error(errmsg) + } + const senderCom = await getCommunityByUuid(senderCommunityUuid) + methodLogger.debug('sender community: ', senderCom?.id) + if (senderCom === null) { + const errmsg = `no sender community found for identifier: ${senderCommunityUuid}` + methodLogger.error(errmsg) + throw new Error(errmsg) + } + const senderUser = await findUserByIdentifier(senderGradidoId, senderCommunityUuid) + if (senderUser === null) { + const errmsg = `no sender user found for identifier: ${senderCommunityUuid}:${senderGradidoId}` + methodLogger.error(errmsg) + throw new Error(errmsg) + } + const recipientCom = await getCommunityByUuid(recipientCommunityUuid) + methodLogger.debug('recipient community: ', recipientCom?.id) + if (recipientCom === null) { + const errmsg = `no recipient community found for identifier: ${recipientCommunityUuid}` + methodLogger.error(errmsg) + throw new Error(errmsg) + } + if (recipientCom !== null && recipientCom.authenticatedAt === null) { + const errmsg = 'recipient community is connected, but still not authenticated yet!' + methodLogger.error(errmsg) + throw new Error(errmsg) + } + if(code !== undefined) { + try { + const dbTransactionLink = await findTransactionLinkByCode(code) + if (dbTransactionLink && dbTransactionLink.validUntil < new Date()) { + const errmsg = `TransactionLink ${code} is expired!` + methodLogger.error(errmsg) + throw new Error(errmsg) + } + } catch (_err) { + const errmsg = `TransactionLink ${code} not found any more!` + methodLogger.error(errmsg) + throw new Error(errmsg) + } + } + if(creationDate === undefined) { + creationDate = new Date() + } + let pendingResult: SendCoinsResponseJwtPayloadType | null = null + let committingResult: SendCoinsResult + + try { + pendingResult = await processXComPendingSendCoins( + recipientCom, + senderCom, + creationDate, + new Decimal(amount), + memo, + senderUser, + recipientGradidoId, + ) + methodLogger.debug('processXComPendingSendCoins result: ', pendingResult) + if (pendingResult && pendingResult.vote && pendingResult.recipGradidoID) { + methodLogger.debug('vor processXComCommittingSendCoins... ') + committingResult = await processXComCommittingSendCoins( + recipientCom, + senderCom, + creationDate, + new Decimal(amount), + memo, + senderUser, + pendingResult, + ) + methodLogger.debug('processXComCommittingSendCoins result: ', committingResult) + if (!committingResult.vote) { + methodLogger.fatal('FATAL ERROR: on processXComCommittingSendCoins for', committingResult) + throw new Error( + 'FATAL ERROR: on processXComCommittingSendCoins with ' + + recipientCom.communityUuid + + recipientGradidoId + + amount.toString() + + memo, + ) + } + // after successful x-com-tx store the recipient as foreign user + methodLogger.debug('store recipient as foreign user...') + if (await storeForeignUser(recipientCom, committingResult)) { + methodLogger.info( + 'X-Com: new foreign user inserted successfully...', + recipientCom.communityUuid, + committingResult.recipGradidoID, + ) + } + } + } catch (err) { + const errmsg = `ERROR: on processXComCommittingSendCoins with ` + + recipientCommunityUuid + + recipientGradidoId + + amount.toString() + + memo + + err + methodLogger.error(errmsg) + throw new Error(errmsg) + } + return true +} + export async function processXComPendingSendCoins( receiverCom: DbCommunity, senderCom: DbCommunity, @@ -60,7 +183,7 @@ export async function processXComPendingSendCoins( if (await countOpenPendingTransactions([sender.gradidoID, recipientIdentifier]) > 0) { const errmsg = `There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!` methodLogger.error(errmsg) - throw new LogError(errmsg) + throw new Error(errmsg) } const handshakeID = randombytes_random().toString() methodLogger.addContext('handshakeID', handshakeID) @@ -69,7 +192,7 @@ export async function processXComPendingSendCoins( if (!senderBalance) { const errmsg = `User has not enough GDD or amount is < 0` methodLogger.error(errmsg) - throw new LogError(errmsg) + throw new Error(errmsg) } if(methodLogger.isDebugEnabled()) { methodLogger.debug(`calculated senderBalance = ${JSON.stringify(senderBalance, null, 2)}`) @@ -78,7 +201,7 @@ export async function processXComPendingSendCoins( const receiverFCom = await DbFederatedCommunity.findOneOrFail({ where: { publicKey: Buffer.from(receiverCom.publicKey), - apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API, + apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, }, }) const client = SendCoinsClientFactory.getInstance(receiverFCom) @@ -173,7 +296,7 @@ export async function processXComPendingSendCoins( methodLogger.error(errmsg) throw new Error(errmsg) } - } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) + } while (CONFIG_CORE.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) const errmsg = `Error in reverting receiver pending transaction even after revertCount=${revertCount}` + JSON.stringify(err, null, 2) methodLogger.error(errmsg) throw new Error(errmsg) @@ -228,7 +351,7 @@ export async function processXComCommittingSendCoins( userGradidoID: sender.gradidoID, userName: fullName(sender.firstName, sender.lastName), linkedUserCommunityUuid: - receiverCom.communityUuid ?? CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID, + receiverCom.communityUuid ?? CONFIG_CORE.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID, linkedUserGradidoID: recipient.recipGradidoID ? recipient.recipGradidoID : undefined, typeId: TransactionTypeId.SEND, state: PendingTransactionState.NEW, @@ -242,7 +365,7 @@ export async function processXComCommittingSendCoins( const receiverFCom = await DbFederatedCommunity.findOneOrFail({ where: { publicKey: Buffer.from(receiverCom.publicKey), - apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API, + apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, }, }) const client = SendCoinsClientFactory.getInstance(receiverFCom) @@ -252,7 +375,7 @@ export async function processXComCommittingSendCoins( handshakeID, pendingTx.linkedUserCommunityUuid ? pendingTx.linkedUserCommunityUuid - : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID, + : CONFIG_CORE.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID, pendingTx.linkedUserGradidoID!, pendingTx.balanceDate.toISOString(), pendingTx.amount.mul(-1), @@ -264,7 +387,7 @@ export async function processXComCommittingSendCoins( ) payload.recipientCommunityUuid = pendingTx.linkedUserCommunityUuid ? pendingTx.linkedUserCommunityUuid - : CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID + : CONFIG_CORE.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID if (pendingTx.linkedUserGradidoID) { payload.recipientUserIdentifier = pendingTx.linkedUserGradidoID } @@ -305,20 +428,21 @@ export async function processXComCommittingSendCoins( methodLogger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`) // revert the existing pending transaction on receiver side let revertCount = 0 - methodLogger.debug('first try to revertSetteledSendCoins of receiver') + methodLogger.debug('first try to revertSettledSendCoins of receiver') do { if (await client.revertSettledSendCoins(args)) { methodLogger.debug( `revertSettledSendCoins()-1_0... successfull after revertCount=${revertCount}`, ) // treat revertingSettledSendCoins as an error of the whole sendCoins-process - throw new LogError('Error in settle sender pending transaction: ', err) + const errmsg = `Error in settle sender pending transaction: ${JSON.stringify(err, null, 2)}` + methodLogger.error(errmsg) + throw new Error(errmsg) } - } while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) - throw new LogError( - `Error in reverting receiver pending transaction even after revertCount=${revertCount}`, - err, - ) + } while (CONFIG_CORE.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++) + const errmsg = `Error in reverting receiver pending transaction even after revertCount=${revertCount}` + methodLogger.error(errmsg) + throw new Error(errmsg) } } } diff --git a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts b/core/src/graphql/logic/settlePendingSenderTransaction.ts similarity index 84% rename from backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts rename to core/src/graphql/logic/settlePendingSenderTransaction.ts index c528b0ca7..87ab8116c 100644 --- a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts +++ b/core/src/graphql/logic/settlePendingSenderTransaction.ts @@ -10,17 +10,16 @@ import { } from 'database' import { Decimal } from 'decimal.js-light' +import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' import { PendingTransactionState } from 'shared' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { LogError } from '@/server/LogError' -import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' -import { calculateSenderBalance } from '@/util/calculateSenderBalance' +// import { LogError } from '@/server/LogError' +import { calculateSenderBalance } from 'core' +import { TRANSACTIONS_LOCK, getLastTransaction } from 'database' import { getLogger } from 'log4js' -import { getLastTransaction } from './getLastTransaction' const db = AppDatabase.getInstance() const logger = getLogger( - `${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.settlePendingSenderTransaction`, + `${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.settlePendingSenderTransaction`, ) export async function settlePendingSenderTransaction( @@ -53,15 +52,17 @@ export async function settlePendingSenderTransaction( ], }) if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) { - throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient') + const errmsg = `There are more than 1 pending Transactions for Sender and/or Recipient` + logger.error(errmsg) + throw new Error(errmsg) } const lastTransaction = await getLastTransaction(senderUser.id) if (lastTransaction?.id !== pendingTx.previous) { - throw new LogError( - `X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}`, - ) + const errmsg = `X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}` + logger.error(errmsg) + throw new Error(errmsg) } // transfer the pendingTx to the transactions table @@ -82,7 +83,9 @@ export async function settlePendingSenderTransaction( pendingTx.balanceDate, ) if (!sendBalance) { - throw new LogError(`Sender has not enough GDD or amount is < 0', sendBalance`) + const errmsg = 'Sender has not enough GDD or amount is < 0' + logger.error(errmsg) + throw new Error(errmsg) } transactionSend.balance = sendBalance?.balance ?? new Decimal(0) transactionSend.balanceDate = pendingTx.balanceDate @@ -114,7 +117,8 @@ export async function settlePendingSenderTransaction( // void sendTransactionsToDltConnector() } catch (e) { await queryRunner.rollbackTransaction() - throw new LogError('X-Com: send Transaction was not successful', e) + logger.error('X-Com: send Transaction was not successful', e) + throw new Error('X-Com: send Transaction was not successful') } finally { await queryRunner.release() releaseLock() diff --git a/backend/src/graphql/resolver/util/storeForeignUser.ts b/core/src/graphql/logic/storeForeignUser.ts similarity index 81% rename from backend/src/graphql/resolver/util/storeForeignUser.ts rename to core/src/graphql/logic/storeForeignUser.ts index adceb155e..1e6b8153f 100644 --- a/backend/src/graphql/resolver/util/storeForeignUser.ts +++ b/core/src/graphql/logic/storeForeignUser.ts @@ -1,10 +1,10 @@ -import { Community as DbCommunity, User as DbUser } from 'database' +import { Community as DbCommunity, User as DbUser, findForeignUserByUuids } from 'database' -import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult' +import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' import { getLogger } from 'log4js' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.storeForeignUser`) +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.storeForeignUser`) export async function storeForeignUser( recipCom: DbCommunity, @@ -12,13 +12,7 @@ export async function storeForeignUser( ): Promise { if (recipCom.communityUuid !== null && committingResult.recipGradidoID !== null) { try { - const user = await DbUser.findOne({ - where: { - foreign: true, - communityUuid: recipCom.communityUuid, - gradidoID: committingResult.recipGradidoID, - }, - }) + const user = await findForeignUserByUuids(recipCom.communityUuid, committingResult.recipGradidoID) if (!user) { logger.debug( 'no foreignUser found for:', diff --git a/backend/src/graphql/model/Decay.ts b/core/src/graphql/model/Decay.ts similarity index 100% rename from backend/src/graphql/model/Decay.ts rename to core/src/graphql/model/Decay.ts diff --git a/core/src/index.ts b/core/src/index.ts index 28aae758a..a355bb9bd 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,3 +1,25 @@ -export * from './validation/user' +export {SendCoinsClient as V1_0_SendCoinsClient} from './federation/client/1_0/SendCoinsClient' +export * from './federation/client/1_0/logging/SendCoinsArgsLogging.view' +export * from './federation/client/1_0/logging/SendCoinsResultLogging.view' +export * from './federation/client/1_0/model/SendCoinsArgs' +export * from './federation/client/1_0/model/SendCoinsResult' +export * from './federation/client/1_0/query/revertSendCoins' +export * from './federation/client/1_0/query/revertSettledSendCoins' +export * from './federation/client/1_0/query/settleSendCoins' +export * from './federation/client/1_0/query/voteForSendCoins' +export {SendCoinsClient as V1_1_SendCoinsClient} from './federation/client/1_1/SendCoinsClient' +export * from './federation/client/SendCoinsClientFactory' +export * from './federation/enum/apiVersionType' +export * from './graphql/enum/TransactionTypeId' +export * from './graphql/logging/DecayLogging.view' export * from './graphql/logic/interpretEncryptedTransferArgs' +export * from './graphql/logic/processXComSendCoins' +export * from './graphql/logic/settlePendingSenderTransaction' +export * from './graphql/logic/storeForeignUser' +export * from './graphql/model/Decay' export * from './graphql/model/EncryptedTransferArgs' +export * from './util/calculateSenderBalance' +export * from './util/utilities' +export * from './validation/user' +export * from './config/index' + diff --git a/backend/src/util/calculateSenderBalance.ts b/core/src/util/calculateSenderBalance.ts similarity index 83% rename from backend/src/util/calculateSenderBalance.ts rename to core/src/util/calculateSenderBalance.ts index a01b1f15e..7fe5ae19f 100644 --- a/backend/src/util/calculateSenderBalance.ts +++ b/core/src/util/calculateSenderBalance.ts @@ -1,8 +1,8 @@ import { Decimal } from 'decimal.js-light' -import { Decay } from '@model/Decay' +import { Decay } from '../graphql/model/Decay' -import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' +import { getLastTransaction } from 'database' import { calculateDecay } from 'shared' diff --git a/backend/src/util/utilities.test.ts b/core/src/util/utilities.test.ts similarity index 100% rename from backend/src/util/utilities.test.ts rename to core/src/util/utilities.test.ts diff --git a/backend/src/util/utilities.ts b/core/src/util/utilities.ts similarity index 100% rename from backend/src/util/utilities.ts rename to core/src/util/utilities.ts diff --git a/core/tsconfig.json b/core/tsconfig.json index 75b686340..b8e0f1d0a 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -44,8 +44,8 @@ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ + // "paths": { }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [".", "../database"], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": ["bun-types"], /* Type declaration files to be included in compilation. */ diff --git a/database/package.json b/database/package.json index 6a6240fa1..26aa6b9e1 100644 --- a/database/package.json +++ b/database/package.json @@ -41,6 +41,7 @@ "@types/geojson": "^7946.0.13", "@types/jest": "27.0.2", "@types/node": "^18.7.14", + "await-semaphore": "^0.1.3", "crypto-random-bigint": "^2.1.1", "jest": "27.2.4", "ts-jest": "27.0.5", diff --git a/database/src/index.ts b/database/src/index.ts index 3ff4efa17..56dec24ee 100644 --- a/database/src/index.ts +++ b/database/src/index.ts @@ -60,4 +60,5 @@ export const entities = [ export { latestDbVersion } export * from './logging' export * from './queries' +export * from './util' export { AppDatabase } from './AppDatabase' diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index 868db42e8..edc3bd7ed 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -1,4 +1,4 @@ -import { Community as DbCommunity } from '../entity/Community' +import { Community as DbCommunity } from '../entity' /** * Retrieves the home community, i.e., a community that is not foreign. @@ -8,4 +8,10 @@ export async function getHomeCommunity(): Promise { return await DbCommunity.findOne({ where: { foreign: false }, }) -} \ No newline at end of file +} + +export async function getCommunityByUuid(communityUuid: string): Promise { + return await DbCommunity.findOne({ + where: [{ communityUuid }], + }) +} diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts index 9acf80871..1fec568bf 100644 --- a/database/src/queries/index.ts +++ b/database/src/queries/index.ts @@ -3,5 +3,7 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const' export * from './user' export * from './communities' export * from './pendingTransactions' +export * from './transactions' +export * from './transactionLinks' export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` diff --git a/database/src/queries/transactionLinks.ts b/database/src/queries/transactionLinks.ts new file mode 100644 index 000000000..eb9908101 --- /dev/null +++ b/database/src/queries/transactionLinks.ts @@ -0,0 +1,8 @@ +import { TransactionLink as DbTransactionLink } from "../entity" + +export async function findTransactionLinkByCode(code: string): Promise { + return await DbTransactionLink.findOneOrFail({ + where: { code }, + withDeleted: true, + }) +} diff --git a/backend/src/graphql/resolver/util/getLastTransaction.ts b/database/src/queries/transactions.ts similarity index 81% rename from backend/src/graphql/resolver/util/getLastTransaction.ts rename to database/src/queries/transactions.ts index 0e5b236e2..5df8186e7 100644 --- a/backend/src/graphql/resolver/util/getLastTransaction.ts +++ b/database/src/queries/transactions.ts @@ -1,4 +1,4 @@ -import { Transaction as DbTransaction } from 'database' +import { Transaction as DbTransaction } from '../entity' export const getLastTransaction = async ( userId: number, diff --git a/database/src/queries/user.ts b/database/src/queries/user.ts index 92ed7a8f8..2cb0eaaee 100644 --- a/database/src/queries/user.ts +++ b/database/src/queries/user.ts @@ -61,3 +61,12 @@ export const findUserByIdentifier = async ( } return null } + +export async function findForeignUserByUuids( + communityUuid: string, + gradidoID: string, +): Promise { + return DbUser.findOne({ + where: { foreign: true, communityUuid, gradidoID }, + }) +} diff --git a/backend/src/util/TRANSACTIONS_LOCK.ts b/database/src/util/TRANSACTIONS_LOCK.ts similarity index 100% rename from backend/src/util/TRANSACTIONS_LOCK.ts rename to database/src/util/TRANSACTIONS_LOCK.ts diff --git a/backend/src/util/TRANSACTION_LINK_LOCK.ts b/database/src/util/TRANSACTION_LINK_LOCK.ts similarity index 100% rename from backend/src/util/TRANSACTION_LINK_LOCK.ts rename to database/src/util/TRANSACTION_LINK_LOCK.ts diff --git a/database/src/util/index.ts b/database/src/util/index.ts new file mode 100644 index 000000000..d2fde2126 --- /dev/null +++ b/database/src/util/index.ts @@ -0,0 +1,2 @@ +export * from './TRANSACTIONS_LOCK' +export * from './TRANSACTION_LINK_LOCK' diff --git a/e2e-tests/cypress.config.ts b/e2e-tests/cypress.config.ts index b182e7f32..254640e81 100644 --- a/e2e-tests/cypress.config.ts +++ b/e2e-tests/cypress.config.ts @@ -35,8 +35,8 @@ export default defineConfig({ excludeSpecPattern: '*.js', baseUrl: 'http://127.0.0.1:3000', chromeWebSecurity: false, - defaultCommandTimeout: 25000, - pageLoadTimeout: 24000, + defaultCommandTimeout: 50000, + pageLoadTimeout: 32000, supportFile: 'cypress/support/index.ts', viewportHeight: 720, viewportWidth: 1280, diff --git a/federation/src/client/1_0/AuthenticationClient.ts b/federation/src/client/1_0/AuthenticationClient.ts index 60a205aa7..ac14bcb84 100644 --- a/federation/src/client/1_0/AuthenticationClient.ts +++ b/federation/src/client/1_0/AuthenticationClient.ts @@ -3,7 +3,7 @@ import { GraphQLClient } from 'graphql-request' import { getLogger, Logger } from 'log4js' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs' +import { EncryptedTransferArgs } from 'core' import { authenticate } from './query/authenticate' import { openConnectionCallback } from './query/openConnectionCallback' diff --git a/federation/src/client/AuthenticationClientFactory.ts b/federation/src/client/AuthenticationClientFactory.ts index 29e55279d..94a74581a 100644 --- a/federation/src/client/AuthenticationClientFactory.ts +++ b/federation/src/client/AuthenticationClientFactory.ts @@ -3,7 +3,7 @@ import { FederatedCommunity as DbFederatedCommunity } from 'database' import { AuthenticationClient as V1_0_AuthenticationClient } from './1_0/AuthenticationClient' import { AuthenticationClient as V1_1_AuthenticationClient } from './1_1/AuthenticationClient' -import { ApiVersionType } from './enum/ApiVersionType' +import { ApiVersionType } from 'core' type AuthenticationClient = V1_0_AuthenticationClient | V1_1_AuthenticationClient diff --git a/federation/src/client/enum/ApiVersionType.ts b/federation/src/client/enum/ApiVersionType.ts deleted file mode 100644 index 60da9de57..000000000 --- a/federation/src/client/enum/ApiVersionType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ApiVersionType { - V1_0 = '1_0', - V1_1 = '1_1', -} diff --git a/federation/src/graphql/api/1_0/resolver/DisbursementResolver.ts b/federation/src/graphql/api/1_0/resolver/DisbursementResolver.ts new file mode 100644 index 000000000..2388aaec0 --- /dev/null +++ b/federation/src/graphql/api/1_0/resolver/DisbursementResolver.ts @@ -0,0 +1,54 @@ +import { getLogger } from "log4js" +import { Arg, Mutation, Resolver } from "type-graphql" + +import { LOG4JS_BASE_CATEGORY_NAME } from "@/config/const" +import { interpretEncryptedTransferArgs } from "core" +import { EncryptedTransferArgs } from "core" +import { DisburseJwtPayloadType } from "shared" +import { processXComCompleteTransaction } from "core" + +const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.DisbursementResolver.${method}`) + +@Resolver() +export class DisbursementResolver { + @Mutation(() => String) + async processDisburseJwtOnSenderCommunity( + @Arg('data') + args: EncryptedTransferArgs, + ): Promise { + const methodLogger = createLogger(`processDisburseJwtOnSenderCommunity`) + methodLogger.addContext('handshakeID', args.handshakeID) + if(methodLogger.isDebugEnabled()) { + methodLogger.debug(`processDisburseJwtOnSenderCommunity() via apiVersion=1_0 ...`, args) + } + const authArgs = await interpretEncryptedTransferArgs(args) as DisburseJwtPayloadType + if (!authArgs) { + const errmsg = `invalid disbursement payload of requesting community with publicKey` + args.publicKey + methodLogger.error(errmsg) + throw new Error(errmsg) + } + if(methodLogger.isDebugEnabled()) { + methodLogger.debug(`processDisburseJwtOnSenderCommunity() via apiVersion=1_0 ...`, authArgs) + } + let result = 'Disbursement of Redeem-Link failed!' + try { + if(await processXComCompleteTransaction( + authArgs.sendercommunityuuid, + authArgs.sendergradidoid, + authArgs.recipientcommunityuuid, + authArgs.recipientgradidoid, + authArgs.amount, + authArgs.memo, + authArgs.code, + authArgs.recipientfirstname, + authArgs.recipientalias, + )) { + result = 'Disbursement of Redeem-Link successfull!' + } + } catch (err) { + result = `Error in Disbursement of Redeem-Link: ` + err + methodLogger.error(result) + } + return result + } +} \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/schema.ts b/federation/src/graphql/api/1_0/schema.ts index 7b88fba5c..fcb05c803 100644 --- a/federation/src/graphql/api/1_0/schema.ts +++ b/federation/src/graphql/api/1_0/schema.ts @@ -1,9 +1,10 @@ import { NonEmptyArray } from 'type-graphql' import { AuthenticationResolver } from './resolver/AuthenticationResolver' +import { DisbursementResolver } from './resolver/DisbursementResolver' import { PublicCommunityInfoResolver } from './resolver/PublicCommunityInfoResolver' import { PublicKeyResolver } from './resolver/PublicKeyResolver' import { SendCoinsResolver } from './resolver/SendCoinsResolver' export const getApiResolvers = (): NonEmptyArray => { - return [AuthenticationResolver, PublicCommunityInfoResolver, PublicKeyResolver, SendCoinsResolver] + return [AuthenticationResolver, DisbursementResolver, PublicCommunityInfoResolver, PublicKeyResolver, SendCoinsResolver] } diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index 285625882..53a742313 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -340,10 +340,7 @@ function setRedeemJwtLinkInformation() { if (deepCopy) { // recipientUser is only set if the user is logged in if (store.state.gradidoID !== null) { - // console.log( - // 'TransactionLink.setRedeemJwtLinkInformation... gradidoID=', - // store.state.gradidoID, - // ) + // console.log('TransactionLink.setRedeemJwtLinkInformation... gradidoID=', store.state.gradidoID) deepCopy.queryTransactionLink.recipientUser = { __typename: 'User', gradidoID: store.state.gradidoID, @@ -362,11 +359,23 @@ function setRedeemJwtLinkInformation() { async function mutationLink(amount) { // console.log('TransactionLink.mutationLink... params=', params) + // console.log('TransactionLink.mutationLink... linkData.value=', linkData.value) + // console.log('TransactionLink.mutationLink... linkData=', linkData) if (isRedeemJwtLink.value) { // console.log('TransactionLink.mutationLink... trigger disbursement from recipient-community') try { await disburseMutate({ - code: params.code, + senderCommunityUuid: linkData.value.senderCommunity.uuid, + senderGradidoId: linkData.value.senderUser.gradidoID, + recipientCommunityUuid: linkData.value.recipientCommunity.uuid, + recipientCommunityName: linkData.value.recipientCommunity.name, + recipientGradidoId: linkData.value.recipientUser.gradidoID, + recipientFirstName: linkData.value.recipientUser.firstName, + code: linkData.value.code, + amount: linkData.value.amount, + memo: linkData.value.memo, + validUntil: linkData.value.validUntil, + recipientAlias: linkData.value.recipientUser.alias, }) toastSuccess(t('gdd_per_link.disbured', { n: amount })) await router.push('/overview') diff --git a/shared/src/index.ts b/shared/src/index.ts index afbe82bce..dd7965fdc 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -12,4 +12,5 @@ export * from './jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType' export * from './jwt/payloadtypes/RedeemJwtPayloadType' export * from './jwt/payloadtypes/SendCoinsJwtPayloadType' export * from './jwt/payloadtypes/SendCoinsResponseJwtPayloadType' +export * from './jwt/payloadtypes/SignedTransferPayloadType' diff --git a/shared/src/jwt/payloadtypes/DisburseJwtPayloadType.ts b/shared/src/jwt/payloadtypes/DisburseJwtPayloadType.ts index 2940161df..426e3f58a 100644 --- a/shared/src/jwt/payloadtypes/DisburseJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/DisburseJwtPayloadType.ts @@ -17,6 +17,7 @@ export class DisburseJwtPayloadType extends JwtPayloadType { recipientalias: string constructor( + handshakeID: string, senderCommunityUuid: string, senderGradidoId: string, recipientCommunityUuid: string, @@ -29,9 +30,7 @@ export class DisburseJwtPayloadType extends JwtPayloadType { validUntil: string, recipientAlias: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - super('handshakeID') - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + super(handshakeID) this.tokentype = DisburseJwtPayloadType.DISBURSE_ACTIVATION_TYPE this.sendercommunityuuid = senderCommunityUuid this.sendergradidoid = senderGradidoId diff --git a/shared/src/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts b/shared/src/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts index f353d885b..0d6d86476 100644 --- a/shared/src/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts @@ -9,9 +9,7 @@ export class EncryptedJWEJwtPayloadType extends JwtPayloadType { handshakeID: string, jwe: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call super(handshakeID) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE this.jwe = jwe } diff --git a/shared/src/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts b/shared/src/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts index 7b4992284..544e23800 100644 --- a/shared/src/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType.ts @@ -11,9 +11,7 @@ export class OpenConnectionCallbackJwtPayloadType extends JwtPayloadType { oneTimeCode: string, url: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call super(handshakeID) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = OpenConnectionCallbackJwtPayloadType.OPEN_CONNECTION_CALLBACK_TYPE this.oneTimeCode = oneTimeCode this.url = url diff --git a/shared/src/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts b/shared/src/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts index 3a3c249cb..a9e7e0a5a 100644 --- a/shared/src/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts @@ -9,9 +9,7 @@ export class OpenConnectionJwtPayloadType extends JwtPayloadType { handshakeID: string, url: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call super(handshakeID) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE this.url = url } diff --git a/shared/src/jwt/payloadtypes/RedeemJwtPayloadType.ts b/shared/src/jwt/payloadtypes/RedeemJwtPayloadType.ts index faeda2b71..841f725e1 100644 --- a/shared/src/jwt/payloadtypes/RedeemJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/RedeemJwtPayloadType.ts @@ -20,9 +20,7 @@ export class RedeemJwtPayloadType extends JwtPayloadType { memo: string, validUntil: string, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call super('handshakeID') - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE this.sendercommunityuuid = senderCom this.sendergradidoid = senderUser diff --git a/shared/src/jwt/payloadtypes/SendCoinsJwtPayloadType.ts b/shared/src/jwt/payloadtypes/SendCoinsJwtPayloadType.ts index 6048eb254..833b004fc 100644 --- a/shared/src/jwt/payloadtypes/SendCoinsJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/SendCoinsJwtPayloadType.ts @@ -26,9 +26,7 @@ export class SendCoinsJwtPayloadType extends JwtPayloadType { senderUserName: string, senderAlias?: string | null, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call super(handshakeID) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = SendCoinsJwtPayloadType.SEND_COINS_TYPE this.recipientCommunityUuid = recipientCommunityUuid this.recipientUserIdentifier = recipientUserIdentifier diff --git a/shared/src/jwt/payloadtypes/SendCoinsResponseJwtPayloadType.ts b/shared/src/jwt/payloadtypes/SendCoinsResponseJwtPayloadType.ts index e76cf00eb..fb08d6a97 100644 --- a/shared/src/jwt/payloadtypes/SendCoinsResponseJwtPayloadType.ts +++ b/shared/src/jwt/payloadtypes/SendCoinsResponseJwtPayloadType.ts @@ -17,9 +17,7 @@ export class SendCoinsResponseJwtPayloadType extends JwtPayloadType { recipLastName: string | null, recipAlias: string | null, ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call super(handshakeID) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.tokentype = SendCoinsResponseJwtPayloadType.SEND_COINS_RESPONSE_TYPE this.vote = vote this.recipGradidoID = recipGradidoID diff --git a/shared/src/jwt/payloadtypes/SignedTransferPayloadType.ts b/shared/src/jwt/payloadtypes/SignedTransferPayloadType.ts new file mode 100644 index 000000000..1b3f3a468 --- /dev/null +++ b/shared/src/jwt/payloadtypes/SignedTransferPayloadType.ts @@ -0,0 +1,19 @@ +import { JwtPayloadType } from './JwtPayloadType' + +export class SignedTransferPayloadType extends JwtPayloadType { + static SIGNED_TRANSFER_TYPE = 'signed-transfer' + + publicKey: string + jwt: string + + constructor( + publicKey: string, + jwt: string, + handshakeID: string, + ) { + super(handshakeID) + this.tokentype = SignedTransferPayloadType.SIGNED_TRANSFER_TYPE + this.publicKey = publicKey + this.jwt = jwt + } +}