From 5ea4aec9220656d5f76962b20599a4f2e836d77f Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 20 Sep 2025 15:21:55 +0200 Subject: [PATCH] simplify backend code for dlt connector calling --- .../apis/dltConnector/DltConnectorClient.ts | 7 +- .../dltConnector/enum/TransactionErrorType.ts | 14 - backend/src/apis/dltConnector/index.ts | 107 +++ .../AbstractTransactionToDlt.role.ts | 63 -- .../TransactionLinkDeleteToDlt.role.ts | 94 --- .../TransactionLinkToDlt.role.ts | 68 -- .../transactionToDlt/TransactionToDlt.role.ts | 105 --- .../transactionToDlt/UserToDlt.role.ts | 61 -- .../transactionToDlt.context.ts | 67 -- .../dltConnector/model/TransactionDraft.ts | 59 +- .../sendTransactionsToDltConnector.test.ts | 728 ------------------ .../sendTransactionsToDltConnector.ts | 69 -- .../graphql/resolver/ContributionResolver.ts | 36 +- .../graphql/resolver/TransactionResolver.ts | 21 +- backend/src/graphql/resolver/UserResolver.ts | 26 +- backend/src/index.ts | 2 +- dlt-connector/bun.lock | 2 +- dlt-connector/package.json | 2 +- .../client/backend/community.schema.test.ts | 2 + dlt-connector/src/client/hiero/HieroClient.ts | 17 +- .../src/data/KeyPairIdentifier.logic.ts | 8 +- dlt-connector/src/index.ts | 1 - .../KeyPairCalculation.context.test.ts | 99 +++ .../sendToHiero/CreationTransaction.role.ts | 7 +- .../RegisterAddressTransaction.role.test.ts | 35 + .../RegisterAddressTransaction.role.ts | 18 +- .../sendToHiero/SendToHiero.context.ts | 37 +- dlt-connector/src/schemas/account.schema.ts | 16 +- .../src/schemas/transaction.schema.test.ts | 69 +- .../src/schemas/transaction.schema.ts | 15 +- .../src/schemas/typeConverter.schema.test.ts | 41 +- .../src/schemas/typeConverter.schema.ts | 2 +- dlt-connector/src/schemas/typeGuard.schema.ts | 34 +- dlt-connector/src/server/index.test.ts | 75 ++ dlt-connector/src/server/index.ts | 37 +- 35 files changed, 639 insertions(+), 1405 deletions(-) delete mode 100644 backend/src/apis/dltConnector/enum/TransactionErrorType.ts create mode 100644 backend/src/apis/dltConnector/index.ts delete mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts delete mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkDeleteToDlt.role.ts delete mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts delete mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts delete mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts delete mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts delete mode 100644 backend/src/apis/dltConnector/sendTransactionsToDltConnector.test.ts delete mode 100644 backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts create mode 100644 dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts create mode 100644 dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts create mode 100644 dlt-connector/src/server/index.test.ts diff --git a/backend/src/apis/dltConnector/DltConnectorClient.ts b/backend/src/apis/dltConnector/DltConnectorClient.ts index 16c74ab0c..a5e162ba0 100644 --- a/backend/src/apis/dltConnector/DltConnectorClient.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.ts @@ -58,8 +58,11 @@ export class DltConnectorClient { * transmit transaction via dlt-connector to hiero * and update dltTransactionId of transaction in db with hiero transaction id */ - public async sendTransaction(input: TransactionDraft): Promise> { + public async sendTransaction(input: TransactionDraft): Promise> { logger.debug('transmit transaction or user to dlt connector', input) - return await this.client.create('/sendTransaction', input) + return await this.client.create<{ transactionId: string }>( + '/sendTransaction', + input + ) } } diff --git a/backend/src/apis/dltConnector/enum/TransactionErrorType.ts b/backend/src/apis/dltConnector/enum/TransactionErrorType.ts deleted file mode 100644 index 5a2c5485e..000000000 --- a/backend/src/apis/dltConnector/enum/TransactionErrorType.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Error Types for dlt-connector graphql responses - */ -export enum TransactionErrorType { - NOT_IMPLEMENTED_YET = 'Not Implemented yet', - MISSING_PARAMETER = 'Missing parameter', - ALREADY_EXIST = 'Already exist', - DB_ERROR = 'DB Error', - PROTO_DECODE_ERROR = 'Proto Decode Error', - PROTO_ENCODE_ERROR = 'Proto Encode Error', - INVALID_SIGNATURE = 'Invalid Signature', - LOGIC_ERROR = 'Logic Error', - NOT_FOUND = 'Not found', -} diff --git a/backend/src/apis/dltConnector/index.ts b/backend/src/apis/dltConnector/index.ts new file mode 100644 index 000000000..7780006e9 --- /dev/null +++ b/backend/src/apis/dltConnector/index.ts @@ -0,0 +1,107 @@ +import { IRestResponse } from 'typed-rest-client' +import { DltTransactionType } from './enum/DltTransactionType' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { DltConnectorClient } from './DltConnectorClient' +import { + Community as DbCommunity, + Contribution as DbContribution, + DltTransaction as DbDltTransaction, + User as DbUser, + getHomeCommunity, +} from 'database' +import { TransactionDraft } from './model/TransactionDraft' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector`) +// will be undefined if dlt connect is disabled +const dltConnectorClient = DltConnectorClient.getInstance() + +async function checkDltConnectorResult(dltTransaction: DbDltTransaction, clientResponse: Promise>) + : Promise { + // check result from dlt connector + try { + const response = await clientResponse + if (response.statusCode === 200 && response.result) { + dltTransaction.messageId = response.result.transactionId + } else { + dltTransaction.error = `empty result with status code ${response.statusCode}` + logger.error('error from dlt-connector', response) + } + } catch (e) { + logger.debug(e) + if (e instanceof Error) { + dltTransaction.error = e.message + } else if (typeof e === 'string') { + dltTransaction.error = e + } else { + throw e + } + } + return dltTransaction +} + +/** + * send register address transaction via dlt-connector to hiero + * and update dltTransactionId of transaction in db with hiero transaction id + */ +export async function registerAddressTransaction(user: DbUser, community: DbCommunity): Promise { + if (!user.id) { + logger.error(`missing id for user: ${user.gradidoID}, please call registerAddressTransaction after user.save()`) + return null + } + // return null if some data where missing and log error + const draft = TransactionDraft.createRegisterAddress(user, community) + if (draft && dltConnectorClient) { + const clientResponse = dltConnectorClient.sendTransaction(draft) + let dltTransaction = new DbDltTransaction() + dltTransaction.typeId = DltTransactionType.REGISTER_ADDRESS + if (user.id) { + dltTransaction.userId = user.id + } + dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse) + return await dltTransaction.save() + } + return null +} + +export async function contributionTransaction( + contribution: DbContribution, + signingUser: DbUser, + createdAt: Date, +): Promise { + const homeCommunity = await getHomeCommunity() + if (!homeCommunity) { + logger.error('home community not found') + return null + } + const draft = TransactionDraft.createContribution(contribution, createdAt, signingUser, homeCommunity) + if (draft && dltConnectorClient) { + const clientResponse = dltConnectorClient.sendTransaction(draft) + let dltTransaction = new DbDltTransaction() + dltTransaction.typeId = DltTransactionType.CREATION + dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse) + return await dltTransaction.save() + } + return null +} + +export async function transferTransaction( + senderUser: DbUser, + recipientUser: DbUser, + amount: string, + memo: string, + createdAt: Date +): Promise { + + const draft = TransactionDraft.createTransfer(senderUser, recipientUser, amount, memo, createdAt) + if (draft && dltConnectorClient) { + const clientResponse = dltConnectorClient.sendTransaction(draft) + let dltTransaction = new DbDltTransaction() + dltTransaction.typeId = DltTransactionType.TRANSFER + dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse) + return await dltTransaction.save() + } + return null +} + + diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts deleted file mode 100644 index 22f7104b2..000000000 --- a/backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts +++ /dev/null @@ -1,63 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from 'typeorm' -import { DltTransaction } from 'database' - -import { TransactionDraft } from '@dltConnector/model/TransactionDraft' -import { Logger } from 'log4js' - -export abstract class AbstractTransactionToDltRole { - protected self: T | null - - // public interface - public abstract initWithLast(): Promise - public abstract getTimestamp(): number - public abstract convertToGraphqlInput(): TransactionDraft - - - public constructor(protected logger: Logger) {} - - public getEntity(): T | null { - return this.self - } - - public async saveTransactionResult(messageId: string, error: string | null): Promise { - const dltTransaction = DltTransaction.create() - dltTransaction.messageId = messageId - dltTransaction.error = error - this.setJoinIdAndType(dltTransaction) - await DltTransaction.save(dltTransaction) - if (dltTransaction.error) { - this.logger.error( - `Store dltTransaction with error: id=${dltTransaction.id}, error=${dltTransaction.error}`, - ) - } else { - this.logger.info( - `Store dltTransaction: messageId=${dltTransaction.messageId}, id=${dltTransaction.id}`, - ) - } - } - - // intern - protected abstract setJoinIdAndType(dltTransaction: DltTransaction): void - - // helper - protected createQueryForPendingItems( - qb: SelectQueryBuilder, - joinCondition: string, - orderBy: OrderByCondition, - ): SelectQueryBuilder { - return qb - .leftJoin(DltTransaction, 'dltTransaction', joinCondition) - .where('dltTransaction.user_id IS NULL') - .andWhere('dltTransaction.transaction_id IS NULL') - .andWhere('dltTransaction.transaction_link_Id IS NULL') - .orderBy(orderBy) - } - - protected createDltTransactionEntry(messageId: string, error: string | null): DltTransaction { - const dltTransaction = DltTransaction.create() - dltTransaction.messageId = messageId - dltTransaction.error = error - return dltTransaction - } -} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkDeleteToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkDeleteToDlt.role.ts deleted file mode 100644 index d4964a9f8..000000000 --- a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkDeleteToDlt.role.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { DltTransaction, TransactionLink } from 'database' - -import { DltTransactionType } from '@dltConnector/enum/DltTransactionType' -import { TransactionType } from '@dltConnector/enum/TransactionType' -import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed' -import { TransactionDraft } from '@dltConnector/model/TransactionDraft' -import { AccountIdentifier } from '@dltConnector/model/AccountIdentifier' - -import { LogError } from '@/server/LogError' - -import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' -import { CommunityAccountIdentifier } from '@dltConnector/model/CommunityAccountIdentifier' - -/** - * redeem deferred transfer transaction by creator, so "deleting" it - */ -export class TransactionLinkDeleteToDltRole extends AbstractTransactionToDltRole { - async initWithLast(): Promise { - const queryBuilder = this.createQueryForPendingItems( - TransactionLink - .createQueryBuilder() - .leftJoinAndSelect('TransactionLink.user', 'user') - .leftJoinAndSelect('user.community', 'community'), - 'TransactionLink.id = dltTransaction.transactionLinkId and dltTransaction.type_id <> 4', - // eslint-disable-next-line camelcase - { TransactionLink_deletedAt: 'ASC', User_id: 'ASC' }, - ) - .andWhere('TransactionLink.deletedAt IS NOT NULL') - .withDeleted() - /* - const queryBuilder2 = TransactionLink.createQueryBuilder() - .leftJoinAndSelect('TransactionLink.user', 'user') - .where('TransactionLink.deletedAt IS NOT NULL') - .andWhere(() => { - const subQuery = DltTransaction.createQueryBuilder() - .select('1') - .where('DltTransaction.transaction_link_id = TransactionLink.id') - .andWhere('DltTransaction.type_id = :typeId', { - typeId: DltTransactionType.DELETE_DEFERRED_TRANSFER, - }) - .getQuery() - return `NOT EXIST (${subQuery})` - }) - .withDeleted() - // eslint-disable-next-line camelcase - .orderBy({ TransactionLink_deletedAt: 'ASC', User_id: 'ASC' }) - */ - // console.log('query: ', queryBuilder.getSql()) - this.self = await queryBuilder.getOne() - return this - } - - public getTimestamp(): number { - if (!this.self) { - return Infinity - } - if (!this.self.deletedAt) { - throw new LogError('not deleted transaction link selected') - } - return this.self.deletedAt.getTime() - } - - public convertToGraphqlInput(): TransactionDraft { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction link') - } - if (!this.self.deletedAt) { - throw new LogError('not deleted transaction link selected') - } - const draft = new TransactionDraft() - draft.amount = this.self.amount.abs().toString() - const user = this.self.user - if (!user.community) { - throw new LogError(`missing community for user ${user.id}`) - } - const topicId = user.community.hieroTopicId - if (!topicId) { - throw new LogError(`missing topicId for community ${user.community.id}`) - } - draft.user = new AccountIdentifier(topicId, new IdentifierSeed(this.self.code)) - draft.linkedUser = new AccountIdentifier(topicId, new CommunityAccountIdentifier(user.gradidoID)) - draft.createdAt = this.self.deletedAt.toISOString() - draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER - return draft - } - - protected setJoinIdAndType(dltTransaction: DltTransaction): void { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction link') - } - dltTransaction.transactionLinkId = this.self.id - dltTransaction.typeId = DltTransactionType.DELETE_DEFERRED_TRANSFER - } -} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts deleted file mode 100644 index 05f9d319e..000000000 --- a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { DltTransaction, TransactionLink } from 'database' - -import { DltTransactionType } from '@dltConnector/enum/DltTransactionType' -import { TransactionType } from '@dltConnector/enum/TransactionType' -import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed' -import { TransactionDraft } from '@dltConnector/model/TransactionDraft' -import { AccountIdentifier } from '@dltConnector/model/AccountIdentifier' - -import { LogError } from '@/server/LogError' - -import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' -import { CommunityAccountIdentifier } from '../../model/CommunityAccountIdentifier' - -/** - * send transactionLink as Deferred Transfers - */ -export class TransactionLinkToDltRole extends AbstractTransactionToDltRole { - async initWithLast(): Promise { - this.self = await this.createQueryForPendingItems( - TransactionLink - .createQueryBuilder() - .leftJoinAndSelect('TransactionLink.user', 'user') - .leftJoinAndSelect('user.community', 'community'), - 'TransactionLink.id = dltTransaction.transactionLinkId', - // eslint-disable-next-line camelcase - { TransactionLink_createdAt: 'ASC', User_id: 'ASC' }, - ).getOne() - return this - } - - public getTimestamp(): number { - if (!this.self) { - return Infinity - } - return this.self.createdAt.getTime() - } - - public convertToGraphqlInput(): TransactionDraft { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction link') - } - const draft = new TransactionDraft() - draft.amount = this.self.amount.abs().toString() - const user = this.self.user - if (!user.community) { - throw new LogError(`missing community for user ${user.id}`) - } - const topicId = user.community.hieroTopicId - if (!topicId) { - throw new LogError(`missing topicId for community ${user.community.id}`) - } - draft.user = new AccountIdentifier(topicId, new CommunityAccountIdentifier(user.gradidoID, 1)) - draft.linkedUser = new AccountIdentifier(topicId, new IdentifierSeed(this.self.code)) - draft.createdAt = this.self.createdAt.toISOString() - draft.timeoutDuration = (this.self.validUntil.getTime() - this.self.createdAt.getTime()) / 1000 - draft.memo = this.self.memo - draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER - return draft - } - - protected setJoinIdAndType(dltTransaction: DltTransaction): void { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction link') - } - dltTransaction.transactionLinkId = this.self.id - dltTransaction.typeId = DltTransactionType.DEFERRED_TRANSFER - } -} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts deleted file mode 100644 index 63f25d22d..000000000 --- a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { DltTransaction, Transaction } from 'database' - -import { DltTransactionType } from '@dltConnector/enum/DltTransactionType' -import { TransactionType } from '@dltConnector/enum/TransactionType' -import { CommunityUser } from '@dltConnector/model/CommunityUser' -import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed' -import { TransactionDraft } from '@dltConnector/model/TransactionDraft' -import { UserIdentifier } from '@/apis/dltConnector/model/AccountIdentifier' - -import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' -import { LogError } from '@/server/LogError' - -import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' - -/** - * send transfer and creations transactions to dlt connector as GradidoTransfer and GradidoCreation - */ -export class TransactionToDltRole extends AbstractTransactionToDltRole { - private type: DltTransactionType - async initWithLast(): Promise { - this.self = await this.createQueryForPendingItems( - Transaction.createQueryBuilder().leftJoinAndSelect( - 'Transaction.transactionLink', - 'transactionLink', - ), - 'Transaction.id = dltTransaction.transactionId', - // eslint-disable-next-line camelcase - { balance_date: 'ASC', Transaction_id: 'ASC' }, - ) - // we don't need the receive transactions, there contain basically the same data as the send transactions - .andWhere('Transaction.type_id <> :typeId', { typeId: TransactionTypeId.RECEIVE }) - .getOne() - return this - } - - public getTimestamp(): number { - if (!this.self) { - return Infinity - } - return this.self.balanceDate.getTime() - } - - public convertToGraphqlInput(): TransactionDraft { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction') - } - const draft = new TransactionDraft() - draft.amount = this.self.amount.abs().toString() - - switch (this.self.typeId as TransactionTypeId) { - case TransactionTypeId.CREATION: - draft.type = TransactionType.GRADIDO_CREATION - this.type = DltTransactionType.CREATION - break - case TransactionTypeId.SEND: - case TransactionTypeId.RECEIVE: - draft.type = TransactionType.GRADIDO_TRANSFER - this.type = DltTransactionType.TRANSFER - break - default: - this.type = DltTransactionType.UNKNOWN - throw new LogError('wrong role for type', this.self.typeId as TransactionTypeId) - } - if ( - !this.self.linkedUserGradidoID || - !this.self.linkedUserCommunityUuid || - !this.self.userCommunityUuid - ) { - throw new LogError( - `missing necessary field in transaction: ${this.self.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`, - ) - } - // it is a redeem of a transaction link? - const transactionLink = this.self.transactionLink - if (transactionLink) { - draft.user = new UserIdentifier( - this.self.userCommunityUuid, - new IdentifierSeed(transactionLink.code), - ) - draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER - this.type = DltTransactionType.REDEEM_DEFERRED_TRANSFER - } else { - draft.user = new UserIdentifier( - this.self.userCommunityUuid, - new CommunityUser(this.self.userGradidoID, 1), - ) - } - draft.linkedUser = new UserIdentifier( - this.self.linkedUserCommunityUuid, - new CommunityUser(this.self.linkedUserGradidoID, 1), - ) - draft.memo = this.self.memo - draft.createdAt = this.self.balanceDate.toISOString() - draft.targetDate = this.self.creationDate?.toISOString() - return draft - } - - protected setJoinIdAndType(dltTransaction: DltTransaction): void { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction') - } - dltTransaction.transactionId = this.self.id - dltTransaction.typeId = this.type - } -} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts deleted file mode 100644 index d302a36b3..000000000 --- a/backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { DltTransaction, User } from 'database' - -import { AccountType } from '@dltConnector/enum/AccountType' -import { DltTransactionType } from '@dltConnector/enum/DltTransactionType' -import { TransactionType } from '@dltConnector/enum/TransactionType' -import { TransactionDraft } from '@dltConnector/model/TransactionDraft' -import { AccountIdentifier } from '@/apis/dltConnector/model/AccountIdentifier' - -import { LogError } from '@/server/LogError' - -import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' -import { CommunityAccountIdentifier } from '../../model/CommunityAccountIdentifier' - -/** - * send new user to dlt connector, will be made to RegisterAddress Transaction - */ -export class UserToDltRole extends AbstractTransactionToDltRole { - async initWithLast(): Promise { - this.self = await this.createQueryForPendingItems( - User.createQueryBuilder().leftJoinAndSelect('User.community', 'community'), - 'User.id = dltTransaction.userId', - // eslint-disable-next-line camelcase - { User_created_at: 'ASC', User_id: 'ASC' }, - ).getOne() - return this - } - - public getTimestamp(): number { - if (!this.self) { - return Infinity - } - return this.self.createdAt.getTime() - } - - public convertToGraphqlInput(): TransactionDraft { - if (!this.self) { - throw new LogError('try to create dlt entry for empty transaction') - } - if (!this.self.community) { - throw new LogError(`missing community for user ${this.self.id}`) - } - const topicId = this.self.community.hieroTopicId - if (!topicId) { - throw new LogError(`missing topicId for community ${this.self.community.id}`) - } - const draft = new TransactionDraft() - draft.user = new AccountIdentifier(topicId, new CommunityAccountIdentifier(this.self.gradidoID)) - draft.createdAt = this.self.createdAt.toISOString() - draft.accountType = AccountType.COMMUNITY_HUMAN - draft.type = TransactionType.REGISTER_ADDRESS - return draft - } - - protected setJoinIdAndType(dltTransaction: DltTransaction): void { - if (!this.self) { - throw new LogError('try to create dlt entry for empty user') - } - dltTransaction.userId = this.self.id - dltTransaction.typeId = DltTransactionType.REGISTER_ADDRESS - } -} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts deleted file mode 100644 index cb1d8cca2..000000000 --- a/backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Transaction, TransactionLink, User } from 'database' - -import { DltConnectorClient } from '@/apis/dltConnector/DltConnectorClient' - -import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' -import { TransactionLinkDeleteToDltRole } from './TransactionLinkDeleteToDlt.role' -import { TransactionLinkToDltRole } from './TransactionLinkToDlt.role' -import { TransactionToDltRole } from './TransactionToDlt.role' -import { UserToDltRole } from './UserToDlt.role' -import { getLogger, Logger } from 'log4js' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' - -/** - * @DCI-Context - * Context for sending transactions to dlt connector, always the oldest not sended transaction first - */ -export async function transactionToDlt(dltConnector: DltConnectorClient): Promise { - async function findNextPendingTransaction(logger: Logger): Promise< - AbstractTransactionToDltRole - > { - // collect each oldest not sended entity from db and choose oldest - const results = await Promise.all([ - new TransactionToDltRole(logger).initWithLast(), - new UserToDltRole(logger).initWithLast(), - new TransactionLinkToDltRole(logger).initWithLast(), - new TransactionLinkDeleteToDltRole(logger).initWithLast(), - ]) - - // sort array to get oldest at first place - results.sort((a, b) => { - return a.getTimestamp() - b.getTimestamp() - }) - return results[0] - } - - const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector.interaction.transactionToDlt`) - while (true) { - const pendingTransactionRole = await findNextPendingTransaction(logger) - const pendingTransaction = pendingTransactionRole.getEntity() - if (!pendingTransaction) { - break - } - let messageId = '' - let error: string | null = null - try { - const result = await dltConnector.sendTransaction( - pendingTransactionRole.convertToGraphqlInput() - ) - if (result.statusCode === 200 && result.result) { - messageId = result.result - } else { - error = `empty result with status code ${result.statusCode}` - logger.error('error from dlt-connector', result) - } - } catch (e) { - logger.debug(e) - if (e instanceof Error) { - error = e.message - } else if (typeof e === 'string') { - error = e - } else { - throw e - } - } - await pendingTransactionRole.saveTransactionResult(messageId, error) - } -} diff --git a/backend/src/apis/dltConnector/model/TransactionDraft.ts b/backend/src/apis/dltConnector/model/TransactionDraft.ts index 8dba79bdc..1342a3f8f 100755 --- a/backend/src/apis/dltConnector/model/TransactionDraft.ts +++ b/backend/src/apis/dltConnector/model/TransactionDraft.ts @@ -3,6 +3,12 @@ import { AccountType } from '@dltConnector/enum/AccountType' import { TransactionType } from '@dltConnector/enum/TransactionType' import { AccountIdentifier } from './AccountIdentifier' +import { Community as DbCommunity, Contribution as DbContribution, User as DbUser } from 'database' +import { CommunityAccountIdentifier } from './CommunityAccountIdentifier' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector.model.TransactionDraft`) export class TransactionDraft { user: AccountIdentifier @@ -19,4 +25,55 @@ export class TransactionDraft { timeoutDuration?: number // only for register address accountType?: AccountType -} + + static createRegisterAddress(user: DbUser, community: DbCommunity): TransactionDraft | null { + if (community.hieroTopicId) { + const draft = new TransactionDraft() + draft.user = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(user.gradidoID)) + draft.type = TransactionType.REGISTER_ADDRESS + draft.createdAt = user.createdAt.toISOString() + draft.accountType = AccountType.COMMUNITY_HUMAN + return draft + } else { + logger.warn(`missing topicId for community ${community.id}`) + } + return null + } + + static createContribution(contribution: DbContribution, createdAt: Date, signingUser: DbUser, community: DbCommunity): TransactionDraft | null { + if (community.hieroTopicId) { + const draft = new TransactionDraft() + draft.user = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(contribution.user.gradidoID)) + draft.linkedUser = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(signingUser.gradidoID)) + draft.type = TransactionType.GRADIDO_CREATION + draft.createdAt = createdAt.toISOString() + draft.amount = contribution.amount.toString() + draft.memo = contribution.memo + draft.targetDate = contribution.contributionDate.toISOString() + return draft + } else { + logger.warn(`missing topicId for community ${community.id}`) + } + return null + } + + 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 + } + 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}`) + } + return null + } +} \ No newline at end of file diff --git a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.test.ts b/backend/src/apis/dltConnector/sendTransactionsToDltConnector.test.ts deleted file mode 100644 index 50af15e9a..000000000 --- a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.test.ts +++ /dev/null @@ -1,728 +0,0 @@ -import { Community, DltTransaction, Transaction } from 'database' -import { Decimal } from 'decimal.js-light' -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 '@/graphql/enum/TransactionTypeId' -import { creations } from '@/seeds/creation' -import { creationFactory } from '@/seeds/factory/creation' -import { userFactory } from 'database/src/seeds/factory/user' -import { bibiBloxberg } from 'database/src/seeds/users/bibi-bloxberg' -import { bobBaumeister } from 'database/src/seeds/users/bob-baumeister' -import { peterLustig } from 'database/src/seeds/users/peter-lustig' -import { raeuberHotzenplotz } from 'database/src/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`, -) - -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: { - 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 - - 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/apis/dltConnector/sendTransactionsToDltConnector.ts b/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts deleted file mode 100644 index ef58ed606..000000000 --- a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts +++ /dev/null @@ -1,69 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { CONFIG } from '@/config' -import { TypeORMError } from 'typeorm' -// eslint-disable-next-line import/named, n/no-extraneous-import -import { FetchError } from 'node-fetch' - -import { DltConnectorClient } from '@dltConnector/DltConnectorClient' - -import { LogError } from '@/server/LogError' -import { - InterruptiveSleepManager, - TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, -} from '@/util/InterruptiveSleepManager' - -import { transactionToDlt } from './interaction/transactionToDlt/transactionToDlt.context' -import { getLogger } from 'log4js' -import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' - -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector.sendTransactionsToDltConnector`) - -let isLoopRunning = true - -export const stopSendTransactionsToDltConnector = (): void => { - isLoopRunning = false -} - -export async function sendTransactionsToDltConnector(): Promise { - const dltConnector = DltConnectorClient.getInstance() - - if (!dltConnector) { - logger.info('currently not configured...') - isLoopRunning = false - return - } - logger.info('task started') - - // define outside of loop for reuse and reducing gb collection - // const queries = getFindNextPendingTransactionQueries() - - // eslint-disable-next-line no-unmodified-loop-condition - while (isLoopRunning) { - try { - // return after no pending transactions are left - await transactionToDlt(dltConnector) - await InterruptiveSleepManager.getInstance().sleep( - TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, - // TODO: put sleep time into config, because it influence performance, - // transactionToDlt call 4 db queries to look for new transactions - CONFIG.PRODUCTION ? 100000 : 1000, - ) - } catch (e) { - // couldn't connect to dlt-connector? We wait - if (e instanceof FetchError) { - logger.error(`error connecting dlt-connector, wait 5 seconds before retry: ${String(e)}`) - await InterruptiveSleepManager.getInstance().sleep( - TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, - 5000, - ) - } else { - if (e instanceof TypeORMError) { - // seems to be a error in code, so let better stop here - throw new LogError(e.message, e.stack) - } else { - logger.error(`Error while sending to DLT-connector or writing messageId`, e) - } - } - } - } -} diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index f5ed2f1d2..34ea8b1da 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -2,6 +2,7 @@ import { Contribution as DbContribution, Transaction as DbTransaction, User as DbUser, + DltTransaction as DbDltTransaction, UserContact, } from 'database' import { Decimal } from 'decimal.js-light' @@ -60,6 +61,7 @@ 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 { contributionTransaction } from '@/apis/dltConnector' const db = AppDatabase.getInstance() const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`) @@ -436,11 +438,13 @@ export class ContributionResolver { const logger = createLogger() logger.addContext('contribution', id) + let transaction: DbTransaction + let dltTransactionPromise: Promise // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() try { const clientTimezoneOffset = getClientTimezoneOffset(context) - const contribution = await DbContribution.findOne({ where: { id } }) + const contribution = await DbContribution.findOne({ where: { id }, relations: {user: {emailContact: true}} }) if (!contribution) { throw new LogError('Contribution not found', id) } @@ -450,18 +454,18 @@ export class ContributionResolver { if (contribution.contributionStatus === 'DENIED') { throw new LogError('Contribution already denied', id) } + const moderatorUser = getUser(context) if (moderatorUser.id === contribution.userId) { throw new LogError('Moderator can not confirm own contribution') } - const user = await DbUser.findOneOrFail({ - where: { id: contribution.userId }, - withDeleted: true, - relations: ['emailContact'], - }) + const user = contribution.user if (user.deletedAt) { throw new LogError('Can not confirm contribution since the user was deleted') } + const receivedCallDate = new Date() + dltTransactionPromise = contributionTransaction(contribution, moderatorUser, receivedCallDate) + const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false) validateContribution( creations, @@ -469,8 +473,7 @@ export class ContributionResolver { contribution.contributionDate, clientTimezoneOffset, ) - - const receivedCallDate = new Date() + const queryRunner = db.getDataSource().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') @@ -491,7 +494,7 @@ export class ContributionResolver { } newBalance = newBalance.add(contribution.amount.toString()) - const transaction = new DbTransaction() + transaction = new DbTransaction() transaction.typeId = TransactionTypeId.CREATION transaction.memo = contribution.memo transaction.userId = contribution.userId @@ -509,7 +512,7 @@ export class ContributionResolver { transaction.balanceDate = receivedCallDate transaction.decay = decay ? decay.decay : new Decimal(0) transaction.decayStart = decay ? decay.start : null - await queryRunner.manager.insert(DbTransaction, transaction) + transaction = await queryRunner.manager.save(DbTransaction, transaction) contribution.confirmedAt = receivedCallDate contribution.confirmedBy = moderatorUser.id @@ -519,9 +522,6 @@ export class ContributionResolver { await queryRunner.commitTransaction() - // notify dlt-connector loop for new work - InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) - logger.info('creation commited successfuly.') await sendContributionConfirmedEmail({ firstName: user.firstName, @@ -547,6 +547,16 @@ export class ContributionResolver { } finally { releaseLock() } + // update transaction id in dlt transaction tables + // wait for finishing transaction by dlt-connector/hiero + const startTime = new Date() + const dltTransaction = await dltTransactionPromise + if(dltTransaction) { + dltTransaction.transactionId = transaction.id + await dltTransaction.save() + } + const endTime = new Date() + logger.debug(`dlt-connector contribution finished in ${endTime.getTime() - startTime.getTime()} ms`) return true } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index b14ef2a4e..33d34d928 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -2,6 +2,7 @@ import { AppDatabase, countOpenPendingTransactions, Community as DbCommunity, + DltTransaction as DbDltTransaction, PendingTransaction as DbPendingTransaction, Transaction as dbTransaction, TransactionLink as dbTransactionLink, @@ -54,6 +55,7 @@ import { } from './util/processXComSendCoins' import { storeForeignUser } from './util/storeForeignUser' import { transactionLinkSummary } from './util/transactionLinkSummary' +import { transferTransaction } from '@/apis/dltConnector' const db = AppDatabase.getInstance() const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`) @@ -66,6 +68,12 @@ export const executeTransaction = async ( logger: Logger, transactionLink?: dbTransactionLink | null, ): Promise => { + const receivedCallDate = new Date() + let dltTransactionPromise: Promise = Promise.resolve(null) + if (!transactionLink) { + dltTransactionPromise = transferTransaction(sender, recipient, amount.toString(), memo, receivedCallDate) + } + // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() @@ -82,8 +90,7 @@ export const executeTransaction = async ( throw new LogError('Sender and Recipient are the same', sender.id) } - // validate amount - const receivedCallDate = new Date() + // validate amount const sendBalance = await calculateBalance( sender.id, amount.mul(-1), @@ -163,7 +170,12 @@ export const executeTransaction = async ( } await queryRunner.commitTransaction() - logger.info(`commit Transaction successful...`) + // update dltTransaction with transactionId + const dltTransaction = await dltTransactionPromise + if (dltTransaction) { + dltTransaction.transactionId = transactionSend.id + await dltTransaction.save() + } await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount) @@ -179,8 +191,9 @@ export const executeTransaction = async ( } finally { await queryRunner.release() } + // notify dlt-connector loop for new work - InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) + // InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) await sendTransactionReceivedEmail({ firstName: recipient.firstName, lastName: recipient.lastName, diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2e6667802..385811dba 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -1,6 +1,7 @@ import { AppDatabase, ContributionLink as DbContributionLink, + DltTransaction as DbDltTransaction, TransactionLink as DbTransactionLink, User as DbUser, UserContact as DbUserContact, @@ -85,10 +86,6 @@ 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 { - InterruptiveSleepManager, - TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, -} from '@/util/InterruptiveSleepManager' import { delay } from '@/util/utilities' import random from 'random-bigint' @@ -108,6 +105,7 @@ import { deleteUserRole, setUserRole } from './util/modifyUserRole' import { sendUserToGms } from './util/sendUserToGms' import { syncHumhub } from './util/syncHumhub' import { validateAlias } from 'core' +import { registerAddressTransaction } from '@/apis/dltConnector' const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' @@ -391,6 +389,7 @@ export class UserResolver { if (homeCom.communityUuid) { dbUser.communityUuid = homeCom.communityUuid } + dbUser.gradidoID = gradidoID dbUser.firstName = firstName dbUser.lastName = lastName @@ -401,8 +400,11 @@ export class UserResolver { dbUser.alias = alias } dbUser.publisherId = publisherId ?? 0 - dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD - logger.debug('new dbUser', new UserLoggingView(dbUser)) + dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD + + if(logger.isDebugEnabled()) { + logger.debug('new dbUser', new UserLoggingView(dbUser)) + } if (redeemCode) { if (redeemCode.match(/^CL-/)) { const contributionLink = await DbContributionLink.findOne({ @@ -438,7 +440,7 @@ export class UserResolver { dbUser.emailContact = emailContact dbUser.emailId = emailContact.id - await queryRunner.manager.save(dbUser).catch((error) => { + dbUser = await queryRunner.manager.save(dbUser).catch((error) => { throw new LogError('Error while updating dbUser', error) }) @@ -470,6 +472,8 @@ export class UserResolver { } finally { await queryRunner.release() } + // register user into blockchain + const dltTransactionPromise = registerAddressTransaction(dbUser, homeCom) logger.info('createUser() successful...') if (CONFIG.HUMHUB_ACTIVE) { let spaceId: number | null = null @@ -483,9 +487,6 @@ export class UserResolver { } } - // notify dlt-connector loop for new work - InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) - if (redeemCode) { eventRegisterRedeem.affectedUser = dbUser eventRegisterRedeem.actingUser = dbUser @@ -509,6 +510,11 @@ export class UserResolver { } } } + // wait for finishing dlt transaction + const startTime = new Date() + await dltTransactionPromise + const endTime = new Date() + logger.info(`dlt-connector register address finished in ${endTime.getTime() - startTime.getTime()} ms`) return new User(dbUser) } diff --git a/backend/src/index.ts b/backend/src/index.ts index a810b64a9..8a46cbb35 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -24,7 +24,7 @@ async function main() { // task is running the whole time for transmitting transaction via dlt-connector to iota // can be notified with InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) // that a new transaction or user was stored in db - void sendTransactionsToDltConnector() + // void sendTransactionsToDltConnector() void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER)) } diff --git a/dlt-connector/bun.lock b/dlt-connector/bun.lock index 05852adda..93b3943c1 100644 --- a/dlt-connector/bun.lock +++ b/dlt-connector/bun.lock @@ -20,7 +20,7 @@ "log4js": "^6.9.1", "typescript": "^5.8.3", "uuid": "^8.3.2", - "valibot": "^1.1.0", + "valibot": "1.1.0", }, }, }, diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 26762c39e..bb7269703 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -33,7 +33,7 @@ "log4js": "^6.9.1", "typescript": "^5.8.3", "uuid": "^8.3.2", - "valibot": "^1.1.0" + "valibot": "1.1.0" }, "engines": { "node": ">=18" diff --git a/dlt-connector/src/client/backend/community.schema.test.ts b/dlt-connector/src/client/backend/community.schema.test.ts index e56056377..180ab9d5a 100644 --- a/dlt-connector/src/client/backend/community.schema.test.ts +++ b/dlt-connector/src/client/backend/community.schema.test.ts @@ -11,12 +11,14 @@ describe('community.schema', () => { uuid: '4f28e081-5c39-4dde-b6a4-3bde71de8d65', hieroTopicId: '0.0.4', foreign: false, + name: 'Test', creationDate: '2021-01-01', }), ).toEqual({ hieroTopicId: v.parse(hieroIdSchema, '0.0.4'), uuid: v.parse(uuidv4Schema, '4f28e081-5c39-4dde-b6a4-3bde71de8d65'), foreign: false, + name: 'Test', creationDate: new Date('2021-01-01'), }) }) diff --git a/dlt-connector/src/client/hiero/HieroClient.ts b/dlt-connector/src/client/hiero/HieroClient.ts index 9c48800d8..0815437ef 100644 --- a/dlt-connector/src/client/hiero/HieroClient.ts +++ b/dlt-connector/src/client/hiero/HieroClient.ts @@ -2,10 +2,8 @@ import { AccountBalance, AccountBalanceQuery, Client, - Key, LocalProvider, PrivateKey, - Timestamp, TopicCreateTransaction, TopicId, TopicInfoQuery, @@ -59,6 +57,7 @@ export class HieroClient { topicId: HieroId, transaction: GradidoTransaction, ): Promise<{ receipt: TransactionReceipt; response: TransactionResponse }> { + let startTime = new Date() this.logger.addContext('topicId', topicId.toString()) const serializedTransaction = transaction.getSerializedTransaction() if (!serializedTransaction) { @@ -69,13 +68,27 @@ export class HieroClient { topicId, message: serializedTransaction.data(), }).freezeWithSigner(this.wallet) + let endTime = new Date() + this.logger.info(`prepare message, until freeze, cost: ${endTime.getTime() - startTime.getTime()}ms`) + startTime = new Date() const signedHieroTransaction = await hieroTransaction.signWithSigner(this.wallet) + endTime = new Date() + this.logger.info(`sign message, cost: ${endTime.getTime() - startTime.getTime()}ms`) + startTime = new Date() const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet) + endTime = new Date() + this.logger.info(`send message, cost: ${endTime.getTime() - startTime.getTime()}ms`) + startTime = new Date() const sendReceipt = await sendResponse.getReceiptWithSigner(this.wallet) + endTime = new Date() + this.logger.info(`get receipt, cost: ${endTime.getTime() - startTime.getTime()}ms`) this.logger.info( `message sent to topic ${topicId}, status: ${sendReceipt.status.toString()}, transaction id: ${sendResponse.transactionId.toString()}`, ) + startTime = new Date() const record = await sendResponse.getRecordWithSigner(this.wallet) + endTime = new Date() + this.logger.info(`get record, cost: ${endTime.getTime() - startTime.getTime()}ms`) this.logger.info(`message sent, cost: ${record.transactionFee.toString()}`) return { receipt: sendReceipt, response: sendResponse } } diff --git a/dlt-connector/src/data/KeyPairIdentifier.logic.ts b/dlt-connector/src/data/KeyPairIdentifier.logic.ts index 1563d4b53..c64dda299 100644 --- a/dlt-connector/src/data/KeyPairIdentifier.logic.ts +++ b/dlt-connector/src/data/KeyPairIdentifier.logic.ts @@ -1,10 +1,10 @@ import { MemoryBlock } from 'gradido-blockchain-js' import { ParameterError } from '../errors' -import { IdentifierAccount } from '../schemas/account.schema' +import { IdentifierKeyPair } from '../schemas/account.schema' import { HieroId } from '../schemas/typeGuard.schema' export class KeyPairIdentifierLogic { - public constructor(public identifier: IdentifierAccount) {} + public constructor(public identifier: IdentifierKeyPair) {} isCommunityKeyPair(): boolean { return !this.identifier.seed && !this.identifier.account @@ -91,8 +91,8 @@ export class KeyPairIdentifierLogic { if (!this.identifier.account?.userUuid || !this.identifier.communityTopicId) { throw new ParameterError('userUuid and/or communityTopicId is undefined') } - const resultHexString = + const resultString = this.identifier.communityTopicId + this.identifier.account.userUuid.replace(/-/g, '') - return MemoryBlock.fromHex(resultHexString).calculateHash().convertToHex() + return new MemoryBlock(resultString).calculateHash().convertToHex() } } diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index e2eb30a8a..afd517069 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -7,7 +7,6 @@ import { BackendClient } from './client/backend/BackendClient' import { GradidoNodeClient } from './client/GradidoNode/GradidoNodeClient' import { HieroClient } from './client/hiero/HieroClient' import { CONFIG } from './config' -import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE } from './config/const' import { SendToHieroContext } from './interactions/sendToHiero/SendToHiero.context' import { KeyPairCacheManager } from './KeyPairCacheManager' import { Community, communitySchema } from './schemas/transaction.schema' diff --git a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts b/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts new file mode 100644 index 000000000..7c7103d94 --- /dev/null +++ b/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, mock, beforeAll, afterAll } from 'bun:test' +import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic' +import { KeyPairCalculation } from './KeyPairCalculation.context' +import { parse } from 'valibot' +import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema' +import { KeyPairCacheManager } from '../../KeyPairCacheManager' +import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js' +import { identifierKeyPairSchema } from '../../schemas/account.schema' +/* +// Mock JsonRpcClient +const mockRpcCall = mock((params) => { + console.log('mockRpcCall', params) + return { + isSuccess: () => false, + isError: () => true, + error: { + code: GradidoNodeErrorCodes.TRANSACTION_NOT_FOUND + } + } +}) +const mockRpcCallResolved = mock() + +mock.module('../../utils/network', () => ({ + isPortOpenRetry: async () => true, +})) + +mock.module('jsonrpc-ts-client', () => { + return { + default: class MockJsonRpcClient { + constructor() {} + exec = mockRpcCall + }, + } +}) +*/ + +mock.module('../../KeyPairCacheManager', () => { + let homeCommunityTopicId: HieroId | undefined + return { + KeyPairCacheManager: { + getInstance: () => ({ + setHomeCommunityTopicId: (topicId: HieroId) => { + homeCommunityTopicId = topicId + }, + getHomeCommunityTopicId: () => homeCommunityTopicId, + getKeyPair: (key: string, create: () => KeyPairEd25519) => { + return create() + }, + }), + }, + } +}) + +mock.module('../../config', () => ({ + CONFIG: { + HOME_COMMUNITY_SEED: MemoryBlock.fromHex('0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7'), + }, +})) + +const topicId = '0.0.21732' +const userUuid = 'aa25cf6f-2879-4745-b2ea-6d3c37fb44b0' + +console.log('userUuid', userUuid) + +afterAll(() => { + mock.restore() +}) + +describe('KeyPairCalculation', () => { + beforeAll(() => { + KeyPairCacheManager.getInstance().setHomeCommunityTopicId(parse(hieroIdSchema, '0.0.21732')) + }) + it('community key pair', async () => { + const identifier = new KeyPairIdentifierLogic(parse(identifierKeyPairSchema, { communityTopicId: topicId })) + const keyPair = await KeyPairCalculation(identifier) + expect(keyPair.getPublicKey()?.convertToHex()).toBe('7bcb0d0ad26d3f7ba597716c38a570220cece49b959e57927ee0c39a5a9c3adf') + }) + it('user key pair', async () => { + const identifier = new KeyPairIdentifierLogic(parse(identifierKeyPairSchema, { + communityTopicId: topicId, + account: { userUuid } + })) + expect(identifier.isAccountKeyPair()).toBe(false) + expect(identifier.isUserKeyPair()).toBe(true) + const keyPair = await KeyPairCalculation(identifier) + expect(keyPair.getPublicKey()?.convertToHex()).toBe('d61ae86c262fc0b5d763a8f41a03098fae73a7649a62aac844378a0eb0055921') + }) + + it('account key pair', async () => { + const identifier = new KeyPairIdentifierLogic(parse(identifierKeyPairSchema, { + communityTopicId: topicId, + account: { userUuid, accountNr: 1 } + })) + expect(identifier.isAccountKeyPair()).toBe(true) + expect(identifier.isUserKeyPair()).toBe(false) + const keyPair = await KeyPairCalculation(identifier) + expect(keyPair.getPublicKey()?.convertToHex()).toBe('6cffb0ee0b20dae828e46f2e003f78ac57b85e7268e587703932f06e1b2daee4') + }) +}) diff --git a/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts index 834f813c2..fd70682ea 100644 --- a/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts @@ -21,7 +21,12 @@ export class CreationTransactionRole extends AbstractTransactionRole { private readonly creationTransaction: CreationTransaction constructor(transaction: Transaction) { super() - this.creationTransaction = parse(creationTransactionSchema, transaction) + try { + this.creationTransaction = parse(creationTransactionSchema, transaction) + } catch (error) { + console.error('creation: invalid transaction', JSON.stringify(error, null, 2)) + throw new Error('creation: invalid transaction') + } this.homeCommunityTopicId = KeyPairCacheManager.getInstance().getHomeCommunityTopicId() if ( this.homeCommunityTopicId !== this.creationTransaction.user.communityTopicId || diff --git a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts new file mode 100644 index 000000000..032ec5eec --- /dev/null +++ b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'bun:test' +import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role' +import { parse } from 'valibot' +import { + transactionSchema, +} from '../../schemas/transaction.schema' +import { hieroIdSchema } from '../../schemas/typeGuard.schema' +import { InteractionToJson, InteractionValidate, ValidateType_SINGLE } from 'gradido-blockchain-js' + +const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec' +const transaction = { + user: { + communityTopicId: '0.0.21732', + account: { + userUuid, + accountNr: 0, + }, + }, + type: 'REGISTER_ADDRESS', + accountType: 'COMMUNITY_HUMAN', + createdAt: '2022-01-01T00:00:00.000Z', +} + +describe('RegisterAddressTransaction.role', () => { + it('get correct prepared builder', async () => { + const registerAddressTransactionRole = new RegisterAddressTransactionRole(parse(transactionSchema, transaction)) + expect(registerAddressTransactionRole.getSenderCommunityTopicId()).toBe(parse(hieroIdSchema, '0.0.21732')) + expect(() => registerAddressTransactionRole.getRecipientCommunityTopicId()).toThrow() + const builder = await registerAddressTransactionRole.getGradidoTransactionBuilder() + const gradidoTransaction = builder.build() + expect(() => new InteractionValidate(gradidoTransaction).run(ValidateType_SINGLE)).not.toThrow() + const json = JSON.parse(new InteractionToJson(gradidoTransaction).run()) + expect(json.bodyBytes.json.registerAddress.nameHash).toBe('bac2c06682808947f140d6766d02943761d4129ec055bb1f84dc3a4201a94c08') + }) +}) \ No newline at end of file diff --git a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts index 64076b920..32f78bac1 100644 --- a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts @@ -34,19 +34,15 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRole { public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() - const communityKeyPair = await KeyPairCalculation( - new KeyPairIdentifierLogic({ - communityTopicId: this.registerAddressTransaction.user.communityTopicId, - }), - ) - const accountKeyPairIdentifier = this.registerAddressTransaction.user + const communityTopicId = this.registerAddressTransaction.user.communityTopicId + const communityKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic({ communityTopicId })) + const keyPairIdentifier = this.registerAddressTransaction.user // when accountNr is 0 it is the user account - const userKeyPairIdentifier = accountKeyPairIdentifier - userKeyPairIdentifier.account.accountNr = 0 - - const userKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(userKeyPairIdentifier)) + keyPairIdentifier.account.accountNr = 0 + const userKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(keyPairIdentifier)) + keyPairIdentifier.account.accountNr = 1 const accountKeyPair = await KeyPairCalculation( - new KeyPairIdentifierLogic(accountKeyPairIdentifier), + new KeyPairIdentifierLogic(keyPairIdentifier), ) builder diff --git a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts index 996be0d10..8fde79267 100644 --- a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts +++ b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts @@ -58,28 +58,25 @@ export async function SendToHieroContext( // choose correct role based on transaction type and input type const chooseCorrectRole = (input: Transaction | Community): AbstractTransactionRole => { - const transactionParsingResult = safeParse(transactionSchema, input) const communityParsingResult = safeParse(communitySchema, input) - if (transactionParsingResult.success) { - const transaction = transactionParsingResult.output - switch (transaction.type) { - case InputTransactionType.GRADIDO_CREATION: - return new CreationTransactionRole(transaction) - case InputTransactionType.GRADIDO_TRANSFER: - return new TransferTransactionRole(transaction) - case InputTransactionType.REGISTER_ADDRESS: - return new RegisterAddressTransactionRole(transaction) - case InputTransactionType.GRADIDO_DEFERRED_TRANSFER: - return new DeferredTransferTransactionRole(transaction) - case InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER: - return new RedeemDeferredTransferTransactionRole(transaction) - default: - throw new Error('not supported transaction type: ' + transaction.type) - } - } else if (communityParsingResult.success) { + if (communityParsingResult.success) { return new CommunityRootTransactionRole(communityParsingResult.output) - } else { - throw new Error('not expected input') + } + + const transaction = input as Transaction + switch (transaction.type) { + case InputTransactionType.GRADIDO_CREATION: + return new CreationTransactionRole(transaction) + case InputTransactionType.GRADIDO_TRANSFER: + return new TransferTransactionRole(transaction) + case InputTransactionType.REGISTER_ADDRESS: + return new RegisterAddressTransactionRole(transaction) + case InputTransactionType.GRADIDO_DEFERRED_TRANSFER: + return new DeferredTransferTransactionRole(transaction) + case InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER: + return new RedeemDeferredTransferTransactionRole(transaction) + default: + throw new Error('not supported transaction type: ' + transaction.type) } } diff --git a/dlt-connector/src/schemas/account.schema.ts b/dlt-connector/src/schemas/account.schema.ts index 7be2cd4f2..d1dbb4802 100644 --- a/dlt-connector/src/schemas/account.schema.ts +++ b/dlt-connector/src/schemas/account.schema.ts @@ -11,18 +11,22 @@ export type IdentifierSeed = v.InferOutput // identifier for gradido community accounts, inside a community export const identifierCommunityAccountSchema = v.object({ userUuid: uuidv4Schema, - accountNr: v.nullish(v.number('expect number type'), 0), + accountNr: v.optional(v.number('expect number type'), 0), }) export type IdentifierCommunityAccount = v.InferOutput +export const identifierKeyPairSchema = v.object({ + communityTopicId: hieroIdSchema, + account: v.optional(identifierCommunityAccountSchema), + seed: v.optional(identifierSeedSchema), +}) +export type IdentifierKeyPairInput = v.InferInput +export type IdentifierKeyPair = v.InferOutput + // identifier for gradido account, including the community uuid export const identifierAccountSchema = v.pipe( - v.object({ - communityTopicId: hieroIdSchema, - account: v.nullish(identifierCommunityAccountSchema, undefined), - seed: v.nullish(identifierSeedSchema, undefined), - }), + identifierKeyPairSchema, v.custom((value: any) => { const setFieldsCount = Number(value.seed !== undefined) + Number(value.account !== undefined) if (setFieldsCount !== 1) { diff --git a/dlt-connector/src/schemas/transaction.schema.test.ts b/dlt-connector/src/schemas/transaction.schema.test.ts index 61f485db6..7d204dba5 100644 --- a/dlt-connector/src/schemas/transaction.schema.test.ts +++ b/dlt-connector/src/schemas/transaction.schema.test.ts @@ -1,4 +1,6 @@ import { beforeAll, describe, expect, it } from 'bun:test' +import { TypeBoxFromValibot } from '@sinclair/typemap' +import { TypeCompiler } from '@sinclair/typebox/compiler' import { randomBytes } from 'crypto' import { v4 as uuidv4 } from 'uuid' import { parse } from 'valibot' @@ -13,7 +15,9 @@ import { Uuidv4, uuidv4Schema, } from '../schemas/typeGuard.schema' -import { TransactionInput, transactionSchema } from './transaction.schema' +import { registerAddressTransactionSchema, TransactionInput, transactionSchema } from './transaction.schema' +import { AccountType } from '../enum/AccountType' +import { AddressType_COMMUNITY_HUMAN } from 'gradido-blockchain-js' const transactionLinkCode = (date: Date): string => { const time = date.getTime().toString(16) @@ -40,27 +44,54 @@ describe('transaction schemas', () => { memoString = 'TestMemo' memo = parse(memoSchema, memoString) }) - it('valid, register new user address', () => { - const registerAddress: TransactionInput = { - user: { - communityTopicId: topicString, - account: { userUuid: userUuidString }, - }, - type: InputTransactionType.REGISTER_ADDRESS, - createdAt: '2022-01-01T00:00:00.000Z', - } - expect(parse(transactionSchema, registerAddress)).toEqual({ - user: { - communityTopicId: topic, - account: { - userUuid, - accountNr: 0, + describe('register address', () => { + let registerAddress: TransactionInput + beforeAll(() => { + registerAddress = { + user: { + communityTopicId: topicString, + account: { userUuid: userUuidString }, }, - }, - type: registerAddress.type, - createdAt: new Date(registerAddress.createdAt), + type: InputTransactionType.REGISTER_ADDRESS, + accountType: AccountType.COMMUNITY_HUMAN, + createdAt: new Date().toISOString(), + } + }) + it('valid transaction schema', () => { + expect(parse(transactionSchema, registerAddress)).toEqual({ + user: { + communityTopicId: topic, + account: { + userUuid, + accountNr: 0, + }, + }, + type: registerAddress.type, + accountType: AccountType.COMMUNITY_HUMAN, + createdAt: new Date(registerAddress.createdAt), + }) + }) + it('valid register address schema', () => { + expect(parse(registerAddressTransactionSchema, registerAddress)).toEqual({ + user: { + communityTopicId: topic, + account: { + userUuid, + accountNr: 0, + }, + }, + accountType: AddressType_COMMUNITY_HUMAN, + createdAt: new Date(registerAddress.createdAt), + }) + }) + it('valid, transaction schema with typebox', () => { + // console.log(JSON.stringify(TypeBoxFromValibot(transactionSchema), null, 2)) + const TTransactionSchema = TypeBoxFromValibot(transactionSchema) + const check = TypeCompiler.Compile(TTransactionSchema) + expect(check.Check(registerAddress)).toBe(true) }) }) + it('valid, gradido transfer', () => { const gradidoTransfer: TransactionInput = { user: { diff --git a/dlt-connector/src/schemas/transaction.schema.ts b/dlt-connector/src/schemas/transaction.schema.ts index 4345ec901..7c1855ec8 100644 --- a/dlt-connector/src/schemas/transaction.schema.ts +++ b/dlt-connector/src/schemas/transaction.schema.ts @@ -5,7 +5,7 @@ import { identifierCommunityAccountSchema, identifierSeedSchema, } from './account.schema' -import { accountTypeSchema, addressTypeSchema, dateSchema } from './typeConverter.schema' +import { addressTypeSchema, dateSchema } from './typeConverter.schema' import { gradidoAmountSchema, hieroIdSchema, @@ -13,6 +13,7 @@ import { timeoutDurationSchema, uuidv4Schema, } from './typeGuard.schema' +import { AccountType } from '../enum/AccountType' /** * Schema for community, for creating new CommunityRoot Transaction on gradido blockchain @@ -29,14 +30,14 @@ export type Community = v.InferOutput export const transactionSchema = v.object({ user: identifierAccountSchema, - linkedUser: v.nullish(identifierAccountSchema, undefined), - amount: v.nullish(gradidoAmountSchema, undefined), - memo: v.nullish(memoSchema, undefined), + linkedUser: v.optional(identifierAccountSchema), + amount: v.optional(gradidoAmountSchema), + memo: v.optional(memoSchema), type: v.enum(InputTransactionType), createdAt: dateSchema, - targetDate: v.nullish(dateSchema, undefined), - timeoutDuration: v.nullish(timeoutDurationSchema, undefined), - accountType: v.nullish(accountTypeSchema, undefined), + targetDate: v.optional(dateSchema), + timeoutDuration: v.optional(timeoutDurationSchema), + accountType: v.optional(v.enum(AccountType)), }) export type TransactionInput = v.InferInput diff --git a/dlt-connector/src/schemas/typeConverter.schema.test.ts b/dlt-connector/src/schemas/typeConverter.schema.test.ts index 71cae3619..dd8944247 100644 --- a/dlt-connector/src/schemas/typeConverter.schema.test.ts +++ b/dlt-connector/src/schemas/typeConverter.schema.test.ts @@ -1,6 +1,8 @@ +import { Static, TypeBoxFromValibot } from '@sinclair/typemap' +import { TypeCompiler } from '@sinclair/typebox/compiler' // only for IDE, bun don't need this to work import { describe, expect, it } from 'bun:test' -import { AddressType_COMMUNITY_AUF, AddressType_COMMUNITY_PROJECT } from 'gradido-blockchain-js' +import { AddressType_COMMUNITY_AUF } from 'gradido-blockchain-js' import * as v from 'valibot' import { AccountType } from '../enum/AccountType' import { @@ -23,6 +25,27 @@ describe('basic.schema', () => { it('invalid date', () => { expect(() => v.parse(dateSchema, 'invalid date')).toThrow(new Error('invalid date')) }) + it('with type box', () => { + // Derive TypeBox Schema from the Valibot Schema + const DateSchema = TypeBoxFromValibot(dateSchema) + + // Build the compiler + const check = TypeCompiler.Compile(DateSchema) + + // Valid value (String) + expect(check.Check('2021-01-01T10:10:00.000Z')).toBe(true) + + // typebox cannot use valibot custom validation and transformations, it will check only the input types + expect(check.Check('invalid date')).toBe(true) + + // Type inference (TypeScript) + type DateType = Static + const validDate: DateType = '2021-01-01T10:10:00.000Z' + const validDate2: DateType = new Date('2021-01-01') + + // @ts-expect-error + const invalidDate: DateType = 123 // should fail in TS + }) }) describe('AddressType and AccountType', () => { @@ -46,6 +69,22 @@ describe('basic.schema', () => { const accountType = v.parse(accountTypeSchema, AddressType_COMMUNITY_AUF) expect(accountType).toBe(AccountType.COMMUNITY_AUF) }) + it('addressType with type box', () => { + const AddressTypeSchema = TypeBoxFromValibot(addressTypeSchema) + const check = TypeCompiler.Compile(AddressTypeSchema) + expect(check.Check(AccountType.COMMUNITY_AUF)).toBe(true) + // type box will throw an error, because it cannot handle valibots custom validation + expect(() => check.Check(AddressType_COMMUNITY_AUF)).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) + expect(() => check.Check('invalid')).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) + }) + it('accountType with type box', () => { + const AccountTypeSchema = TypeBoxFromValibot(accountTypeSchema) + const check = TypeCompiler.Compile(AccountTypeSchema) + expect(check.Check(AccountType.COMMUNITY_AUF)).toBe(true) + // type box will throw an error, because it cannot handle valibots custom validation + expect(() => check.Check(AddressType_COMMUNITY_AUF)).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) + expect(() => check.Check('invalid')).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) + }) }) it('confirmedTransactionSchema', () => { diff --git a/dlt-connector/src/schemas/typeConverter.schema.ts b/dlt-connector/src/schemas/typeConverter.schema.ts index 4410b54be..2e4811c11 100644 --- a/dlt-connector/src/schemas/typeConverter.schema.ts +++ b/dlt-connector/src/schemas/typeConverter.schema.ts @@ -47,8 +47,8 @@ export const addressTypeSchema = v.pipe( */ export const accountTypeSchema = v.pipe( v.union([ - v.custom(isAddressType, 'expect AddressType'), v.enum(AccountType, 'expect AccountType'), + v.custom(isAddressType, 'expect AddressType'), ]), v.transform((value) => toAccountType(value)), ) diff --git a/dlt-connector/src/schemas/typeGuard.schema.ts b/dlt-connector/src/schemas/typeGuard.schema.ts index b084e8531..d580abe3a 100644 --- a/dlt-connector/src/schemas/typeGuard.schema.ts +++ b/dlt-connector/src/schemas/typeGuard.schema.ts @@ -168,11 +168,21 @@ declare const validTimeoutDuration: unique symbol export type TimeoutDuration = DurationSeconds & { [validTimeoutDuration]: true } export const timeoutDurationSchema = v.pipe( - v.number('expect number type'), - v.minValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MIN, 'expect number >= 1 hour'), - v.maxValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MAX, 'expect number <= 3 months'), - v.transform( - (input: number) => new DurationSeconds(input) as TimeoutDuration, + v.union([ + v.pipe( + v.number('expect number type'), + v.minValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MIN, 'expect number >= 1 hour'), + v.maxValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MAX, 'expect number <= 3 months'), + ), + v.instance(DurationSeconds, 'expect DurationSeconds type'), + ]), + v.transform( + (input: number | DurationSeconds) => { + if (input instanceof DurationSeconds) { + return input as TimeoutDuration + } + return new DurationSeconds(input) as TimeoutDuration + }, ), ) @@ -200,8 +210,16 @@ declare const validGradidoAmount: unique symbol export type GradidoAmount = GradidoUnit & { [validGradidoAmount]: true } export const gradidoAmountSchema = v.pipe( - amountSchema, - v.transform( - (input: Amount) => GradidoUnit.fromString(input) as GradidoAmount, + v.union([ + amountSchema, + v.instance(GradidoUnit, 'expect GradidoUnit type'), + ]), + v.transform( + (input: Amount | GradidoUnit) => { + if (input instanceof GradidoUnit) { + return input as GradidoAmount + } + return GradidoUnit.fromString(input) as GradidoAmount + }, ), ) diff --git a/dlt-connector/src/server/index.test.ts b/dlt-connector/src/server/index.test.ts new file mode 100644 index 000000000..f3a9aed7d --- /dev/null +++ b/dlt-connector/src/server/index.test.ts @@ -0,0 +1,75 @@ +import { appRoutes } from '.' +import { describe, it, expect, beforeAll, mock } from 'bun:test' +import { KeyPairCacheManager } from '../KeyPairCacheManager' +import { hieroIdSchema } from '../schemas/typeGuard.schema' +import { parse } from 'valibot' +import { HieroId } from '../schemas/typeGuard.schema' +import { GradidoTransaction, KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js' + +const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec' + +mock.module('../KeyPairCacheManager', () => { + let homeCommunityTopicId: HieroId | undefined + return { + KeyPairCacheManager: { + getInstance: () => ({ + setHomeCommunityTopicId: (topicId: HieroId) => { + homeCommunityTopicId = topicId + }, + getHomeCommunityTopicId: () => homeCommunityTopicId, + getKeyPair: (key: string, create: () => KeyPairEd25519) => { + return create() + }, + }), + }, + } +}) + +mock.module('../client/hiero/HieroClient', () => ({ + HieroClient: { + getInstance: () => ({ + sendMessage: (topicId: HieroId, transaction: GradidoTransaction) => { + return { receipt: { status: '0.0.21732' }, response: { transactionId: '0.0.6566984@1758029639.561157605' } } + }, + }), + }, +})) + +mock.module('../config', () => ({ + CONFIG: { + HOME_COMMUNITY_SEED: MemoryBlock.fromHex('0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7'), + }, +})) + +beforeAll(() => { + KeyPairCacheManager.getInstance().setHomeCommunityTopicId(parse(hieroIdSchema, '0.0.21732')) +}) + +describe('Server', () => { + it('send register address transaction', async () => { + const transaction = { + user: { + communityTopicId: '0.0.21732', + account: { + userUuid, + accountNr: 0, + }, + }, + type: 'REGISTER_ADDRESS', + accountType: 'COMMUNITY_HUMAN', + createdAt: '2022-01-01T00:00:00.000Z', + } + const response = await appRoutes.handle(new Request('http://localhost/sendTransaction', { + method: 'POST', + body: JSON.stringify(transaction), + headers: { + 'Content-Type': 'application/json', + }, + })) + if (response.status !== 200) { + console.log(await response.text()) + } + expect(response.status).toBe(200) + expect(await response.text()).toBe('0.0.6566984@1758029639.561157605') + }) +}) diff --git a/dlt-connector/src/server/index.ts b/dlt-connector/src/server/index.ts index 3aa13dcac..b8aa44fd3 100644 --- a/dlt-connector/src/server/index.ts +++ b/dlt-connector/src/server/index.ts @@ -1,6 +1,6 @@ import { TypeBoxFromValibot } from '@sinclair/typemap' import { Type } from '@sinclair/typebox' -import { Elysia, status } from 'elysia' +import { Elysia, status, t } from 'elysia' import { AddressType_NONE } from 'gradido-blockchain-js' import { getLogger } from 'log4js' import { parse } from 'valibot' @@ -11,7 +11,7 @@ import { KeyPairCalculation } from '../interactions/keyPairCalculation/KeyPairCa import { SendToHieroContext } from '../interactions/sendToHiero/SendToHiero.context' import { IdentifierAccount, identifierAccountSchema } from '../schemas/account.schema' import { transactionSchema } from '../schemas/transaction.schema' -import { hieroTransactionIdSchema } from '../schemas/typeGuard.schema' +import { hieroIdSchema, hieroTransactionIdSchema } from '../schemas/typeGuard.schema' import { accountIdentifierSeedSchema, accountIdentifierUserSchema, @@ -48,29 +48,22 @@ export const appRoutes = new Elysia() .post( '/sendTransaction', async ({ body }) => { - console.log("sendTransaction was called") - return "0.0.123" - console.log(body) - console.log(parse(transactionSchema, body)) - const transaction = parse(transactionSchema, body) - return await SendToHieroContext(transaction) + try { + const hieroTransactionId = await SendToHieroContext(parse(transactionSchema, body)) + console.log('server will return:', hieroTransactionId) + return { transactionId: hieroTransactionId } + } catch (e) { + if (e instanceof TypeError) { + console.log(`message: ${e.message}, stack: ${e.stack}`) + } + console.log(e) + throw status(500, e) + } }, // validation schemas { - // body: TypeBoxFromValibot(transactionSchema), - body: Type.Object({ - user: Type.Object({ - communityUser: Type.Object({ - uuid: Type.String({ format: 'uuid' }), - accountNr: Type.Optional(Type.String()), // optional/undefined - }), - communityUuid: Type.String({ format: 'uuid' }), - }), - createdAt: Type.String({ format: 'date-time' }), - accountType: Type.Literal('COMMUNITY_HUMAN'), - type: Type.Literal('REGISTER_ADDRESS'), - }) - // response: TypeBoxFromValibot(hieroTransactionIdSchema), + body: TypeBoxFromValibot(transactionSchema), + response: t.Object({ transactionId: TypeBoxFromValibot(hieroTransactionIdSchema) }), }, )