diff --git a/backend/src/apis/dltConnector/index.ts b/backend/src/apis/dltConnector/index.ts index 7780006e9..671c88297 100644 --- a/backend/src/apis/dltConnector/index.ts +++ b/backend/src/apis/dltConnector/index.ts @@ -7,8 +7,12 @@ import { Community as DbCommunity, Contribution as DbContribution, DltTransaction as DbDltTransaction, + TransactionLink as DbTransactionLink, User as DbUser, - getHomeCommunity, + getCommunityByUuid, + getHomeCommunity, + getUserById, + UserLoggingView, } from 'database' import { TransactionDraft } from './model/TransactionDraft' @@ -92,7 +96,15 @@ export async function transferTransaction( memo: string, createdAt: Date ): Promise { - + // load community if not already loaded, maybe they are remote communities + if (!senderUser.community) { + senderUser.community = await getCommunityByUuid(senderUser.communityUuid) + } + if (!recipientUser.community) { + recipientUser.community = await getCommunityByUuid(recipientUser.communityUuid) + } + logger.info(`sender user: ${new UserLoggingView(senderUser)}`) + logger.info(`recipient user: ${new UserLoggingView(recipientUser)}`) const draft = TransactionDraft.createTransfer(senderUser, recipientUser, amount, memo, createdAt) if (draft && dltConnectorClient) { const clientResponse = dltConnectorClient.sendTransaction(draft) @@ -104,4 +116,50 @@ export async function transferTransaction( return null } +export async function deferredTransferTransaction(senderUser: DbUser, transactionLink: DbTransactionLink) +: Promise { + // load community if not already loaded + if (!senderUser.community) { + senderUser.community = await getCommunityByUuid(senderUser.communityUuid) + } + const draft = TransactionDraft.createDeferredTransfer(senderUser, transactionLink) + if (draft && dltConnectorClient) { + const clientResponse = dltConnectorClient.sendTransaction(draft) + let dltTransaction = new DbDltTransaction() + dltTransaction.typeId = DltTransactionType.DEFERRED_TRANSFER + dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse) + return await dltTransaction.save() + } + return null +} + +export async function redeemDeferredTransferTransaction(transactionLink: DbTransactionLink, amount: string, createdAt: Date, recipientUser: DbUser) +: Promise { + // load user and communities if not already loaded + if (!transactionLink.user) { + logger.debug('load sender user') + transactionLink.user = await getUserById(transactionLink.userId, true, false) + } + if (!transactionLink.user.community) { + logger.debug('load sender community') + transactionLink.user.community = await getCommunityByUuid(transactionLink.user.communityUuid) + } + if (!recipientUser.community) { + logger.debug('load recipient community') + recipientUser.community = await getCommunityByUuid(recipientUser.communityUuid) + } + logger.debug(`sender: ${new UserLoggingView(transactionLink.user)}`) + logger.debug(`recipient: ${new UserLoggingView(recipientUser)}`) + const draft = TransactionDraft.redeemDeferredTransfer(transactionLink, amount, createdAt, recipientUser) + if (draft && dltConnectorClient) { + const clientResponse = dltConnectorClient.sendTransaction(draft) + let dltTransaction = new DbDltTransaction() + dltTransaction.typeId = DltTransactionType.REDEEM_DEFERRED_TRANSFER + dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse) + return await dltTransaction.save() + } + return null +} + + diff --git a/backend/src/apis/dltConnector/model/TransactionDraft.ts b/backend/src/apis/dltConnector/model/TransactionDraft.ts index 86f46a43b..5c40da289 100755 --- a/backend/src/apis/dltConnector/model/TransactionDraft.ts +++ b/backend/src/apis/dltConnector/model/TransactionDraft.ts @@ -3,10 +3,17 @@ 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 { + Community as DbCommunity, + Contribution as DbContribution, + TransactionLink as DbTransactionLink, + User as DbUser +} from 'database' import { CommunityAccountIdentifier } from './CommunityAccountIdentifier' import { getLogger } from 'log4js' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' +import { IdentifierSeed } from './IdentifierSeed' +import { CODE_VALID_DAYS_DURATION } from '@/graphql/resolver/const/const' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector.model.TransactionDraft`) @@ -21,7 +28,7 @@ export class TransactionDraft { createdAt: string // only for creation transaction targetDate?: string - // only for deferred transaction + // only for deferred transaction, duration in seconds timeoutDuration?: number // only for register address accountType?: AccountType @@ -75,4 +82,48 @@ export class TransactionDraft { draft.memo = memo return draft } + + static createDeferredTransfer(sendingUser: DbUser, transactionLink: DbTransactionLink) + : TransactionDraft | null { + if (!sendingUser.community) { + throw new Error(`missing community for user ${sendingUser.id}`) + } + const senderUserTopic = sendingUser.community.hieroTopicId + if (!senderUserTopic) { + throw new Error(`missing topicId for community ${sendingUser.community.id}`) + } + const draft = new TransactionDraft() + draft.user = new AccountIdentifier(senderUserTopic, new CommunityAccountIdentifier(sendingUser.gradidoID)) + draft.linkedUser = new AccountIdentifier(senderUserTopic, new IdentifierSeed(transactionLink.code)) + draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER + draft.createdAt = transactionLink.createdAt.toISOString() + draft.amount = transactionLink.amount.toString() + draft.memo = transactionLink.memo + draft.timeoutDuration = CODE_VALID_DAYS_DURATION * 24 * 60 * 60 + return draft + } + + static redeemDeferredTransfer(transactionLink: DbTransactionLink, amount: string, createdAt: Date, recipientUser: DbUser): TransactionDraft | null { + if (!transactionLink.user.community) { + throw new Error(`missing community for user ${transactionLink.user.id}`) + } + if (!recipientUser.community) { + throw new Error(`missing community for user ${recipientUser.id}`) + } + const senderUserTopic = transactionLink.user.community.hieroTopicId + if (!senderUserTopic) { + throw new Error(`missing topicId for community ${transactionLink.user.community.id}`) + } + const recipientUserTopic = recipientUser.community.hieroTopicId + if (!recipientUserTopic) { + throw new Error(`missing topicId for community ${recipientUser.community.id}`) + } + const draft = new TransactionDraft() + draft.user = new AccountIdentifier(senderUserTopic, new IdentifierSeed(transactionLink.code)) + draft.linkedUser = new AccountIdentifier(recipientUserTopic, new CommunityAccountIdentifier(recipientUser.gradidoID)) + draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER + draft.createdAt = createdAt.toISOString() + draft.amount = amount + return draft + } } \ No newline at end of file diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 7c31209e0..ac06f012e 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -436,9 +436,6 @@ export class ContributionResolver { ): Promise { const logger = createLogger() logger.addContext('contribution', id) - - let transaction: DbTransaction - let dltTransactionPromise: Promise // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() try { @@ -463,8 +460,7 @@ export class ContributionResolver { throw new LogError('Can not confirm contribution since the user was deleted') } const receivedCallDate = new Date() - dltTransactionPromise = contributionTransaction(contribution, moderatorUser, receivedCallDate) - + const dltTransactionPromise = contributionTransaction(contribution, moderatorUser, receivedCallDate) const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false) validateContribution( creations, @@ -476,7 +472,6 @@ export class ContributionResolver { const queryRunner = db.getDataSource().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') - const lastTransaction = await getLastTransaction(contribution.userId) logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined') @@ -493,7 +488,7 @@ export class ContributionResolver { } newBalance = newBalance.add(contribution.amount.toString()) - transaction = new DbTransaction() + let transaction = new DbTransaction() transaction.typeId = TransactionTypeId.CREATION transaction.memo = contribution.memo transaction.userId = contribution.userId @@ -520,7 +515,7 @@ export class ContributionResolver { await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution) await queryRunner.commitTransaction() - + logger.info('creation commited successfuly.') await sendContributionConfirmedEmail({ firstName: user.firstName, @@ -536,6 +531,17 @@ export class ContributionResolver { contribution.createdAt, ), }) + + // update transaction id in dlt transaction tables + // wait for finishing transaction by dlt-connector/hiero + const dltStartTime = new Date() + const dltTransaction = await dltTransactionPromise + if(dltTransaction) { + dltTransaction.transactionId = transaction.id + await dltTransaction.save() + } + const dltEndTime = new Date() + logger.debug(`dlt-connector contribution finished in ${dltEndTime.getTime() - dltStartTime.getTime()} ms`) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Creation was not successful', e) @@ -546,16 +552,6 @@ 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/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 624038c20..4521ce4ae 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -14,10 +14,13 @@ import { User } from '@model/User' import { QueryLinkResult } from '@union/QueryLinkResult' import { Decay, interpretEncryptedTransferArgs, TransactionTypeId } from 'core' import { - AppDatabase, Community as DbCommunity, Contribution as DbContribution, - ContributionLink as DbContributionLink, FederatedCommunity as DbFederatedCommunity, Transaction as DbTransaction, + AppDatabase, Contribution as DbContribution, + ContributionLink as DbContributionLink, FederatedCommunity as DbFederatedCommunity, + DltTransaction as DbDltTransaction, + Transaction as DbTransaction, TransactionLink as DbTransactionLink, User as DbUser, + findModeratorCreatingContributionLink, findTransactionLinkByCode, getHomeCommunity } from 'database' @@ -33,14 +36,10 @@ import { } from '@/event/Events' import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' -import { - InterruptiveSleepManager, - TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, -} from '@/util/InterruptiveSleepManager' import { calculateBalance } from '@/util/validate' import { fullName } from 'core' import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database' -import { calculateDecay, decode, DisburseJwtPayloadType, encode, encryptAndSign, EncryptedJWEJwtPayloadType, RedeemJwtPayloadType, verify } from 'shared' +import { calculateDecay, decode, DisburseJwtPayloadType, encode, encryptAndSign, RedeemJwtPayloadType, verify } from 'shared' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { DisbursementClient as V1_0_DisbursementClient } from '@/federation/client/1_0/DisbursementClient' @@ -58,6 +57,8 @@ import { import { getUserCreation, validateContribution } from './util/creations' import { transactionLinkList } from './util/transactionLinkList' import { SignedTransferPayloadType } from 'shared' +import { contributionTransaction, deferredTransferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector' +import { CODE_VALID_DAYS_DURATION } from './const/const' const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver.${method}`) @@ -71,7 +72,7 @@ export const transactionLinkCode = (date: Date): string => { ) } -const CODE_VALID_DAYS_DURATION = 14 + const db = AppDatabase.getInstance() export const transactionLinkExpireDate = (date: Date): Date => { @@ -109,11 +110,20 @@ export class TransactionLinkResolver { transactionLink.code = transactionLinkCode(createdDate) transactionLink.createdAt = createdDate transactionLink.validUntil = validUntil + const dltTransactionPromise = deferredTransferTransaction(user, transactionLink) await DbTransactionLink.save(transactionLink).catch((e) => { throw new LogError('Unable to save transaction link', e) }) await EVENT_TRANSACTION_LINK_CREATE(user, transactionLink, amount) - + // wait for dlt transaction to be created + const startTime = Date.now() + const dltTransaction = await dltTransactionPromise + const endTime = Date.now() + createLogger('createTransactionLink').debug(`dlt transaction created in ${endTime - startTime} ms`) + if (dltTransaction) { + dltTransaction.transactionLinkId = transactionLink.id + await DbDltTransaction.save(dltTransaction) + } return new TransactionLink(transactionLink, new User(user)) } @@ -137,7 +147,6 @@ export class TransactionLinkResolver { user.id, ) } - if (transactionLink.redeemedBy) { throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy) } @@ -146,7 +155,19 @@ export class TransactionLinkResolver { throw new LogError('Transaction link could not be deleted', e) }) + transactionLink.user = user + const dltTransactionPromise = redeemDeferredTransferTransaction(transactionLink, transactionLink.amount.toString(), transactionLink.deletedAt!, user) + await EVENT_TRANSACTION_LINK_DELETE(user, transactionLink) + // wait for dlt transaction to be created + const startTime = Date.now() + const dltTransaction = await dltTransactionPromise + const endTime = Date.now() + createLogger('deleteTransactionLink').debug(`dlt transaction created in ${endTime - startTime} ms`) + if (dltTransaction) { + dltTransaction.transactionLinkId = transactionLink.id + await DbDltTransaction.save(dltTransaction) + } return true } @@ -279,7 +300,7 @@ export class TransactionLinkResolver { throw new LogError('Contribution link has unknown cycle', contributionLink.cycle) } } - + const moderatorPromise = findModeratorCreatingContributionLink(contributionLink) const creations = await getUserCreation(user.id, clientTimezoneOffset) methodLogger.info('open creations', creations) validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset) @@ -293,6 +314,12 @@ export class TransactionLinkResolver { contribution.contributionType = ContributionType.LINK contribution.contributionStatus = ContributionStatus.CONFIRMED + let dltTransactionPromise: Promise = Promise.resolve(null) + const moderator = await moderatorPromise + if (moderator) { + dltTransactionPromise = contributionTransaction(contribution, moderator, now) + } + await queryRunner.manager.insert(DbContribution, contribution) const lastTransaction = await getLastTransaction(user.id) @@ -338,6 +365,17 @@ export class TransactionLinkResolver { contributionLink, contributionLink.amount, ) + if (dltTransactionPromise) { + const startTime = new Date() + const dltTransaction = await dltTransactionPromise + const endTime = new Date() + methodLogger.info(`dlt-connector transaction finished in ${endTime.getTime() - startTime.getTime()} ms`) + if (dltTransaction) { + dltTransaction.transactionId = transaction.id + await dltTransaction.save() + } + + } } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Creation from contribution link was not successful', e) @@ -346,10 +384,7 @@ export class TransactionLinkResolver { } } finally { releaseLock() - } - // notify dlt-connector loop for new work - InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) - + } return true } else { const now = new Date() @@ -399,8 +434,6 @@ export class TransactionLinkResolver { } finally { releaseLinkLock() } - // notify dlt-connector loop for new work - InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) return true } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 79b6eff5f..0b1d6d866 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -6,7 +6,9 @@ import { Transaction as dbTransaction, TransactionLink as dbTransactionLink, User as dbUser, - findUserByIdentifier + findUserByIdentifier, + TransactionLoggingView, + UserLoggingView } from 'database' import { Decimal } from 'decimal.js-light' import { Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql' @@ -43,7 +45,7 @@ import { GdtResolver } from './GdtResolver' import { getCommunityName, isHomeCommunity } from './util/communities' import { getTransactionList } from './util/getTransactionList' import { transactionLinkSummary } from './util/transactionLinkSummary' -import { transferTransaction } from '@/apis/dltConnector' +import { transferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector' const db = AppDatabase.getInstance() const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`) @@ -60,13 +62,15 @@ export const executeTransaction = async ( let dltTransactionPromise: Promise = Promise.resolve(null) if (!transactionLink) { dltTransactionPromise = transferTransaction(sender, recipient, amount.toString(), memo, receivedCallDate) + } else { + dltTransactionPromise = redeemDeferredTransferTransaction(transactionLink, amount.toString(), receivedCallDate, recipient) } // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() try { - logger.info('executeTransaction', amount, memo, sender, recipient) + logger.info('executeTransaction', memo) if (await countOpenPendingTransactions([sender.gradidoID, recipient.gradidoID]) > 0) { throw new LogError( @@ -85,7 +89,7 @@ export const executeTransaction = async ( receivedCallDate, transactionLink, ) - logger.debug(`calculated Balance=${sendBalance}`) + logger.debug(`calculated balance=${sendBalance?.balance.toString()} decay=${sendBalance?.decay.decay.toString()} lastTransactionId=${sendBalance?.lastTransactionId}`) if (!sendBalance) { throw new LogError('User has not enough GDD or amount is < 0', sendBalance) } @@ -144,7 +148,7 @@ export const executeTransaction = async ( // Save linked transaction id for send transactionSend.linkedTransactionId = transactionReceive.id await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) - logger.debug('send Transaction updated', transactionSend) + logger.debug('send Transaction updated', new TransactionLoggingView(transactionSend).toJSON()) if (transactionLink) { logger.info('transactionLink', transactionLink) @@ -158,21 +162,23 @@ export const executeTransaction = async ( } await queryRunner.commitTransaction() - // 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) - await EVENT_TRANSACTION_RECEIVE( recipient, sender, transactionReceive, transactionReceive.amount, ) + // update dltTransaction with transactionId + const startTime = new Date() + const dltTransaction = await dltTransactionPromise + const endTime = new Date() + logger.debug(`dlt-connector transaction finished in ${endTime.getTime() - startTime.getTime()} ms`) + if (dltTransaction) { + dltTransaction.transactionId = transactionSend.id + await dltTransaction.save() + } } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Transaction was not successful', e) diff --git a/backend/src/graphql/resolver/const/const.ts b/backend/src/graphql/resolver/const/const.ts index 8497ca6d5..8e0a0722e 100644 --- a/backend/src/graphql/resolver/const/const.ts +++ b/backend/src/graphql/resolver/const/const.ts @@ -12,3 +12,4 @@ export const MEMO_MAX_CHARS = 512 export const MEMO_MIN_CHARS = 5 export const DEFAULT_PAGINATION_PAGE_SIZE = 25 export const FRONTEND_CONTRIBUTIONS_ITEM_ANCHOR_PREFIX = 'contributionListItem-' +export const CODE_VALID_DAYS_DURATION = 14 \ No newline at end of file diff --git a/backend/src/util/InterruptiveSleep.ts b/backend/src/util/InterruptiveSleep.ts index dc8ed5ae0..86afcf9b5 100644 --- a/backend/src/util/InterruptiveSleep.ts +++ b/backend/src/util/InterruptiveSleep.ts @@ -1,4 +1,4 @@ -import { delay } from './utilities' +import { delay } from 'core' /** * Sleep, that can be interrupted diff --git a/database/src/logging/UserContactLogging.view.ts b/database/src/logging/UserContactLogging.view.ts index d80b17c67..4bd567cad 100644 --- a/database/src/logging/UserContactLogging.view.ts +++ b/database/src/logging/UserContactLogging.view.ts @@ -8,7 +8,7 @@ enum OptInType { } export class UserContactLoggingView extends AbstractLoggingView { - public constructor(private self: UserContact) { + public constructor(private self: UserContact, private showUser = true) { super() } @@ -16,7 +16,7 @@ export class UserContactLoggingView extends AbstractLoggingView { return { id: this.self.id, type: this.self.type, - user: this.self.user + user: this.showUser && this.self.user ? new UserLoggingView(this.self.user).toJSON() : { id: this.self.userId }, email: this.self.email?.substring(0, 3) + '...', diff --git a/database/src/logging/UserLogging.view.ts b/database/src/logging/UserLogging.view.ts index 1aa5e4407..84db8a6fe 100644 --- a/database/src/logging/UserLogging.view.ts +++ b/database/src/logging/UserLogging.view.ts @@ -4,6 +4,7 @@ import { ContributionLoggingView } from './ContributionLogging.view' import { ContributionMessageLoggingView } from './ContributionMessageLogging.view' import { UserContactLoggingView } from './UserContactLogging.view' import { UserRoleLoggingView } from './UserRoleLogging.view' +import { CommunityLoggingView } from './CommunityLogging.view' enum PasswordEncryptionType { NO_PASSWORD = 0, @@ -21,10 +22,12 @@ export class UserLoggingView extends AbstractLoggingView { id: this.self.id, foreign: this.self.foreign, gradidoID: this.self.gradidoID, - communityUuid: this.self.communityUuid, + community: this.self.community + ? new CommunityLoggingView(this.self.community).toJSON() + : { id: this.self.communityUuid }, alias: this.self.alias?.substring(0, 3) + '...', emailContact: this.self.emailContact - ? new UserContactLoggingView(this.self.emailContact).toJSON() + ? new UserContactLoggingView(this.self.emailContact, false).toJSON() : { id: this.self.emailId }, firstName: this.self.firstName?.substring(0, 3) + '...', lastName: this.self.lastName?.substring(0, 3) + '...', @@ -35,7 +38,7 @@ export class UserLoggingView extends AbstractLoggingView { hideAmountGDD: this.self.hideAmountGDD, hideAmountGDT: this.self.hideAmountGDT, userRoles: this.self.userRoles - ? this.self.userRoles.map((userRole) => new UserRoleLoggingView(userRole).toJSON()) + ? this.self.userRoles.map((userRole) => new UserRoleLoggingView(userRole, false).toJSON()) : undefined, referrerId: this.self.referrerId, contributionLinkId: this.self.contributionLinkId, @@ -50,7 +53,7 @@ export class UserLoggingView extends AbstractLoggingView { : undefined, userContacts: this.self.userContacts ? this.self.userContacts.map((userContact) => - new UserContactLoggingView(userContact).toJSON(), + new UserContactLoggingView(userContact, false).toJSON(), ) : undefined, } diff --git a/database/src/logging/UserRoleLogging.view.ts b/database/src/logging/UserRoleLogging.view.ts index 52684d242..ebfa3aed8 100644 --- a/database/src/logging/UserRoleLogging.view.ts +++ b/database/src/logging/UserRoleLogging.view.ts @@ -3,14 +3,14 @@ import { AbstractLoggingView } from './AbstractLogging.view' import { UserLoggingView } from './UserLogging.view' export class UserRoleLoggingView extends AbstractLoggingView { - public constructor(private self: UserRole) { + public constructor(private self: UserRole, private showUser = true) { super() } public toJSON(): any { return { id: this.self.id, - user: this.self.user + user: this.showUser && this.self.user ? new UserLoggingView(this.self.user).toJSON() : { id: this.self.userId }, role: this.self.role, diff --git a/database/src/queries/events.ts b/database/src/queries/events.ts new file mode 100644 index 000000000..ff0967944 --- /dev/null +++ b/database/src/queries/events.ts @@ -0,0 +1,19 @@ +import { + ContributionLink as DbContributionLink, + Event as DbEvent, + User as DbUser +} from '../entity' + +export async function findModeratorCreatingContributionLink(contributionLink: DbContributionLink): Promise { + const event = await DbEvent.findOne( + { + where: { + involvedContributionLinkId: contributionLink.id, + // todo: move event types into db + type: 'ADMIN_CONTRIBUTION_LINK_CREATE' + }, + relations: { actingUser: true } + } + ) + return event?.actingUser +} \ No newline at end of file diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts index 1fec568bf..54b5c6d4e 100644 --- a/database/src/queries/index.ts +++ b/database/src/queries/index.ts @@ -5,5 +5,6 @@ export * from './communities' export * from './pendingTransactions' export * from './transactions' export * from './transactionLinks' +export * from './events' export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` diff --git a/database/src/queries/user.ts b/database/src/queries/user.ts index 2cb0eaaee..eb3f58d60 100644 --- a/database/src/queries/user.ts +++ b/database/src/queries/user.ts @@ -12,6 +12,13 @@ export async function aliasExists(alias: string): Promise { return user !== null } +export async function getUserById(id: number, withCommunity: boolean = false, withEmailContact: boolean = false): Promise { + return DbUser.findOneOrFail({ + where: { id }, + relations: { community: withCommunity, emailContact: withEmailContact }, + }) +} + /** * * @param identifier could be gradidoID, alias or email of user diff --git a/dlt-connector/bun.lock b/dlt-connector/bun.lock index 93b3943c1..c7616863d 100644 --- a/dlt-connector/bun.lock +++ b/dlt-connector/bun.lock @@ -4,6 +4,7 @@ "": { "name": "dlt-connector", "dependencies": { + "async-exit-hook": "^2.0.1", "gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js", }, "devDependencies": { @@ -11,6 +12,7 @@ "@hashgraph/sdk": "^2.70.0", "@sinclair/typebox": "^0.34.33", "@sinclair/typemap": "^0.10.1", + "@types/async-exit-hook": "^2.0.2", "@types/bun": "^1.2.17", "dotenv": "^10.0.0", "elysia": "1.3.8", @@ -247,6 +249,8 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/async-exit-hook": ["@types/async-exit-hook@2.0.2", "", {}, "sha512-RJbTNivnnn+JzNiQTtUgwo/1S6QUHwI5JfXCeUPsqZXB4LuvRwvHhbKFSS5jFDYpk8XoEAYVW2cumBOdGpXL2Q=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -301,6 +305,8 @@ "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="], + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], diff --git a/dlt-connector/src/client/hiero/HieroClient.ts b/dlt-connector/src/client/hiero/HieroClient.ts index 0815437ef..92a994345 100644 --- a/dlt-connector/src/client/hiero/HieroClient.ts +++ b/dlt-connector/src/client/hiero/HieroClient.ts @@ -9,6 +9,7 @@ import { TopicInfoQuery, TopicMessageSubmitTransaction, TopicUpdateTransaction, + TransactionId, TransactionReceipt, TransactionResponse, Wallet, @@ -30,6 +31,9 @@ export class HieroClient { wallet: Wallet client: Client logger: Logger + // transaction counter for logging + transactionInternNr: number = 0 + pendingPromises: Promise[] = [] private constructor() { this.logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.HieroClient`) @@ -53,12 +57,23 @@ export class HieroClient { return HieroClient.instance } + public async waitForPendingPromises() { + const startTime = new Date() + this.logger.info(`waiting for ${this.pendingPromises.length} pending promises`) + await Promise.all(this.pendingPromises) + const endTime = new Date() + this.logger.info(`all pending promises resolved, used time: ${endTime.getTime() - startTime.getTime()}ms`) + } + public async sendMessage( topicId: HieroId, transaction: GradidoTransaction, - ): Promise<{ receipt: TransactionReceipt; response: TransactionResponse }> { - let startTime = new Date() - this.logger.addContext('topicId', topicId.toString()) + ): Promise { + const startTime = new Date() + this.transactionInternNr++ + const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.HieroClient`) + logger.addContext('trNr', this.transactionInternNr) + logger.addContext('topicId', topicId.toString()) const serializedTransaction = transaction.getSerializedTransaction() if (!serializedTransaction) { throw new Error('cannot serialize transaction') @@ -68,29 +83,34 @@ 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()}`, + // sign and execute transaction needs some time, so let it run in background + const pendingPromiseIndex = this.pendingPromises.push( + hieroTransaction.signWithSigner(this.wallet).then(async (signedHieroTransaction) => { + const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet) + logger.info(`message sent to topic ${topicId}, transaction id: ${sendResponse.transactionId.toString()}`) + if (logger.isInfoEnabled()) { + // only for logging + sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => { + logger.info( + `message send status: ${receipt.status.toString()}`, + ) + }) + // only for logging + sendResponse.getRecordWithSigner(this.wallet).then((record) => { + logger.info(`message sent, cost: ${record.transactionFee.toString()}`) + const localEndTime = new Date() + logger.info(`HieroClient.sendMessage used time (full process): ${localEndTime.getTime() - startTime.getTime()}ms`) + }) + } + }).catch((e) => { + logger.error(e) + }).finally(() => { + this.pendingPromises.splice(pendingPromiseIndex, 1) + }) ) - 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 } + const endTime = new Date() + logger.info(`HieroClient.sendMessage used time: ${endTime.getTime() - startTime.getTime()}ms`) + return hieroTransaction.transactionId } public async getBalance(): Promise { diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index afd517069..835a1cef5 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -46,9 +46,36 @@ async function main() { // listen for rpc request from backend (graphql replaced with elysiaJS) new Elysia().use(appRoutes).listen(CONFIG.DLT_CONNECTOR_PORT, () => { logger.info(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`) + setupGracefulShutdown(logger) }) } +function setupGracefulShutdown(logger: Logger) { + const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'] + signals.forEach(sig => { + process.on(sig, async () => { + logger.info(`[shutdown] Got ${sig}, cleaning up…`) + await gracefulShutdown(logger) + process.exit(0) + }) + }) + + if (process.platform === "win32") { + const rl = require("readline").createInterface({ + input: process.stdin, + output: process.stdout, + }) + rl.on("SIGINT", () => { + process.emit("SIGINT" as any) + }) + } +} + +async function gracefulShutdown(logger: Logger) { + logger.info('graceful shutdown') + await HieroClient.getInstance().waitForPendingPromises() +} + function loadConfig(): Logger { // configure log4js // TODO: replace late by loader from config-schema @@ -113,3 +140,7 @@ main().catch((e) => { console.error(e) process.exit(1) }) +function exitHook(arg0: () => void) { + throw new Error('Function not implemented.') +} + diff --git a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts index 8fde79267..5061233be 100644 --- a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts +++ b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts @@ -50,10 +50,12 @@ export async function SendToHieroContext( topic: HieroId, ): Promise => { const client = HieroClient.getInstance() - const resultMessage = await client.sendMessage(topic, gradidoTransaction) - const transactionId = resultMessage.response.transactionId.toString() - logger.info('transmitted Gradido Transaction to Hiero', { transactionId }) - return transactionId + const transactionId = await client.sendMessage(topic, gradidoTransaction) + if (!transactionId) { + throw new Error('missing transaction id from hiero') + } + logger.info('transmitted Gradido Transaction to Hiero', { transactionId: transactionId.toString() }) + return transactionId.toString() } // choose correct role based on transaction type and input type