From e691db05b74b79145e58d4a589460a53a26e19b6 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 13 Nov 2024 14:22:04 +0100 Subject: [PATCH] move code into interaction in backend, implement transaction link in dlt-connector --- .../dltConnector/DltConnectorClient.test.ts | 5 +- .../apis/dltConnector/DltConnectorClient.ts | 22 +-- .../AbstractTransactionToDlt.role.ts | 63 ++++++++ .../TransactionLinkToDlt.role.ts | 45 ++++++ .../transactionToDlt/TransactionToDlt.role.ts | 45 ++++++ .../transactionToDlt/UserToDlt.role.ts | 45 ++++++ .../transactionToDlt.context.ts | 65 +++++++++ .../sendTransactionsToDltConnector.ts | 136 +----------------- backend/src/util/InterruptiveSleep.ts | 10 +- backend/src/util/utilities.ts | 4 + .../src/graphql/input/IdentifierSeed.ts | 9 ++ .../src/graphql/input/TransactionDraft.ts | 3 +- .../KeyPairCalculation.context.ts | 71 ++++----- .../LinkedTransactionKeyPair.role.ts | 22 +++ .../sendToIota/CreationTransaction.role.ts | 11 +- .../DeferredTransferTransaction.role.ts | 24 ++-- .../sendToIota/TransferTransaction.role.ts | 9 ++ .../logging/TransactionDraftLogging.view.ts | 6 +- .../src/manager/KeyPairCacheManager.ts | 12 +- dlt-connector/yarn.lock | 41 +----- 20 files changed, 400 insertions(+), 248 deletions(-) create mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts create mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts create mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts create mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts create mode 100644 backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts create mode 100644 dlt-connector/src/graphql/input/IdentifierSeed.ts create mode 100644 dlt-connector/src/interactions/keyPairCalculation/LinkedTransactionKeyPair.role.ts diff --git a/backend/src/apis/dltConnector/DltConnectorClient.test.ts b/backend/src/apis/dltConnector/DltConnectorClient.test.ts index d99093a1b..8169be112 100644 --- a/backend/src/apis/dltConnector/DltConnectorClient.test.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.test.ts @@ -14,6 +14,7 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { DltConnectorClient } from './DltConnectorClient' +import { TransactionDraft } from './model/TransactionDraft' let con: Connection @@ -113,7 +114,9 @@ describe('transmitTransaction', () => { const localTransaction = new DbTransaction() localTransaction.typeId = 12 try { - await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction) + await DltConnectorClient.getInstance()?.transmitTransaction( + new TransactionDraft(localTransaction), + ) } catch (e) { expect(e).toMatchObject( new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()), diff --git a/backend/src/apis/dltConnector/DltConnectorClient.ts b/backend/src/apis/dltConnector/DltConnectorClient.ts index b8f2dbbe3..f864bb652 100644 --- a/backend/src/apis/dltConnector/DltConnectorClient.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.ts @@ -1,6 +1,3 @@ -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 import { FetchError } from 'node-fetch' @@ -9,6 +6,7 @@ import { CONFIG } from '@/config' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' +// eslint-disable-next-line import/named, n/no-extraneous-import import { TransactionDraft } from './model/TransactionDraft' import { TransactionResult } from './model/TransactionResult' @@ -100,17 +98,6 @@ export class DltConnectorClient { return DltConnectorClient.instance } - 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) - } - throw new LogError('transaction should be either Transaction or User Entity') - } - private handleTransactionResult(result: TransactionResult) { if (result.error) { throw new Error(result.error.message) @@ -141,17 +128,16 @@ export class DltConnectorClient { * and update dltTransactionId of transaction in db with iota message id */ public async transmitTransaction( - transaction: DbTransaction | User | TransactionLink, + input: TransactionDraft | UserAccountDraft, ): Promise { // we don't need the receive transactions, there contain basically the same data as the send transactions if ( - transaction instanceof DbTransaction && - (transaction.typeId as TransactionTypeId) === TransactionTypeId.RECEIVE + input instanceof TransactionDraft && + TransactionTypeId[input.type as keyof typeof TransactionTypeId] === TransactionTypeId.RECEIVE ) { return } - const input = this.getTransactionParams(transaction) try { logger.debug('transmit transaction or user to dlt connector', input) if (input instanceof TransactionDraft) { diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts new file mode 100644 index 000000000..50296244e --- /dev/null +++ b/backend/src/apis/dltConnector/interaction/transactionToDlt/AbstractTransactionToDlt.role.ts @@ -0,0 +1,63 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from '@dbTools/typeorm' +import { DltTransaction } from '@entity/DltTransaction' + +import { TransactionDraft } from '@dltConnector/model/TransactionDraft' +import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft' + +import { backendLogger as logger } from '@/server/logger' + +export abstract class AbstractTransactionToDltRole { + protected self: T | null + + // public interface + public abstract initWithLast(): Promise + public abstract getTimestamp(): number + public abstract convertToGraphqlInput(): TransactionDraft | UserAccountDraft + 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.setJoinId(dltTransaction) + await DltTransaction.save(dltTransaction) + if (dltTransaction.error) { + logger.error( + `Store dltTransaction with error: id=${dltTransaction.id}, error=${dltTransaction.error}`, + ) + } else { + logger.info( + `Store dltTransaction: messageId=${dltTransaction.messageId}, id=${dltTransaction.id}`, + ) + } + } + + // intern + protected abstract setJoinId(dltTransaction: DltTransaction): void + + // helper + protected 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() + } + + 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/TransactionLinkToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts new file mode 100644 index 000000000..a30d86f65 --- /dev/null +++ b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionLinkToDlt.role.ts @@ -0,0 +1,45 @@ +import { DltTransaction } from '@entity/DltTransaction' +import { TransactionLink } from '@entity/TransactionLink' + +import { TransactionDraft } from '@dltConnector/model/TransactionDraft' +import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft' + +import { LogError } from '@/server/LogError' + +import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' + +/** + * send transactionLink as Deferred Transfers + */ +export class TransactionLinkToDltRole extends AbstractTransactionToDltRole { + async initWithLast(): Promise { + this.self = await this.createQueryForPendingItems( + TransactionLink.createQueryBuilder().leftJoinAndSelect('transactionLink.user', 'user'), + 'TransactionLink.id = dltTransaction.transactionLinkId', + // eslint-disable-next-line camelcase + { TransactionLinkId_created_at: 'ASC', User_id: 'ASC' }, + ) + return this + } + + public getTimestamp(): number { + if (!this.self) { + return Infinity + } + return this.self.createdAt.getTime() + } + + public convertToGraphqlInput(): TransactionDraft | UserAccountDraft { + if (!this.self) { + throw new LogError('try to create dlt entry for empty transaction link') + } + return new TransactionDraft(this.self) + } + + protected setJoinId(dltTransaction: DltTransaction): void { + if (!this.self) { + throw new LogError('try to create dlt entry for empty transaction link') + } + dltTransaction.transactionLinkId = this.self.id + } +} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts new file mode 100644 index 000000000..f777f2683 --- /dev/null +++ b/backend/src/apis/dltConnector/interaction/transactionToDlt/TransactionToDlt.role.ts @@ -0,0 +1,45 @@ +import { DltTransaction } from '@entity/DltTransaction' +import { Transaction } from '@entity/Transaction' + +import { TransactionDraft } from '@dltConnector/model/TransactionDraft' +import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft' + +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 { + async initWithLast(): Promise { + this.self = await this.createQueryForPendingItems( + Transaction.createQueryBuilder(), + 'Transaction.id = dltTransaction.transactionId', + // eslint-disable-next-line camelcase + { balance_date: 'ASC', Transaction_id: 'ASC' }, + ) + return this + } + + public getTimestamp(): number { + if (!this.self) { + return Infinity + } + return this.self.balanceDate.getTime() + } + + public convertToGraphqlInput(): TransactionDraft | UserAccountDraft { + if (!this.self) { + throw new LogError('try to create dlt entry for empty transaction') + } + return new TransactionDraft(this.self) + } + + protected setJoinId(dltTransaction: DltTransaction): void { + if (!this.self) { + throw new LogError('try to create dlt entry for empty transaction') + } + dltTransaction.transactionId = this.self.id + } +} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts new file mode 100644 index 000000000..fc3b0b2c3 --- /dev/null +++ b/backend/src/apis/dltConnector/interaction/transactionToDlt/UserToDlt.role.ts @@ -0,0 +1,45 @@ +import { DltTransaction } from '@entity/DltTransaction' +import { User } from '@entity/User' + +import { TransactionDraft } from '@dltConnector/model/TransactionDraft' +import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft' + +import { LogError } from '@/server/LogError' + +import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' + +/** + * 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(), + 'User.id = dltTransaction.userId', + // eslint-disable-next-line camelcase + { User_created_at: 'ASC', User_id: 'ASC' }, + ) + return this + } + + public getTimestamp(): number { + if (!this.self) { + return Infinity + } + return this.self.createdAt.getTime() + } + + public convertToGraphqlInput(): TransactionDraft | UserAccountDraft { + if (!this.self) { + throw new LogError('try to create dlt entry for empty transaction') + } + return new UserAccountDraft(this.self) + } + + protected setJoinId(dltTransaction: DltTransaction): void { + if (!this.self) { + throw new LogError('try to create dlt entry for empty user') + } + dltTransaction.userId = this.self.id + } +} diff --git a/backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts b/backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts new file mode 100644 index 000000000..57281f96b --- /dev/null +++ b/backend/src/apis/dltConnector/interaction/transactionToDlt/transactionToDlt.context.ts @@ -0,0 +1,65 @@ +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' + +import { DltConnectorClient } from '@/apis/dltConnector/DltConnectorClient' +import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult' + +import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role' +import { TransactionLinkToDltRole } from './TransactionLinkToDlt.role' +import { TransactionToDltRole } from './TransactionToDlt.role' +import { UserToDltRole } from './UserToDlt.role' + +/** + * @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(): Promise< + AbstractTransactionToDltRole + > { + // collect each oldest not sended entity from db and choose oldest + const results = await Promise.all([ + new TransactionToDltRole().initWithLast(), + new UserToDltRole().initWithLast(), + new TransactionLinkToDltRole().initWithLast(), + ]) + + // sort array to get oldest at first place + results.sort((a, b) => { + return a.getTimestamp() - b.getTimestamp() + }) + return results[0] + } + + while (true) { + const pendingTransactionRole = await findNextPendingTransaction() + const pendingTransaction = pendingTransactionRole.getEntity() + if (!pendingTransaction) { + break + } + let result: TransactionResult | undefined + let messageId = '' + let error: string | null = null + + try { + result = await dltConnector.transmitTransaction( + pendingTransactionRole.convertToGraphqlInput(), + ) + if (result?.succeed && result.recipe) { + messageId = result.recipe.messageIdHex + } else { + error = 'skipped' + } + } catch (e) { + if (e instanceof FetchError) { + throw e + } + error = e instanceof Error ? e.message : String(e) + } + + await pendingTransactionRole.saveTransactionResult(messageId, error) + } +} diff --git a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts b/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts index c9a192189..7e0abf0fa 100644 --- a/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts +++ b/backend/src/apis/dltConnector/sendTransactionsToDltConnector.ts @@ -1,21 +1,18 @@ // 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 { Transaction } from '@entity/Transaction' -import { TransactionLink } from '@entity/TransactionLink' -import { User } from '@entity/User' +import { EntityPropertyNotFoundError } from '@dbTools/typeorm' // eslint-disable-next-line import/named, n/no-extraneous-import import { FetchError } from 'node-fetch' import { DltConnectorClient } from '@dltConnector/DltConnectorClient' -import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' import { InterruptiveSleepManager, TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, } from '@/util/InterruptiveSleepManager' -import { LogError } from '@/server/LogError' + +import { transactionToDlt } from './interaction/transactionToDlt/transactionToDlt.context' let isLoopRunning = true @@ -23,127 +20,6 @@ export const stopSendTransactionsToDltConnector = (): void => { isLoopRunning = false } -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 dltTransaction with error: id=${data.id}, error=${data.error}`) - } else { - logger.info(`Store dltTransaction: messageId=${data.messageId}, id=${data.id}`) - } -} - -async function saveTransactionResult( - pendingTransaction: User | Transaction | TransactionLink, - messageId: string, - error: string | null, -): Promise { - const dltTransaction = DltTransaction.create() - dltTransaction.messageId = messageId - dltTransaction.error = error - if (pendingTransaction instanceof User) { - dltTransaction.userId = pendingTransaction.id - } else if (pendingTransaction instanceof Transaction) { - dltTransaction.transactionId = pendingTransaction.id - } else if (pendingTransaction instanceof TransactionLink) { - dltTransaction.transactionLinkId = pendingTransaction.id - } - await DltTransaction.save(dltTransaction) - logTransactionResult(dltTransaction) -} - -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() - } - - 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 | TransactionLink | null = null - do { - pendingTransaction = await findNextPendingTransaction() - if (!pendingTransaction) { - return - } - let result: TransactionResult | undefined - let messageId = '' - let error: string | null = null - - try { - result = await dltConnector.transmitTransaction(pendingTransaction) - if (result?.succeed && result.recipe) { - messageId = result.recipe.messageIdHex - } else { - error = 'skipped' - } - } catch (e) { - if (e instanceof FetchError) { - throw e - } - error = e instanceof Error ? e.message : String(e) - } - - await saveTransactionResult(pendingTransaction, messageId, error) - } while (pendingTransaction) -} - export async function sendTransactionsToDltConnector(): Promise { const dltConnector = DltConnectorClient.getInstance() @@ -161,7 +37,7 @@ export async function sendTransactionsToDltConnector(): Promise { while (isLoopRunning) { try { // return after no pending transactions are left - await processPendingTransactions(dltConnector) + await transactionToDlt(dltConnector) await InterruptiveSleepManager.getInstance().sleep( TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, 1000, diff --git a/backend/src/util/InterruptiveSleep.ts b/backend/src/util/InterruptiveSleep.ts index c21e57db9..dc8ed5ae0 100644 --- a/backend/src/util/InterruptiveSleep.ts +++ b/backend/src/util/InterruptiveSleep.ts @@ -1,3 +1,5 @@ +import { delay } from './utilities' + /** * Sleep, that can be interrupted * call sleep only for msSteps and than check if interrupt was called @@ -14,17 +16,11 @@ export class InterruptiveSleep { this.interruptSleep = true } - private static _sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) - } - public async sleep(ms: number): Promise { let waited = 0 this.interruptSleep = false while (waited < ms && !this.interruptSleep) { - await InterruptiveSleep._sleep(this.msSteps) + await delay(this.msSteps) waited += this.msSteps } } diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 905cce686..4f45af023 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,3 +1,5 @@ +import { promisify } from 'util' + import { Decimal } from 'decimal.js-light' import i18n from 'i18n' @@ -30,6 +32,8 @@ export function resetInterface>(obj: T): T { return obj } +export const delay = promisify(setTimeout) + export const ensureUrlEndsWithSlash = (url: string): string => { return url.endsWith('/') ? url : url.concat('/') } diff --git a/dlt-connector/src/graphql/input/IdentifierSeed.ts b/dlt-connector/src/graphql/input/IdentifierSeed.ts new file mode 100644 index 000000000..41e7a31cd --- /dev/null +++ b/dlt-connector/src/graphql/input/IdentifierSeed.ts @@ -0,0 +1,9 @@ +import { IsString } from 'class-validator' +import { InputType, Field } from 'type-graphql' + +@InputType() +export class IdentifierSeed { + @Field(() => String) + @IsString() + seed: string +} diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index 9a702aee3..393d5da93 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -5,6 +5,7 @@ import { InputType, Field } from 'type-graphql' import { InputTransactionType } from '@enum/InputTransactionType' import { isValidDateString, isValidNumberString } from '@validator/DateString' +import { IdentifierSeed } from './IdentifierSeed' import { UserIdentifier } from './UserIdentifier' @InputType() @@ -17,7 +18,7 @@ export class TransactionDraft { @Field(() => UserIdentifier) @IsObject() @ValidateNested() - linkedUser: UserIdentifier + linkedUser: UserIdentifier | IdentifierSeed @Field(() => String) @isValidNumberString() diff --git a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts b/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts index 012168bb0..ca54d8d16 100644 --- a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts +++ b/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts @@ -1,12 +1,13 @@ import { KeyPairEd25519 } from 'gradido-blockchain-js' +import { IdentifierSeed } from '@/graphql/input/IdentifierSeed' import { UserIdentifier } from '@/graphql/input/UserIdentifier' import { KeyPairCacheManager } from '@/manager/KeyPairCacheManager' -import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role' import { AccountKeyPairRole } from './AccountKeyPair.role' import { ForeignCommunityKeyPairRole } from './ForeignCommunityKeyPair.role' import { HomeCommunityKeyPairRole } from './HomeCommunityKeyPair.role' +import { LinkedTransactionKeyPairRole } from './LinkedTransactionKeyPair.role' import { RemoteAccountKeyPairRole } from './RemoteAccountKeyPair.role' import { UserKeyPairRole } from './UserKeyPair.role' @@ -14,43 +15,49 @@ import { UserKeyPairRole } from './UserKeyPair.role' * @DCI-Context * Context for calculating key pair for signing transactions */ -export async function KeyPairCalculation(input: UserIdentifier | string): Promise { +export async function KeyPairCalculation( + input: UserIdentifier | string | IdentifierSeed, +): Promise { const cache = KeyPairCacheManager.getInstance() - const keyPair = cache.findKeyPair(input) + + // Try cache lookup first + let keyPair = cache.findKeyPair(input) if (keyPair) { return keyPair } - let communityUUID: string - if (input instanceof UserIdentifier) { - communityUUID = input.communityUuid - } else { - communityUUID = input - } - if (cache.getHomeCommunityUUID() !== communityUUID) { - // it isn't home community so we can only retrieve public keys - let role: AbstractRemoteKeyPairRole - if (input instanceof UserIdentifier) { - role = new RemoteAccountKeyPairRole(input) - } else { - role = new ForeignCommunityKeyPairRole(input) + const retrieveKeyPair = async ( + input: UserIdentifier | string | IdentifierSeed, + ): Promise => { + if (input instanceof IdentifierSeed) { + return new LinkedTransactionKeyPairRole(input.seed).generateKeyPair() } - const keyPair = await role.retrieveKeyPair() - cache.addKeyPair(input, keyPair) - return keyPair + + const communityUUID = input instanceof UserIdentifier ? input.communityUuid : input + + // If input does not belong to the home community, handle as remote key pair + if (cache.getHomeCommunityUUID() !== communityUUID) { + const role = + input instanceof UserIdentifier + ? new RemoteAccountKeyPairRole(input) + : new ForeignCommunityKeyPairRole(input) + return await role.retrieveKeyPair() + } + + let communityKeyPair = cache.findKeyPair(communityUUID) + if (!communityKeyPair) { + communityKeyPair = new HomeCommunityKeyPairRole().generateKeyPair() + cache.addKeyPair(communityUUID, communityKeyPair) + } + if (input instanceof UserIdentifier) { + const userKeyPair = new UserKeyPairRole(input, communityKeyPair).generateKeyPair() + const accountNr = input.accountNr ?? 1 + return new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair() + } + return communityKeyPair } - let communityKeyPair = cache.findKeyPair(communityUUID) - if (!communityKeyPair) { - communityKeyPair = new HomeCommunityKeyPairRole().generateKeyPair() - cache.addKeyPair(communityUUID, communityKeyPair) - } - if (input instanceof UserIdentifier) { - const userKeyPair = new UserKeyPairRole(input, communityKeyPair).generateKeyPair() - const accountNr = input.accountNr ?? 1 - const accountKeyPair = new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair() - cache.addKeyPair(input, accountKeyPair) - return accountKeyPair - } - return communityKeyPair + keyPair = await retrieveKeyPair(input) + cache.addKeyPair(input, keyPair) + return keyPair } diff --git a/dlt-connector/src/interactions/keyPairCalculation/LinkedTransactionKeyPair.role.ts b/dlt-connector/src/interactions/keyPairCalculation/LinkedTransactionKeyPair.role.ts new file mode 100644 index 000000000..39a20dd7a --- /dev/null +++ b/dlt-connector/src/interactions/keyPairCalculation/LinkedTransactionKeyPair.role.ts @@ -0,0 +1,22 @@ +import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js' + +import { LogError } from '@/server/LogError' + +import { AbstractKeyPairRole } from './AbstractKeyPair.role' + +export class LinkedTransactionKeyPairRole extends AbstractKeyPairRole { + public constructor(private seed: string) { + super() + } + + public generateKeyPair(): KeyPairEd25519 { + // seed is expected to be 24 bytes long, but we need 32 + // so hash the seed with blake2 and we have 32 Bytes + const hash = new MemoryBlock(this.seed).calculateHash() + const keyPair = KeyPairEd25519.create(hash) + if (!keyPair) { + throw new LogError('error creating Ed25519 KeyPair from seed', this.seed) + } + return keyPair + } +} diff --git a/dlt-connector/src/interactions/sendToIota/CreationTransaction.role.ts b/dlt-connector/src/interactions/sendToIota/CreationTransaction.role.ts index b7a074a7a..775c60cb7 100644 --- a/dlt-connector/src/interactions/sendToIota/CreationTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToIota/CreationTransaction.role.ts @@ -1,5 +1,6 @@ import { GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js' +import { IdentifierSeed } from '@/graphql/input/IdentifierSeed' import { TransactionDraft } from '@/graphql/input/TransactionDraft' import { LogError } from '@/server/LogError' @@ -21,12 +22,16 @@ export class CreationTransactionRole extends AbstractTransactionRole { } public async getGradidoTransactionBuilder(): Promise { - const builder = new GradidoTransactionBuilder() - const recipientKeyPair = await KeyPairCalculation(this.self.user) - const signerKeyPair = await KeyPairCalculation(this.self.linkedUser) + if (this.self.linkedUser instanceof IdentifierSeed) { + throw new LogError('invalid recipient, it is a IdentifierSeed instead of a UserIdentifier') + } if (!this.self.targetDate) { throw new LogError('target date missing for creation transaction') } + const builder = new GradidoTransactionBuilder() + const recipientKeyPair = await KeyPairCalculation(this.self.user) + const signerKeyPair = await KeyPairCalculation(this.self.linkedUser) + builder .setCreatedAt(new Date(this.self.createdAt)) .setMemo('dummy memo for creation') diff --git a/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts b/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts index 48c025b09..ff252f59d 100644 --- a/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToIota/DeferredTransferTransaction.role.ts @@ -1,20 +1,28 @@ import { GradidoTransactionBuilder, GradidoTransfer, TransferAmount } from 'gradido-blockchain-js' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' 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 { + getRecipientCommunityUuid(): string { + throw new LogError('cannot be used as cross group transaction') + } + 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.linkedUser instanceof UserIdentifier) { + throw new LogError('invalid recipient, it is a UserIdentifier instead of a IdentifierSeed') + } if (!this.self.timeoutDate) { throw new LogError('timeoutDate date missing for deferred transfer transaction') } + const builder = new GradidoTransactionBuilder() + const senderKeyPair = await KeyPairCalculation(this.self.user) + const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser) + builder .setCreatedAt(new Date(this.self.createdAt)) .setMemo('dummy memo for transfer') @@ -25,14 +33,6 @@ export class DeferredTransferTransactionRole extends TransferTransactionRole { ), 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 1e5dde5dd..16a2ec7ee 100644 --- a/dlt-connector/src/interactions/sendToIota/TransferTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToIota/TransferTransaction.role.ts @@ -1,6 +1,8 @@ import { GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js' +import { IdentifierSeed } from '@/graphql/input/IdentifierSeed' import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { LogError } from '@/server/LogError' import { uuid4ToHash } from '@/utils/typeConverter' import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' @@ -17,10 +19,17 @@ export class TransferTransactionRole extends AbstractTransactionRole { } getRecipientCommunityUuid(): string { + if (this.self.linkedUser instanceof IdentifierSeed) { + throw new LogError('invalid recipient, it is a IdentifierSeed instead of a UserIdentifier') + } return this.self.linkedUser.communityUuid } public async getGradidoTransactionBuilder(): Promise { + if (this.self.linkedUser instanceof IdentifierSeed) { + throw new LogError('invalid recipient, it is a IdentifierSeed instead of a UserIdentifier') + } + const builder = new GradidoTransactionBuilder() const senderKeyPair = await KeyPairCalculation(this.self.user) const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser) diff --git a/dlt-connector/src/logging/TransactionDraftLogging.view.ts b/dlt-connector/src/logging/TransactionDraftLogging.view.ts index b68f6d746..8f9e11331 100644 --- a/dlt-connector/src/logging/TransactionDraftLogging.view.ts +++ b/dlt-connector/src/logging/TransactionDraftLogging.view.ts @@ -1,5 +1,6 @@ import { InputTransactionType } from '@/graphql/enum/InputTransactionType' import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' import { getEnumValue } from '@/utils/typeConverter' import { AbstractLoggingView } from './AbstractLogging.view' @@ -14,7 +15,10 @@ export class TransactionDraftLoggingView extends AbstractLoggingView { public toJSON(): any { return { user: new UserIdentifierLoggingView(this.self.user).toJSON(), - linkedUser: new UserIdentifierLoggingView(this.self.linkedUser).toJSON(), + linkedUser: + this.self.linkedUser instanceof UserIdentifier + ? new UserIdentifierLoggingView(this.self.linkedUser).toJSON() + : 'seed', amount: Number(this.self.amount), type: getEnumValue(InputTransactionType, this.self.type), createdAt: this.self.createdAt, diff --git a/dlt-connector/src/manager/KeyPairCacheManager.ts b/dlt-connector/src/manager/KeyPairCacheManager.ts index f5c11e388..844f5ad58 100644 --- a/dlt-connector/src/manager/KeyPairCacheManager.ts +++ b/dlt-connector/src/manager/KeyPairCacheManager.ts @@ -1,5 +1,6 @@ import { KeyPairEd25519 } from 'gradido-blockchain-js' +import { IdentifierSeed } from '@/graphql/input/IdentifierSeed' import { UserIdentifier } from '@/graphql/input/UserIdentifier' import { LogError } from '@/server/LogError' @@ -44,11 +45,14 @@ export class KeyPairCacheManager { return this.homeCommunityUUID } - public findKeyPair(input: UserIdentifier | string): KeyPairEd25519 | undefined { + public findKeyPair(input: UserIdentifier | string | IdentifierSeed): KeyPairEd25519 | undefined { return this.cache.get(this.getKey(input)) } - public addKeyPair(input: UserIdentifier | string, keyPair: KeyPairEd25519): void { + public addKeyPair( + input: UserIdentifier | string | IdentifierSeed, + keyPair: KeyPairEd25519, + ): void { const key = this.getKey(input) if (this.cache.has(key)) { throw new LogError('key already exist, cannot add', key) @@ -56,9 +60,11 @@ export class KeyPairCacheManager { this.cache.set(key, keyPair) } - protected getKey(input: UserIdentifier | string): string { + protected getKey(input: UserIdentifier | string | IdentifierSeed): string { if (input instanceof UserIdentifier) { return input.uuid + } else if (input instanceof IdentifierSeed) { + return input.seed } else { return input } diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 1f60d65f2..c4f244816 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -1082,7 +1082,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/node-fetch@^2.6.1", "@types/node-fetch@^2.6.11": +"@types/node-fetch@^2.6.1": version "2.6.11" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== @@ -2112,11 +2112,6 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -2936,14 +2931,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -3063,13 +3050,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4690,11 +4670,6 @@ node-api-headers@^1.1.0: resolved "https://registry.yarnpkg.com/node-api-headers/-/node-api-headers-1.3.0.tgz#bb32c6b3e33fb0004bd93c66787bf00998c834ea" integrity sha512-8Bviwtw4jNhv0B2qDjj4M5e6GyAuGtxsmZTrFJu3S3Z0+oHwIgSUdIKkKJmZd+EbMo7g3v4PLBbrjxwmZOqMBg== -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -4702,15 +4677,6 @@ node-fetch@^2.6.12, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - node-gyp-build@^4.8.1: version "4.8.2" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa" @@ -6299,11 +6265,6 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"