diff --git a/dlt-connector/src/data/Transaction.builder.ts b/dlt-connector/src/data/Transaction.builder.ts index 4b1b385ab..321dda76b 100644 --- a/dlt-connector/src/data/Transaction.builder.ts +++ b/dlt-connector/src/data/Transaction.builder.ts @@ -8,6 +8,7 @@ import { CommunityRepository } from './Community.repository' import { LogError } from '@/server/LogError' import { Account } from '@entity/Account' import { Community } from '@entity/Community' +import { TransactionBodyBuilder } from './proto/TransactionBody.builder' export class TransactionBuilder { private transaction: Transaction @@ -50,6 +51,10 @@ export class TransactionBuilder { return this.transaction } + public getSenderCommunity(): Community { + return this.transaction.senderCommunity + } + public setSigningAccount(signingAccount: Account): TransactionBuilder { this.transaction.signingAccount = signingAccount return this @@ -84,6 +89,11 @@ export class TransactionBuilder { return this } + public setBackendTransactionId(backendTransactionId: number): TransactionBuilder { + this.transaction.backendTransactionId = backendTransactionId + return this + } + public async setSenderCommunityFromSenderUser( senderUser: UserIdentifier, ): Promise { @@ -144,4 +154,19 @@ export class TransactionBuilder { this.transaction.bodyBytes ??= transactionBodyToBodyBytes(transactionBody) return this } + + public fromTransactionBodyBuilder( + transactionBodyBuilder: TransactionBodyBuilder, + ): TransactionBuilder { + const signingAccount = transactionBodyBuilder.getSigningAccount() + if (signingAccount) { + this.setSigningAccount(signingAccount) + } + const recipientAccount = transactionBodyBuilder.getRecipientAccount() + if (recipientAccount) { + this.setRecipientAccount(recipientAccount) + } + this.fromTransactionBody(transactionBodyBuilder.getTransactionBody()) + return this + } } diff --git a/dlt-connector/src/data/proto/TransactionBody.builder.ts b/dlt-connector/src/data/proto/TransactionBody.builder.ts index 5f2681824..b0e0a9f79 100644 --- a/dlt-connector/src/data/proto/TransactionBody.builder.ts +++ b/dlt-connector/src/data/proto/TransactionBody.builder.ts @@ -25,6 +25,8 @@ export class TransactionBodyBuilder { public reset(): void { this.body = undefined + this.signingAccount = undefined + this.recipientAccount = undefined } /** @@ -42,14 +44,26 @@ export class TransactionBodyBuilder { * client code before disposing of the previous result. */ public build(): TransactionBody { - const result = this.body - if (!result) { + const result = this.getTransactionBody() + this.reset() + return result + } + + public getTransactionBody(): TransactionBody { + if (!this.body) { throw new LogError( 'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft', ) } - this.reset() - return result + return this.body + } + + public getSigningAccount(): Account | undefined { + return this.signingAccount + } + + public getRecipientAccount(): Account | undefined { + return this.recipientAccount } public setSigningAccount(signingAccount: Account): TransactionBodyBuilder { @@ -67,11 +81,17 @@ export class TransactionBodyBuilder { // TODO: load pubkeys for sender and recipient user from db switch (transactionDraft.type) { case InputTransactionType.CREATION: + if (!this.recipientAccount) { + throw new LogError('missing recipient account for creation transaction!') + } this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount) this.body.data = 'gradidoCreation' break case InputTransactionType.SEND: case InputTransactionType.RECEIVE: + if (!this.recipientAccount || !this.signingAccount) { + throw new LogError('missing signing and/or recipient account for transfer transaction!') + } this.body.transfer = new GradidoTransfer( transactionDraft, this.signingAccount, diff --git a/dlt-connector/src/graphql/enum/TransactionErrorType.ts b/dlt-connector/src/graphql/enum/TransactionErrorType.ts index 7f1902c3d..8238eae08 100644 --- a/dlt-connector/src/graphql/enum/TransactionErrorType.ts +++ b/dlt-connector/src/graphql/enum/TransactionErrorType.ts @@ -7,6 +7,9 @@ export enum TransactionErrorType { DB_ERROR = 'DB Error', PROTO_DECODE_ERROR = 'Proto Decode Error', PROTO_ENCODE_ERROR = 'Proto Encode Error', + INVALID_SIGNATURE = 'Invalid Signature', + LOGIC_ERROR = 'Logic Error', + NOT_FOUND = 'Not found', } registerEnumType(TransactionErrorType, { diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index a94c57354..3cd497eec 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -2,11 +2,11 @@ import { Decimal } from 'decimal.js-light' import { InputTransactionType } from '@enum/InputTransactionType' -import { InputType, Field } from 'type-graphql' +import { InputType, Field, Int } from 'type-graphql' import { UserIdentifier } from './UserIdentifier' import { isValidDateString } from '@validator/DateString' import { IsPositiveDecimal } from '@validator/Decimal' -import { IsEnum, IsObject, ValidateNested } from 'class-validator' +import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' @InputType() export class TransactionDraft { @@ -20,6 +20,10 @@ export class TransactionDraft { @ValidateNested() recipientUser: UserIdentifier + @Field(() => Int) + @IsPositive() + backendTransactionId: number + @Field(() => Decimal) @IsPositiveDecimal() amount: Decimal diff --git a/dlt-connector/src/graphql/model/TransactionRecipe.ts b/dlt-connector/src/graphql/model/TransactionRecipe.ts index e5e1c74fc..3b37ba4ea 100644 --- a/dlt-connector/src/graphql/model/TransactionRecipe.ts +++ b/dlt-connector/src/graphql/model/TransactionRecipe.ts @@ -1,10 +1,10 @@ import { Field, Int, ObjectType } from 'type-graphql' -import { TransactionRecipe as TransactionRecipeEntity } from '@entity/TransactionRecipe' import { TransactionType } from '@enum/TransactionType' +import { Transaction } from '@entity/Transaction' @ObjectType() export class TransactionRecipe { - public constructor({ id, createdAt, type, senderCommunity }: TransactionRecipeEntity) { + public constructor({ id, createdAt, type, senderCommunity }: Transaction) { this.id = id this.createdAt = createdAt.toString() this.type = type diff --git a/dlt-connector/src/graphql/model/TransactionResult.ts b/dlt-connector/src/graphql/model/TransactionResult.ts index dfa28cf1b..bc443ad1f 100644 --- a/dlt-connector/src/graphql/model/TransactionResult.ts +++ b/dlt-connector/src/graphql/model/TransactionResult.ts @@ -24,4 +24,4 @@ export class TransactionResult { @Field(() => Boolean) succeed: boolean -} \ No newline at end of file +} diff --git a/dlt-connector/src/graphql/resolver/CommunityResolver.ts b/dlt-connector/src/graphql/resolver/CommunityResolver.ts index 2259f604c..9b6d8f3f2 100644 --- a/dlt-connector/src/graphql/resolver/CommunityResolver.ts +++ b/dlt-connector/src/graphql/resolver/CommunityResolver.ts @@ -59,7 +59,7 @@ export class CommunityResolver { // TODO: write tests to make sure that it doesn't throw const addCommunityContext = new AddCommunityContext(communityDraft, topic) try { - // actually run interaction, create community, accounts for foreign community and transactionRecipe + // actually run interaction, create community, accounts for foreign community and transactionRecipe await addCommunityContext.run() return new TransactionResult() } catch (error) { diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 96f6dff65..e2b558668 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,90 +1,39 @@ -import { Resolver, Query, Arg, Mutation } from 'type-graphql' +import { Resolver, Arg, Mutation } from 'type-graphql' import { TransactionDraft } from '@input/TransactionDraft' import { TransactionResult } from '../model/TransactionResult' import { TransactionError } from '../model/TransactionError' +import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context' +import { TransactionRecipe } from '../model/TransactionRecipe' +import { TransactionRepository } from '@/data/Transaction.repository' +import { TransactionErrorType } from '../enum/TransactionErrorType' @Resolver() export class TransactionResolver { - // Why a dummy function? - // to prevent this error by start: - // GeneratingSchemaError: Some errors occurred while generating GraphQL schema: - // Type Query must define one or more fields. - // it seems that at least one query must be defined - // https://github.com/ardatan/graphql-tools/issues/764 - @Query(() => String) - version(): string { - return '0.1' - } - @Mutation(() => TransactionResult) async sendTransaction( @Arg('data') - transaction: TransactionDraft, + transactionDraft: TransactionDraft, ): Promise { + const createTransactionRecipeContext = new CreateTransactionRecipeContext(transactionDraft) try { - logger.info('sendTransaction call', transaction) - const signingAccount = await findAccountByUserIdentifier(transaction.senderUser) - if (!signingAccount) { - throw new TransactionError( - TransactionErrorType.NOT_FOUND, - "couldn't found sender user account in db", + await createTransactionRecipeContext.run() + const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe() + transactionRecipe.save() + return new TransactionResult(new TransactionRecipe(transactionRecipe)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.code === 'ER_DUP_ENTRY') { + const existingRecipe = await TransactionRepository.findBySignature( + createTransactionRecipeContext.getTransactionRecipe().signature, ) - } - logger.info('signing account', signingAccount) - - const recipientAccount = await findAccountByUserIdentifier(transaction.recipientUser) - if (!recipientAccount) { - throw new TransactionError( - TransactionErrorType.NOT_FOUND, - "couldn't found recipient user account in db", - ) - } - logger.info('recipient account', recipientAccount) - - const body = createTransactionBody(transaction, signingAccount, recipientAccount) - logger.info('body', body) - const gradidoTransaction = createGradidoTransaction(body) - - const signingKeyPair = getKeyPair(signingAccount) - if (!signingKeyPair) { - throw new TransactionError( - TransactionErrorType.NOT_FOUND, - "couldn't found signing key pair", - ) - } - logger.info('key pair for signing', signingKeyPair) - - KeyManager.getInstance().sign(gradidoTransaction, [signingKeyPair]) - const recipeTransactionController = await TransactionRecipe.create({ - transaction: gradidoTransaction, - senderUser: transaction.senderUser, - recipientUser: transaction.recipientUser, - signingAccount, - recipientAccount, - backendTransactionId: transaction.backendTransactionId, - }) - try { - await recipeTransactionController.getTransactionRecipeEntity().save() - ConditionalSleepManager.getInstance().signal(TRANSMIT_TO_IOTA_SLEEP_CONDITION_KEY) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - if (error.code === 'ER_DUP_ENTRY' && body.type === CrossGroupType.LOCAL) { - const existingRecipe = await findBySignature( - gradidoTransaction.sigMap.sigPair[0].signature, + if (!existingRecipe) { + throw new TransactionError( + TransactionErrorType.LOGIC_ERROR, + "recipe cannot be added because signature exist but couldn't load this existing receipt", ) - if (!existingRecipe) { - throw new TransactionError( - TransactionErrorType.LOGIC_ERROR, - "recipe cannot be added because signature exist but couldn't load this existing receipt", - ) - } - return new TransactionResult(new TransactionRecipeOutput(existingRecipe)) - } else { - throw error } + return new TransactionResult(new TransactionRecipe(existingRecipe)) } - return new TransactionResult() - } catch (error) { if (error instanceof TransactionError) { return new TransactionResult(error) } else { diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 4ae946498..cd52d6de1 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -1,16 +1,19 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { Community } from '@entity/Community' import { CommunityRole } from './Community.role' -import { QueryRunner } from 'typeorm' import { Transaction } from '@entity/Transaction' import { KeyManager } from '@/controller/KeyManager' import { AccountFactory } from '@/data/Account.factory' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' +import { logger } from '@/server/logger' +import { TransactionError } from '@/graphql/model/TransactionError' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { getDataSource } from '@/typeorm/DataSource' export class HomeCommunityRole extends CommunityRole { private transactionRecipe: Transaction - public create(communityDraft: CommunityDraft, topic: string): void { + public async create(communityDraft: CommunityDraft, topic: string): Promise { super.create(communityDraft, topic) // generate key pair for signing transactions and deriving all keys for community const keyPair = KeyManager.generateKeyPair() @@ -26,22 +29,22 @@ export class HomeCommunityRole extends CommunityRole { const transactionRecipeContext = new CreateTransactionRecipeContext(communityDraft) transactionRecipeContext.setCommunity(this.self) - transactionRecipeContext.run() + await transactionRecipeContext.run() this.transactionRecipe = transactionRecipeContext.getTransactionRecipe() } - public store(): Promise { - - } - - public async addCommunity(communityDraft: CommunityDraft, topic: string): Promise { - const community = createHomeCommunity(communityDraft, topic) - - createCommunityRootTransactionRecipe(communityDraft, community).storeAsTransaction( - async (queryRunner: QueryRunner): Promise => { - await queryRunner.manager.save(community) - }, - ) - return community.save() + public async store(): Promise { + try { + return await getDataSource().transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.save(this.transactionRecipe) + return await transactionalEntityManager.save(this.self) + }) + } catch (error) { + logger.error('error saving home community into db: %s', error) + throw new TransactionError( + TransactionErrorType.DB_ERROR, + 'error saving home community into db', + ) + } } } diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts index 454dd5b41..a5da13dcd 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts @@ -27,7 +27,7 @@ export class CreateTransactionRecipeContext { this.community = community } - public getCommunity() { + public getCommunity(): Community | undefined { return this.community } @@ -35,9 +35,10 @@ export class CreateTransactionRecipeContext { return this.transactionRecipeRole.getTransaction() } - public run(): void { + public async run(): Promise { if (this.draft instanceof TransactionDraft) { - this.transactionRecipeRole = new TransactionRecipeRole().create(this.draft) + this.transactionRecipeRole = new TransactionRecipeRole() + await this.transactionRecipeRole.create(this.draft) } else if (this.draft instanceof CommunityDraft) { if (!this.community) { throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community was not set') diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts index 4a432f67e..4107a37b1 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -1,12 +1,12 @@ +import { AccountRepository } from '@/data/Account.repository' +import { KeyPair } from '@/data/KeyPair' import { TransactionBuilder } from '@/data/Transaction.builder' +import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { TransactionDraft } from '@/graphql/input/TransactionDraft' import { TransactionError } from '@/graphql/model/TransactionError' -import { TransactionRecipe } from '@/graphql/model/TransactionRecipe' -import { TransactionResult } from '@/graphql/model/TransactionResult' -import { logger } from '@/server/logger' -import { getDataSource } from '@/typeorm/DataSource' -import { QueryRunner } from 'typeorm' +import { sign } from '@/utils/cryptoHelper' +import { Transaction } from '@entity/Transaction' export class TransactionRecipeRole { protected transactionBuilder: TransactionBuilder @@ -14,40 +14,50 @@ export class TransactionRecipeRole { this.transactionBuilder = new TransactionBuilder() } - public create(transactionDraft: TransactionDraft): TransactionRecipeRole { + public async create(transactionDraft: TransactionDraft): Promise { + const senderUser = transactionDraft.senderUser + const recipientUser = transactionDraft.recipientUser + + // loading signing and recipient account + // TODO: look for ways to use only one db call for both + const signingAccount = await AccountRepository.findAccountByUserIdentifier(senderUser) + if (!signingAccount) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found sender user account in db", + ) + } + const recipientAccount = await AccountRepository.findAccountByUserIdentifier(recipientUser) + if (!recipientAccount) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found recipient user account in db", + ) + } + + // create proto transaction body + const transactionBodyBuilder = new TransactionBodyBuilder() + .setSigningAccount(signingAccount) + .setRecipientAccount(recipientAccount) + .fromTransactionDraft(transactionDraft) + // build transaction entity + + this.transactionBuilder + .fromTransactionBodyBuilder(transactionBodyBuilder) + .setBackendTransactionId(transactionDraft.backendTransactionId) + await this.transactionBuilder.setSenderCommunityFromSenderUser(senderUser) + if (recipientUser.uuid !== senderUser.uuid) { + await this.transactionBuilder.setRecipientCommunityFromRecipientUser(recipientUser) + } + const transaction = this.transactionBuilder.getTransaction() + // sign + this.transactionBuilder.setSignature( + sign(transaction.bodyBytes, new KeyPair(this.transactionBuilder.getSenderCommunity())), + ) return this } public getTransaction(): Transaction { return this.transactionBuilder.getTransaction() } - - public async storeAsTransaction( - transactionFunction: (queryRunner: QueryRunner) => Promise, - ): Promise { - const queryRunner = getDataSource().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction() - - let result: TransactionResult - try { - const transactionRecipe = this.transactionBuilder.build() - await transactionFunction(queryRunner) - await queryRunner.manager.save(transactionRecipe) - await queryRunner.commitTransaction() - result = new TransactionResult(new TransactionRecipe(transactionRecipe)) - } catch (err) { - logger.error('error saving new transaction recipe into db: %s', err) - result = new TransactionResult( - new TransactionError( - TransactionErrorType.DB_ERROR, - 'error saving transaction recipe into db', - ), - ) - await queryRunner.rollbackTransaction() - } finally { - await queryRunner.release() - } - return result - } }