diff --git a/backend/src/apis/dltConnector/DltConnectorClient.ts b/backend/src/apis/dltConnector/DltConnectorClient.ts index 3402e0844..b8f2dbbe3 100644 --- a/backend/src/apis/dltConnector/DltConnectorClient.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.ts @@ -1,4 +1,5 @@ import { Transaction as DbTransaction } from '@entity/Transaction' +import { TransactionLink } from '@entity/TransactionLink' import { User } from '@entity/User' import { gql, GraphQLClient } from 'graphql-request' // eslint-disable-next-line import/named, n/no-extraneous-import @@ -99,8 +100,10 @@ export class DltConnectorClient { return DltConnectorClient.instance } - private getTransactionParams(input: DbTransaction | User): TransactionDraft | UserAccountDraft { - if (input instanceof DbTransaction) { + private getTransactionParams( + input: DbTransaction | User | TransactionLink, + ): TransactionDraft | UserAccountDraft { + if (input instanceof DbTransaction || input instanceof TransactionLink) { return new TransactionDraft(input) } else if (input instanceof User) { return new UserAccountDraft(input) @@ -138,7 +141,7 @@ export class DltConnectorClient { * and update dltTransactionId of transaction in db with iota message id */ public async transmitTransaction( - transaction: DbTransaction | User, + transaction: DbTransaction | User | TransactionLink, ): Promise { // we don't need the receive transactions, there contain basically the same data as the send transactions if ( diff --git a/backend/src/apis/dltConnector/model/IdentifierSeed.ts b/backend/src/apis/dltConnector/model/IdentifierSeed.ts new file mode 100644 index 000000000..c597d2797 --- /dev/null +++ b/backend/src/apis/dltConnector/model/IdentifierSeed.ts @@ -0,0 +1,7 @@ +export class IdentifierSeed { + seed: string + + constructor(seed: string) { + this.seed = seed + } +} diff --git a/backend/src/apis/dltConnector/model/TransactionDraft.ts b/backend/src/apis/dltConnector/model/TransactionDraft.ts index e6b8f8d6d..7413969eb 100755 --- a/backend/src/apis/dltConnector/model/TransactionDraft.ts +++ b/backend/src/apis/dltConnector/model/TransactionDraft.ts @@ -1,38 +1,52 @@ // https://www.npmjs.com/package/@apollo/protobufjs import { Transaction } from '@entity/Transaction' +import { TransactionLink } from '@entity/TransactionLink' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { LogError } from '@/server/LogError' +import { IdentifierSeed } from './IdentifierSeed' import { UserIdentifier } from './UserIdentifier' export class TransactionDraft { user: UserIdentifier - linkedUser: UserIdentifier + linkedUser: UserIdentifier | IdentifierSeed amount: string type: string createdAt: string // only for creation transactions targetDate?: string + // only for transaction links + timeoutDate?: string - constructor(transaction: Transaction) { - if ( - !transaction.linkedUserGradidoID || - !transaction.linkedUserCommunityUuid || - !transaction.userCommunityUuid - ) { - throw new LogError( - `missing necessary field in transaction: ${transaction.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`, - ) - } - this.user = new UserIdentifier(transaction.userGradidoID, transaction.userCommunityUuid) - this.linkedUser = new UserIdentifier( - transaction.linkedUserGradidoID, - transaction.linkedUserCommunityUuid, - ) + constructor(transaction: Transaction | TransactionLink) { this.amount = transaction.amount.abs().toString() - this.type = TransactionTypeId[transaction.typeId] - this.createdAt = transaction.balanceDate.toISOString() - this.targetDate = transaction.creationDate?.toISOString() + + if (transaction instanceof Transaction) { + if ( + !transaction.linkedUserGradidoID || + !transaction.linkedUserCommunityUuid || + !transaction.userCommunityUuid + ) { + throw new LogError( + `missing necessary field in transaction: ${transaction.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`, + ) + } + this.user = new UserIdentifier(transaction.userGradidoID, transaction.userCommunityUuid) + this.linkedUser = new UserIdentifier( + transaction.linkedUserGradidoID, + transaction.linkedUserCommunityUuid, + ) + this.createdAt = transaction.balanceDate.toISOString() + this.targetDate = transaction.creationDate?.toISOString() + this.type = TransactionTypeId[transaction.typeId] + } else if (transaction instanceof TransactionLink) { + const user = transaction.user + this.user = new UserIdentifier(user.gradidoID, user.communityUuid) + this.linkedUser = new IdentifierSeed(transaction.code) + this.createdAt = transaction.createdAt.toISOString() + this.type = TransactionTypeId[TransactionTypeId.LINK_SUMMARY] + this.timeoutDate = transaction.validUntil.toISOString() + } } } diff --git a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts b/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts index 36974b5cf..c9a192189 100644 --- a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts +++ b/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts @@ -1,6 +1,9 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { backendLogger as logger } from '@/server/logger' +import { BaseEntity, EntityPropertyNotFoundError, EntityTarget, OrderByCondition, SelectQueryBuilder } from '@dbTools/typeorm' import { DltTransaction } from '@entity/DltTransaction' -import { DltUser } from '@entity/DltUser' import { Transaction } from '@entity/Transaction' +import { TransactionLink } from '@entity/TransactionLink' import { User } from '@entity/User' // eslint-disable-next-line import/named, n/no-extraneous-import import { FetchError } from 'node-fetch' @@ -8,11 +11,11 @@ import { FetchError } from 'node-fetch' import { DltConnectorClient } from '@dltConnector/DltConnectorClient' import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult' -import { backendLogger as logger } from '@/server/logger' import { InterruptiveSleepManager, TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, } from '@/util/InterruptiveSleepManager' +import { LogError } from '@/server/LogError' let isLoopRunning = true @@ -20,70 +23,105 @@ export const stopSendTransactionsToDltConnector = (): void => { isLoopRunning = false } -function logTransactionResult( - type: 'dltUser' | 'dltTransaction', - data: { id: number; messageId: string; error: string | null }, -): void { +interface NextPendingTransactionQueries { + lastTransactionQuery: SelectQueryBuilder + lastUserQuery: SelectQueryBuilder + lastTransactionLinkQuery: SelectQueryBuilder +} + +function logTransactionResult(data: { id: number; messageId: string; error: string | null }): void { if (data.error) { - logger.error(`Store ${type} with error: id=${data.id}, error=${data.error}`) + logger.error(`Store dltTransaction with error: id=${data.id}, error=${data.error}`) } else { - logger.info(`Store ${type}: messageId=${data.messageId}, id=${data.id}`) + logger.info(`Store dltTransaction: messageId=${data.messageId}, id=${data.id}`) } } async function saveTransactionResult( - pendingTransaction: User | Transaction, + pendingTransaction: User | Transaction | TransactionLink, messageId: string, error: string | null, ): Promise { + const dltTransaction = DltTransaction.create() + dltTransaction.messageId = messageId + dltTransaction.error = error if (pendingTransaction instanceof User) { - const dltUser = DltUser.create() - dltUser.userId = pendingTransaction.id - dltUser.messageId = messageId - dltUser.error = error - await DltUser.save(dltUser) - logTransactionResult('dltUser', dltUser) + dltTransaction.userId = pendingTransaction.id } else if (pendingTransaction instanceof Transaction) { - const dltTransaction = DltTransaction.create() dltTransaction.transactionId = pendingTransaction.id - dltTransaction.messageId = messageId - dltTransaction.error = error - await DltTransaction.save(dltTransaction) - logTransactionResult('dltTransaction', dltTransaction) + } else if (pendingTransaction instanceof TransactionLink) { + dltTransaction.transactionLinkId = pendingTransaction.id } + await DltTransaction.save(dltTransaction) + logTransactionResult(dltTransaction) } -async function findNextPendingTransaction(): Promise { - const lastTransactionPromise: Promise = Transaction.createQueryBuilder() - .leftJoin(DltTransaction, 'dltTransaction', 'Transaction.id = dltTransaction.transactionId') - .where('dltTransaction.transaction_id IS NULL') - // eslint-disable-next-line camelcase - .orderBy({ balance_date: 'ASC', Transaction_id: 'ASC' }) - .limit(1) - .getOne() - - const lastUserPromise: Promise = User.createQueryBuilder() - .leftJoin(DltUser, 'dltUser', 'User.id = dltUser.userId') - .where('dltUser.user_id IS NULL') - // eslint-disable-next-line camelcase - .orderBy({ User_created_at: 'ASC', User_id: 'ASC' }) - .limit(1) - .getOne() - - const results = await Promise.all([lastTransactionPromise, lastUserPromise]) - if (results[0] && results[1]) { - return results[0].balanceDate < results[1].createdAt ? results[0] : results[1] - } else if (results[0]) { - return results[0] - } else if (results[1]) { - return results[1] +async function findNextPendingTransaction(): Promise { + // Helper function to avoid code repetition + const createQueryForPendingItems = ( + qb: SelectQueryBuilder, + joinCondition: string, + orderBy: OrderByCondition, + ): Promise => { + 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) + .limit(1) + .getOne() } - return null + + const lastTransactionPromise = createQueryForPendingItems( + Transaction.createQueryBuilder(), + 'Transaction.id = dltTransaction.transactionId', + // eslint-disable-next-line camelcase + { balance_date: 'ASC', Transaction_id: 'ASC' }, + ) + + const lastUserPromise = createQueryForPendingItems( + User.createQueryBuilder(), + 'User.id = dltTransaction.userId', + // eslint-disable-next-line camelcase + { User_created_at: 'ASC', User_id: 'ASC' }, + ) + + const lastTransactionLinkPromise = createQueryForPendingItems( + TransactionLink.createQueryBuilder().leftJoinAndSelect('transactionLink.user', 'user'), + 'TransactionLink.id = dltTransaction.transactionLinkId', + // eslint-disable-next-line camelcase + { TransactionLinkId_created_at: 'ASC', User_id: 'ASC' }, + ) + + const results = await Promise.all([ + lastTransactionPromise, + lastUserPromise, + lastTransactionLinkPromise, + ]) + + results.sort((a, b) => { + const getTime = (input: Transaction | User | TransactionLink | null) => { + if (!input) return Infinity + if (input instanceof Transaction) { + return input.balanceDate.getTime() + } else if (input instanceof User || input instanceof TransactionLink) { + return input.createdAt.getTime() + } + return Infinity + } + return getTime(a) - getTime(b) + }) + return results[0] ?? null } async function processPendingTransactions(dltConnector: DltConnectorClient): Promise { - let pendingTransaction: Transaction | User | null = null - while ((pendingTransaction = await findNextPendingTransaction())) { + let pendingTransaction: Transaction | User | TransactionLink | null = null + do { + pendingTransaction = await findNextPendingTransaction() + if (!pendingTransaction) { + return + } let result: TransactionResult | undefined let messageId = '' let error: string | null = null @@ -103,7 +141,7 @@ async function processPendingTransactions(dltConnector: DltConnectorClient): Pro } await saveTransactionResult(pendingTransaction, messageId, error) - } + } while (pendingTransaction) } export async function sendTransactionsToDltConnector(): Promise { @@ -114,9 +152,11 @@ export async function sendTransactionsToDltConnector(): Promise { isLoopRunning = false return } - logger.info('Starting sendTransactionsToDltConnector task') + // define outside of loop for reuse and reducing gb collection + // const queries = getFindNextPendingTransactionQueries() + // eslint-disable-next-line no-unmodified-loop-condition while (isLoopRunning) { try { @@ -127,6 +167,9 @@ export async function sendTransactionsToDltConnector(): Promise { 1000, ) } catch (e) { + if (e instanceof EntityPropertyNotFoundError) { + throw new LogError(e.message, e.stack) + } // 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)}`) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 8a5c92cf4..88f84b6e2 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -38,8 +38,6 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' -import { sendTransactionsToDltConnector } from '../../apis/dltConnector/sendTransactionsToDltConnector' - import { executeTransaction } from './TransactionResolver' import { getUserCreation, validateContribution } from './util/creations' import { getLastTransaction } from './util/getLastTransaction' @@ -311,8 +309,6 @@ export class TransactionLinkResolver { } finally { releaseLock() } - // trigger to send transaction via dlt-connector - void sendTransactionsToDltConnector() return true } else { const now = new Date() diff --git a/backend/src/index.ts b/backend/src/index.ts index dd166d3d5..f2026a0f9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,7 +1,7 @@ +import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransactionsToDltConnector' import { CONFIG } from './config' import { startValidateCommunities } from './federation/validateCommunities' import { createServer } from './server/createServer' -import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransactionsToDltConnector' async function main() { const { app } = await createServer() diff --git a/database/entity/0088-merge_dlt_tables/TransactionLink.ts b/database/entity/0088-merge_dlt_tables/TransactionLink.ts index 3258e346f..72c80195b 100644 --- a/database/entity/0088-merge_dlt_tables/TransactionLink.ts +++ b/database/entity/0088-merge_dlt_tables/TransactionLink.ts @@ -10,6 +10,7 @@ import { } from 'typeorm' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' import { DltTransaction } from '../DltTransaction' +import { User } from '../User' @Entity('transaction_links') export class TransactionLink extends BaseEntity { @@ -71,4 +72,8 @@ export class TransactionLink extends BaseEntity { @OneToOne(() => DltTransaction, (dlt) => dlt.transactionLinkId) @JoinColumn({ name: 'id', referencedColumnName: 'transactionLinkId' }) dltTransaction?: DltTransaction | null + + @OneToOne(() => User, (user) => user.transactionLink) + @JoinColumn({ name: 'userId' }) + user: User } diff --git a/database/entity/0088-merge_dlt_tables/User.ts b/database/entity/0088-merge_dlt_tables/User.ts index d133cdad8..64a6261ab 100644 --- a/database/entity/0088-merge_dlt_tables/User.ts +++ b/database/entity/0088-merge_dlt_tables/User.ts @@ -17,6 +17,7 @@ import { UserRole } from '../UserRole' import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer' import { Community } from '../Community' import { DltTransaction } from '../DltTransaction' +import { TransactionLink } from './TransactionLink' @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class User extends BaseEntity { @@ -178,4 +179,8 @@ export class User extends BaseEntity { @OneToOne(() => DltTransaction, (dlt) => dlt.userId) @JoinColumn({ name: 'id', referencedColumnName: 'userId' }) dltTransaction?: DltTransaction | null + + @OneToOne(() => TransactionLink, (transactionLink) => transactionLink.userId) + @JoinColumn({ name: 'id', referencedColumnName: 'userId' }) + transactionLink?: TransactionLink | null } diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index 3f1f57a85..9a702aee3 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -35,4 +35,9 @@ export class TransactionDraft { @Field(() => String, { nullable: true }) @isValidDateString() targetDate?: string + + // only for transaction links + @Field(() => String, { nullable: true }) + @isValidDateString() + timeoutDate?: string } diff --git a/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts b/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts new file mode 100644 index 000000000..48c025b09 --- /dev/null +++ b/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts @@ -0,0 +1,39 @@ +import { GradidoTransactionBuilder, GradidoTransfer, TransferAmount } from 'gradido-blockchain-js' + +import { LogError } from '@/server/LogError' +import { uuid4ToHash } from '@/utils/typeConverter' + +import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' + +import { TransferTransactionRole } from './TransferTransaction.role' + +export class DeferredTransferTransactionRole extends TransferTransactionRole { + public async getGradidoTransactionBuilder(): Promise { + const builder = new GradidoTransactionBuilder() + const senderKeyPair = await KeyPairCalculation(this.self.user) + const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser) + if (!this.self.timeoutDate) { + throw new LogError('timeoutDate date missing for deferred transfer transaction') + } + builder + .setCreatedAt(new Date(this.self.createdAt)) + .setMemo('dummy memo for transfer') + .setDeferredTransfer( + new GradidoTransfer( + new TransferAmount(senderKeyPair.getPublicKey(), this.self.amount.toString()), + recipientKeyPair.getPublicKey(), + ), + new Date(this.self.timeoutDate), + ) + const senderCommunity = this.self.user.communityUuid + const recipientCommunity = this.self.linkedUser.communityUuid + if (senderCommunity !== recipientCommunity) { + // we have a cross group transaction + builder + .setSenderCommunity(uuid4ToHash(senderCommunity).convertToHex()) + .setRecipientCommunity(uuid4ToHash(recipientCommunity).convertToHex()) + } + builder.sign(senderKeyPair) + return builder + } +} diff --git a/dlt-connector/src/interactions/sendToIota/TransferTransaction.role.ts b/dlt-connector/src/interactions/sendToIota/TransferTransaction.role.ts index e0749f002..1e5dde5dd 100644 --- a/dlt-connector/src/interactions/sendToIota/TransferTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToIota/TransferTransaction.role.ts @@ -8,7 +8,7 @@ import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.con import { AbstractTransactionRole } from './AbstractTransaction.role' export class TransferTransactionRole extends AbstractTransactionRole { - constructor(private self: TransactionDraft) { + constructor(protected self: TransactionDraft) { super() }