refactor, add feature for register address

This commit is contained in:
einhornimmond 2024-05-03 15:20:59 +02:00
parent a3cc076e47
commit b9e138939b
16 changed files with 318 additions and 37 deletions

View File

@ -1,5 +1,6 @@
import { Account } from '@entity/Account'
import { User } from '@entity/User'
import { FindOptionsRelations } from 'typeorm'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { getDataSource } from '@/typeorm/DataSource'
@ -21,4 +22,11 @@ export const UserRepository = getDataSource()
return account
}
},
findByGradidoId(
{ uuid }: UserIdentifier,
relations?: FindOptionsRelations<User>,
): Promise<User | null> {
return User.findOne({ where: { gradidoID: uuid }, relations })
},
})

View File

@ -1,15 +1,35 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Account } from '@entity/Account'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { Field, Message } from 'protobufjs'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { accountTypeToAddressType } from '@/utils/typeConverter'
import { AbstractTransaction } from '../AbstractTransaction'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class RegisterAddress extends Message<RegisterAddress> implements AbstractTransaction {
constructor(transaction?: UserAccountDraft, account?: Account) {
if (transaction) {
super({ addressType: accountTypeToAddressType(transaction.accountType) })
if (account) {
this.derivationIndex = account.derivationIndex
this.accountPubkey = account.derive2Pubkey
if (account.user) {
this.userPubkey = account.user.derive1Pubkey
}
}
} else {
super()
}
}
@Field.d(1, 'bytes')
public userPubkey: Buffer

View File

@ -4,12 +4,14 @@ import { Field, Message, OneOf } from 'protobufjs'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { timestampToDate } from '@/utils/typeConverter'
import { AbstractTransaction } from '../AbstractTransaction'
import { determineCrossGroupType, determineOtherGroup } from '../transactionBody.logic'
import { CommunityRoot } from './CommunityRoot'
import { PROTO_TRANSACTION_BODY_VERSION_NUMBER } from './const'
@ -25,14 +27,21 @@ import { Timestamp } from './Timestamp'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class TransactionBody extends Message<TransactionBody> {
public constructor(transaction?: TransactionDraft | CommunityDraft) {
public constructor(transaction?: TransactionDraft | CommunityDraft | UserAccountDraft) {
if (transaction) {
let type = CrossGroupType.LOCAL
let otherGroup = ''
if (transaction instanceof TransactionDraft) {
type = determineCrossGroupType(transaction)
otherGroup = determineOtherGroup(type, transaction)
}
super({
memo: 'Not implemented yet',
createdAt: new Timestamp(new Date(transaction.createdAt)),
versionNumber: PROTO_TRANSACTION_BODY_VERSION_NUMBER,
type: CrossGroupType.LOCAL,
otherGroup: '',
type,
otherGroup,
})
} else {
super()

View File

@ -4,12 +4,14 @@ import { Community } from '@entity/Community'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { LogError } from '@/server/LogError'
import { CommunityRoot } from './3_3/CommunityRoot'
import { CrossGroupType } from './3_3/enum/CrossGroupType'
import { GradidoCreation } from './3_3/GradidoCreation'
import { GradidoTransfer } from './3_3/GradidoTransfer'
import { RegisterAddress } from './3_3/RegisterAddress'
import { TransactionBody } from './3_3/TransactionBody'
export class TransactionBodyBuilder {
@ -99,9 +101,16 @@ export class TransactionBodyBuilder {
return this
}
public fromUserAccountDraft(userAccountDraft: UserAccountDraft, account: Account): this {
this.body = new TransactionBody(userAccountDraft)
this.body.registerAddress = new RegisterAddress(userAccountDraft, account)
this.body.data = 'registerAddress'
return this
}
public fromTransactionDraft(transactionDraft: TransactionDraft): TransactionBodyBuilder {
this.body = new TransactionBody(transactionDraft)
// TODO: load pubkeys for sender and recipient user from db
// TODO: load public keys for sender and recipient user from db
switch (transactionDraft.type) {
case InputTransactionType.CREATION:
if (!this.recipientAccount) {

View File

@ -0,0 +1,59 @@
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { CrossGroupType } from './3_3/enum/CrossGroupType'
export const determineCrossGroupType = ({
user,
linkedUser,
type,
}: TransactionDraft): CrossGroupType => {
if (
!linkedUser.communityUuid ||
!user.communityUuid ||
linkedUser.communityUuid === '' ||
user.communityUuid === '' ||
user.communityUuid === linkedUser.communityUuid ||
type === InputTransactionType.CREATION
) {
return CrossGroupType.LOCAL
} else if (type === InputTransactionType.SEND) {
return CrossGroupType.INBOUND
} else if (type === InputTransactionType.RECEIVE) {
return CrossGroupType.OUTBOUND
}
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
'cannot determine CrossGroupType',
)
}
export const determineOtherGroup = (
type: CrossGroupType,
{ user, linkedUser }: TransactionDraft,
): string => {
switch (type) {
case CrossGroupType.LOCAL:
return ''
case CrossGroupType.INBOUND:
if (!linkedUser.communityUuid || linkedUser.communityUuid === '') {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing linkedUser community id for cross group transaction',
)
}
return linkedUser.communityUuid
case CrossGroupType.OUTBOUND:
if (!user.communityUuid || user.communityUuid === '') {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing user community id for cross group transaction',
)
}
return user.communityUuid
case CrossGroupType.CROSS:
throw new TransactionError(TransactionErrorType.NOT_IMPLEMENTED_YET, 'not implemented yet')
}
}

View File

@ -0,0 +1,60 @@
import { Arg, Mutation, Query, Resolver } from 'type-graphql'
import { QueryFailedError } from 'typeorm'
import { TransactionRecipe } from '@model/TransactionRecipe'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { UserRepository } from '@/data/User.repository'
import { RegisterAddressContext } from '@/interactions/backendToDb/account/RegisterAddress.context'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { getDataSource } from '@/typeorm/DataSource'
import { TransactionErrorType } from '../enum/TransactionErrorType'
import { UserAccountDraft } from '../input/UserAccountDraft'
import { UserIdentifier } from '../input/UserIdentifier'
import { TransactionError } from '../model/TransactionError'
import { TransactionResult } from '../model/TransactionResult'
@Resolver()
export class AccountResolver {
@Query(() => Boolean)
async isAccountExist(@Arg('data') userIdentifier: UserIdentifier): Promise<boolean> {
logger.info('isAccountExist', userIdentifier)
return !!(await UserRepository.findAccountByUserIdentifier(userIdentifier))
}
@Mutation(() => TransactionResult)
async registerAddress(
@Arg('data')
userAccountDraft: UserAccountDraft,
): Promise<TransactionResult> {
const registerAddressContext = new RegisterAddressContext(userAccountDraft)
try {
const { transaction, account } = await registerAddressContext.run()
await getDataSource().transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(account)
await transactionalEntityManager.save(transaction)
logger.debug('store register address transaction', new TransactionLoggingView(transaction))
})
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
return new TransactionResult(new TransactionRecipe(transaction))
} catch (err) {
if (err instanceof QueryFailedError) {
logger.error('error saving user or new account or transaction into db: %s', err)
return new TransactionResult(
new TransactionError(
TransactionErrorType.DB_ERROR,
'error saving user or new account or transaction into db',
),
)
} else if (err instanceof TransactionError) {
return new TransactionResult(err)
} else {
logger.error('error in register address: ', err)
throw err
}
}
}
}

View File

@ -4,7 +4,7 @@ import { TransactionDraft } from '@input/TransactionDraft'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { TransactionRepository } from '@/data/Transaction.repository'
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransactionRecipe.context'
import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'

View File

@ -0,0 +1,63 @@
import { Account } from '@entity/Account'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { AccountFactory } from '@/data/Account.factory'
import { CommunityRepository } from '@/data/Community.repository'
import { KeyPair } from '@/data/KeyPair'
import { UserFactory } from '@/data/User.factory'
import { UserLogic } from '@/data/User.logic'
import { UserRepository } from '@/data/User.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransactionRecipe.context'
export interface TransactionWithAccount {
transaction: Transaction
account: Account
}
export class RegisterAddressContext {
// eslint-disable-next-line no-useless-constructor
public constructor(private userAccountDraft: UserAccountDraft) {}
public async run(): Promise<TransactionWithAccount> {
const communityKeyPair = await CommunityRepository.loadHomeCommunityKeyPair()
const user = await this.loadOrCreateUser(communityKeyPair)
if (this.isAccountAlreadyExistOnUser(user)) {
throw new TransactionError(
TransactionErrorType.ALREADY_EXIST,
'account for this user already exist!',
)
}
logger.info('add user and account', this.userAccountDraft)
const account = this.createAccount(new UserLogic(user).calculateKeyPair(communityKeyPair))
account.user = user
const createTransactionContext = new CreateTransactionRecipeContext(this.userAccountDraft, {
account,
})
await createTransactionContext.run()
return { transaction: createTransactionContext.getTransactionRecipe(), account }
}
public isAccountAlreadyExistOnUser(user: User): boolean {
return !!user.accounts?.find(
(value) => value.derivationIndex === this.userAccountDraft.user.accountNr,
)
}
public async loadOrCreateUser(communityKeyPair: KeyPair): Promise<User> {
let user = await UserRepository.findByGradidoId(this.userAccountDraft.user, { accounts: true })
if (!user) {
user = UserFactory.create(this.userAccountDraft, communityKeyPair)
}
return user
}
public createAccount(userKeyPair: KeyPair): Account {
return AccountFactory.createAccountFromUserAccountDraft(this.userAccountDraft, userKeyPair)
}
}

View File

@ -15,7 +15,7 @@ import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { LogError } from '@/server/LogError'
import { getDataSource } from '@/typeorm/DataSource'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransactionRecipe.context'
import { CommunityRole } from './Community.role'
@ -44,7 +44,9 @@ export class HomeCommunityRole extends CommunityRole {
this.self.aufAccount = AccountFactory.createAufAccount(keyPair, this.self.createdAt)
this.self.gmwAccount = AccountFactory.createGmwAccount(keyPair, this.self.createdAt)
const transactionRecipeContext = new CreateTransactionRecipeContext(communityDraft, this.self)
const transactionRecipeContext = new CreateTransactionRecipeContext(communityDraft, {
community: this.self,
})
await transactionRecipeContext.run()
this.transactionRecipe = transactionRecipeContext.getTransactionRecipe()
}

View File

@ -0,0 +1,15 @@
import { Transaction } from '@entity/Transaction'
import { TransactionBuilder } from '@/data/Transaction.builder'
export class AbstractTransactionRecipeRole {
protected transactionBuilder: TransactionBuilder
public constructor() {
this.transactionBuilder = new TransactionBuilder()
}
public getTransaction(): Transaction {
return this.transactionBuilder.getTransaction()
}
}

View File

@ -1,29 +1,22 @@
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { AccountLogic } from '@/data/Account.logic'
import { KeyPair } from '@/data/KeyPair'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { TransactionBuilder } from '@/data/Transaction.builder'
import { UserRepository } from '@/data/User.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
export class TransactionRecipeRole {
protected transactionBuilder: TransactionBuilder
public constructor() {
this.transactionBuilder = new TransactionBuilder()
}
export class BalanceChangingTransactionRecipeRole extends AbstractTransactionRecipeRole {
public async create(
transactionDraft: TransactionDraft,
transactionTypeRole: AbstractTransactionRole,
): Promise<TransactionRecipeRole> {
): Promise<BalanceChangingTransactionRecipeRole> {
const signingUser = transactionTypeRole.getSigningUser()
const recipientUser = transactionTypeRole.getRecipientUser()
@ -82,8 +75,4 @@ export class TransactionRecipeRole {
}
return this.transactionBuilder.getCommunity()
}
public getTransaction(): Transaction {
return this.transactionBuilder.getTransaction()
}
}

View File

@ -4,13 +4,13 @@ import { KeyPair } from '@/data/KeyPair'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionRecipeRole } from './TransactionRecipe.role'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
export class CommunityRootTransactionRole extends TransactionRecipeRole {
public createFromCommunityRoot(
export class CommunityRootTransactionRole extends AbstractTransactionRecipeRole {
public create(
communityDraft: CommunityDraft,
community: Community,
): CommunityRootTransactionRole {
): AbstractTransactionRecipeRole {
// create proto transaction body
const transactionBody = new TransactionBodyBuilder()
.fromCommunityDraft(communityDraft, community)

View File

@ -15,7 +15,7 @@ import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context'
import { CreateTransactionRecipeContext } from './CreateTransactionRecipe.context'
// eslint-disable-next-line import/order
import { communitySeed } from '@test/seeding/Community.seed'

View File

@ -1,3 +1,4 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
@ -5,30 +6,38 @@ import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
import { BalanceChangingTransactionRecipeRole } from './BalanceChangingTransactionRecipeRole'
import { CommunityRootTransactionRole } from './CommunityRootTransaction.role'
import { CreationTransactionRole } from './CreationTransaction.role'
import { ReceiveTransactionRole } from './ReceiveTransaction.role'
import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role'
import { SendTransactionRole } from './SendTransaction.role'
import { TransactionRecipeRole } from './TransactionRecipe.role'
/**
* @DCI-Context
* Context for create and add Transaction Recipe to DB
*/
export interface AdditionalData {
community?: Community
account?: Account
}
export class CreateTransactionRecipeContext {
private transactionRecipeRole: TransactionRecipeRole
private transactionRecipe: AbstractTransactionRecipeRole
// eslint-disable-next-line no-useless-constructor
public constructor(
private draft: CommunityDraft | TransactionDraft,
private community?: Community,
private draft: CommunityDraft | TransactionDraft | UserAccountDraft,
private data?: AdditionalData,
) {}
public getTransactionRecipe(): Transaction {
return this.transactionRecipeRole.getTransaction()
return this.transactionRecipe.getTransaction()
}
/**
@ -36,7 +45,7 @@ export class CreateTransactionRecipeContext {
*/
public async run(): Promise<boolean> {
if (this.draft instanceof TransactionDraft) {
this.transactionRecipeRole = new TransactionRecipeRole()
const transactionRecipeRole = new BalanceChangingTransactionRecipeRole()
// contain logic for translation from backend to dlt-connector format
let transactionTypeRole: AbstractTransactionRole
switch (this.draft.type) {
@ -50,15 +59,24 @@ export class CreateTransactionRecipeContext {
transactionTypeRole = new ReceiveTransactionRole(this.draft)
break
}
await this.transactionRecipeRole.create(this.draft, transactionTypeRole)
await transactionRecipeRole.create(this.draft, transactionTypeRole)
return true
} else if (this.draft instanceof CommunityDraft) {
if (!this.community) {
if (!this.data?.community) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community was not set')
}
this.transactionRecipeRole = new CommunityRootTransactionRole().createFromCommunityRoot(
this.transactionRecipe = new CommunityRootTransactionRole().create(
this.draft,
this.community,
this.data.community,
)
return true
} else if (this.draft instanceof UserAccountDraft) {
if (!this.data?.account) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'account was not set')
}
this.transactionRecipe = await new RegisterAddressTransactionRole().create(
this.draft,
this.data.account,
)
return true
}

View File

@ -0,0 +1,29 @@
import { Account } from '@entity/Account'
import { AccountLogic } from '@/data/Account.logic'
import { CommunityRepository } from '@/data/Community.repository'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
export class RegisterAddressTransactionRole extends AbstractTransactionRecipeRole {
async create(
userAccountDraft: UserAccountDraft,
account: Account,
): Promise<AbstractTransactionRecipeRole> {
const bodyBuilder = new TransactionBodyBuilder()
const communityKeyPair = await CommunityRepository.loadHomeCommunityKeyPair()
const signingKeyPair = new AccountLogic(account).calculateKeyPair(communityKeyPair)
if (!signingKeyPair) {
throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't found signing key pair")
}
this.transactionBuilder
.fromTransactionBodyBuilder(bodyBuilder.fromUserAccountDraft(userAccountDraft, account))
.setSignature(signingKeyPair.sign(this.transactionBuilder.getTransaction().bodyBytes))
.setSigningAccount(account)
return this
}
}

View File

@ -14,7 +14,7 @@ import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { logger } from '@/logging/logger'
import { CreateTransactionRecipeContext } from '../backendToDb/transaction/CreateTransationRecipe.context'
import { CreateTransactionRecipeContext } from '../backendToDb/transaction/CreateTransactionRecipe.context'
import { TransmitToIotaContext } from './TransmitToIota.context'