mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
finish implementing with DCI, test next
This commit is contained in:
parent
8ccc52ae0a
commit
d230985906
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -24,4 +24,4 @@ export class TransactionResult {
|
||||
|
||||
@Field(() => Boolean)
|
||||
succeed: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user