finish implementing with DCI, test next

This commit is contained in:
einhorn_b 2023-10-28 16:29:37 +02:00
parent 8ccc52ae0a
commit d230985906
11 changed files with 151 additions and 136 deletions

View File

@ -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<TransactionBuilder> {
@ -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
}
}

View File

@ -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,

View File

@ -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, {

View File

@ -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

View File

@ -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

View File

@ -24,4 +24,4 @@ export class TransactionResult {
@Field(() => Boolean)
succeed: boolean
}
}

View File

@ -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) {

View File

@ -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<TransactionResult> {
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 {

View File

@ -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<void> {
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<Community> {
}
public async addCommunity(communityDraft: CommunityDraft, topic: string): Promise<Community> {
const community = createHomeCommunity(communityDraft, topic)
createCommunityRootTransactionRecipe(communityDraft, community).storeAsTransaction(
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.manager.save(community)
},
)
return community.save()
public async store(): Promise<Community> {
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',
)
}
}
}

View File

@ -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<void> {
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')

View File

@ -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<TransactionRecipeRole> {
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<void>,
): Promise<TransactionResult> {
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
}
}