diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index ae7931cdf..1faef9414 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 77, + lines: 70, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/src/data/Account.factory.ts b/dlt-connector/src/data/Account.factory.ts index 14cc68351..808551b87 100644 --- a/dlt-connector/src/data/Account.factory.ts +++ b/dlt-connector/src/data/Account.factory.ts @@ -4,20 +4,25 @@ import { AddressType } from '@/data/proto/3_3/enum/AddressType' import { hardenDerivationIndex } from '@/utils/derivationHelper' import { Account } from '@entity/Account' import Decimal from 'decimal.js-light' +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' +import { accountTypeToAddressType } from '@/utils/typeConverter' const GMW_ACCOUNT_DERIVATION_INDEX = 1 const AUF_ACCOUNT_DERIVATION_INDEX = 2 export class AccountFactory { public static createAccount( - keyPair: KeyPair, createdAt: Date, derivationIndex: number, type: AddressType, + parentKeyPair?: KeyPair, ): Account { const account = Account.create() account.derivationIndex = derivationIndex - account.derive2Pubkey = KeyManager.getInstance().derive([derivationIndex], keyPair).publicKey + account.derive2Pubkey = KeyManager.getInstance().derive( + [derivationIndex], + parentKeyPair, + ).publicKey account.type = type.valueOf() account.createdAt = createdAt account.balance = new Decimal(0) @@ -25,21 +30,33 @@ export class AccountFactory { return account } + public static createAccountFromUserAccountDraft( + { createdAt, accountType, user }: UserAccountDraft, + parentKeyPair?: KeyPair, + ): Account { + return AccountFactory.createAccount( + new Date(createdAt), + user.accountNr ?? 1, + accountTypeToAddressType(accountType), + parentKeyPair, + ) + } + public static createGmwAccount(keyPair: KeyPair, createdAt: Date): Account { return AccountFactory.createAccount( - keyPair, createdAt, hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX), AddressType.COMMUNITY_GMW, + keyPair, ) } public static createAufAccount(keyPair: KeyPair, createdAt: Date): Account { return AccountFactory.createAccount( - keyPair, createdAt, hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX), AddressType.COMMUNITY_AUF, + keyPair, ) } } diff --git a/dlt-connector/src/data/User.factory.ts b/dlt-connector/src/data/User.factory.ts new file mode 100644 index 000000000..1844cff64 --- /dev/null +++ b/dlt-connector/src/data/User.factory.ts @@ -0,0 +1,16 @@ +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' +import { User } from '@entity/User' +import { UserLogic } from './User.logic' +import { KeyPair } from './KeyPair' + +export class UserFactory { + static create(userAccountDraft: UserAccountDraft, parentKeys?: KeyPair): User { + const user = User.create() + user.createdAt = new Date(userAccountDraft.createdAt) + user.gradidoID = userAccountDraft.user.uuid + const userLogic = new UserLogic(user) + // store generated pubkey into entity + userLogic.calculateKeyPair(parentKeys) + return user + } +} diff --git a/dlt-connector/src/data/User.logic.ts b/dlt-connector/src/data/User.logic.ts new file mode 100644 index 000000000..58b441561 --- /dev/null +++ b/dlt-connector/src/data/User.logic.ts @@ -0,0 +1,41 @@ +import { User } from '@entity/User' +import { KeyPair } from './KeyPair' +import { LogError } from '@/server/LogError' +import { uuid4ToBuffer } from '@/utils/typeConverter' +import { hardenDerivationIndex } from '@/utils/derivationHelper' +import { KeyManager } from '@/manager/KeyManager' + +export class UserLogic { + // eslint-disable-next-line no-useless-constructor + constructor(private user: User) {} + + /** + * + * @param parentKeys if undefined use home community key pair + * @returns + */ + + calculateKeyPair = (parentKeys?: KeyPair): KeyPair => { + if (!this.user.gradidoID) { + throw new LogError('missing GradidoID for user.', { id: this.user.id }) + } + // example gradido id: 03857ac1-9cc2-483e-8a91-e5b10f5b8d16 => + // wholeHex: '03857ac19cc2483e8a91e5b10f5b8d16'] + const wholeHex = uuid4ToBuffer(this.user.gradidoID) + const parts = [] + for (let i = 0; i < 4; i++) { + parts[i] = hardenDerivationIndex(wholeHex.subarray(i * 4, (i + 1) * 4).readUInt32BE()) + } + // parts: [2206563009, 2629978174, 2324817329, 2405141782] + const keyPair = KeyManager.getInstance().derive(parts, parentKeys) + if (this.user.derive1Pubkey && this.user.derive1Pubkey.compare(keyPair.publicKey) !== 0) { + throw new LogError( + 'The freshly derived public key does not correspond to the stored public key', + ) + } + if (!this.user.derive1Pubkey) { + this.user.derive1Pubkey = keyPair.publicKey + } + return keyPair + } +} diff --git a/dlt-connector/src/graphql/enum/AccountType.ts b/dlt-connector/src/graphql/enum/AccountType.ts new file mode 100644 index 000000000..a6946275b --- /dev/null +++ b/dlt-connector/src/graphql/enum/AccountType.ts @@ -0,0 +1,16 @@ +import { registerEnumType } from 'type-graphql' + +export enum AccountType { + NONE = 'none', // if no address was found + COMMUNITY_HUMAN = 'COMMUNITY_HUMAN', // creation account for human + COMMUNITY_GMW = 'COMMUNITY_GMW', // community public budget account + COMMUNITY_AUF = 'COMMUNITY_AUF', // community compensation and environment founds account + COMMUNITY_PROJECT = 'COMMUNITY_PROJECT', // no creations allowed + SUBACCOUNT = 'SUBACCOUNT', // no creations allowed + CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations +} + +registerEnumType(AccountType, { + name: 'AccountType', // this one is mandatory + description: 'Type of account', // this one is optional +}) diff --git a/dlt-connector/src/graphql/input/UserAccountDraft.ts b/dlt-connector/src/graphql/input/UserAccountDraft.ts new file mode 100644 index 000000000..bae9c0694 --- /dev/null +++ b/dlt-connector/src/graphql/input/UserAccountDraft.ts @@ -0,0 +1,23 @@ +// https://www.npmjs.com/package/@apollo/protobufjs + +import { InputType, Field } from 'type-graphql' +import { UserIdentifier } from './UserIdentifier' +import { isValidDateString } from '@validator/DateString' +import { IsEnum, IsObject, ValidateNested } from 'class-validator' +import { AccountType } from '@/graphql/enum/AccountType' + +@InputType() +export class UserAccountDraft { + @Field(() => UserIdentifier) + @IsObject() + @ValidateNested() + user: UserIdentifier + + @Field(() => String) + @isValidDateString() + createdAt: string + + @Field(() => AccountType) + @IsEnum(AccountType) + accountType: AccountType +} diff --git a/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts b/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts index 9b106b3f6..2c402c537 100644 --- a/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts +++ b/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts @@ -1,5 +1,6 @@ import 'reflect-metadata' import { ApolloServer } from '@apollo/server' +// must be imported before createApolloTestServer so that TestDB was created before createApolloTestServer imports repositories import { TestDB } from '@test/TestDB' import { createApolloTestServer } from '@test/ApolloServerMock' import assert from 'assert' diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts index 716d5d235..cc5b27c76 100644 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts @@ -1,9 +1,23 @@ import 'reflect-metadata' import { ApolloServer } from '@apollo/server' +// must be imported before createApolloTestServer so that TestDB was created before createApolloTestServer imports repositories import { TestDB } from '@test/TestDB' import { createApolloTestServer } from '@test/ApolloServerMock' import assert from 'assert' import { TransactionResult } from '@model/TransactionResult' +import { AccountFactory } from '@/data/Account.factory' +import { CONFIG } from '@/config' +import { KeyManager } from '@/manager/KeyManager' +import { UserFactory } from '@/data/User.factory' +import { UserAccountDraft } from '../input/UserAccountDraft' +import { UserLogic } from '@/data/User.logic' +import { AccountType } from '../enum/AccountType' +import { UserIdentifier } from '../input/UserIdentifier' +import { KeyPair } from '@/data/KeyPair' +import { CommunityDraft } from '../input/CommunityDraft' +import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context' + +CONFIG.IOTA_HOME_COMMUNITY_SEED = 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899' let apolloTestServer: ApolloServer @@ -19,10 +33,42 @@ jest.mock('@typeorm/DataSource', () => ({ getDataSource: jest.fn(() => TestDB.instance.dbConnect), })) +const communityUUID = '3d813cbb-37fb-42ba-91df-831e1593ac29' + +const createUserStoreAccount = async (uuid: string): Promise => { + const senderUserAccountDraft = new UserAccountDraft() + senderUserAccountDraft.accountType = AccountType.COMMUNITY_HUMAN + senderUserAccountDraft.createdAt = new Date().toString() + senderUserAccountDraft.user = new UserIdentifier() + senderUserAccountDraft.user.uuid = uuid + senderUserAccountDraft.user.communityUuid = communityUUID + const senderUser = UserFactory.create(senderUserAccountDraft) + const senderUserLogic = new UserLogic(senderUser) + const senderAccount = AccountFactory.createAccountFromUserAccountDraft( + senderUserAccountDraft, + senderUserLogic.calculateKeyPair(), + ) + senderAccount.user = senderUser + // user is set to cascade true will be saved together with account + await senderAccount.save() + return senderUserAccountDraft.user +} + describe('Transaction Resolver Test', () => { + let senderUser: UserIdentifier + let recipientUser: UserIdentifier beforeAll(async () => { - apolloTestServer = await createApolloTestServer() await TestDB.instance.setupTestDB() + apolloTestServer = await createApolloTestServer() + + const communityDraft = new CommunityDraft() + communityDraft.uuid = communityUUID + communityDraft.foreign = false + communityDraft.createdAt = new Date().toString() + const addCommunityContext = new AddCommunityContext(communityDraft) + await addCommunityContext.run() + senderUser = await createUserStoreAccount('0ec72b74-48c2-446f-91ce-31ad7d9f4d65') + recipientUser = await createUserStoreAccount('ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe') }) afterAll(async () => { @@ -32,15 +78,11 @@ describe('Transaction Resolver Test', () => { it('test mocked sendTransaction', async () => { const response = await apolloTestServer.executeOperation({ query: - 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', + 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {succeed, recipe { id, topic }} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, + senderUser, + recipientUser, type: 'SEND', amount: '10', createdAt: '2012-04-17T17:12:00Z', @@ -51,6 +93,7 @@ describe('Transaction Resolver Test', () => { assert(response.body.kind === 'single') expect(response.body.singleResult.errors).toBeUndefined() const transactionResult = response.body.singleResult.data?.sendTransaction as TransactionResult + expect(transactionResult.recipe).toBeDefined() expect(transactionResult.succeed).toBe(true) }) @@ -60,12 +103,8 @@ describe('Transaction Resolver Test', () => { 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, + senderUser, + recipientUser, type: 'INVALID', amount: '10', createdAt: '2012-04-17T17:12:00Z', @@ -78,7 +117,7 @@ describe('Transaction Resolver Test', () => { errors: [ { message: - 'Variable "$input" got invalid value "INVALID" at "input.type"; Value "INVALID" does not exist in "TransactionType" enum.', + 'Variable "$input" got invalid value "INVALID" at "input.type"; Value "INVALID" does not exist in "InputTransactionType" enum.', }, ], }) @@ -90,12 +129,8 @@ describe('Transaction Resolver Test', () => { 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, + senderUser, + recipientUser, type: 'SEND', amount: 'no number', createdAt: '2012-04-17T17:12:00Z', @@ -120,12 +155,8 @@ describe('Transaction Resolver Test', () => { 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, + senderUser, + recipientUser, type: 'SEND', amount: '10', createdAt: 'not valid', @@ -160,12 +191,8 @@ describe('Transaction Resolver Test', () => { 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, + senderUser, + recipientUser, type: 'CREATION', amount: '10', createdAt: '2012-04-17T17:12:00Z', diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index e2b558668..eb1cb769b 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -18,7 +18,7 @@ export class TransactionResolver { try { await createTransactionRecipeContext.run() const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe() - transactionRecipe.save() + await transactionRecipe.save() return new TransactionResult(new TransactionRecipe(transactionRecipe)) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { diff --git a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts index ff46de4d3..5cf48a793 100644 --- a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts +++ b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts @@ -3,6 +3,7 @@ import { ForeignCommunityRole } from './ForeignCommunity.role' import { HomeCommunityRole } from './HomeCommunity.role' import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' import { CommunityRole } from './Community.role' +import { Community } from '@entity/Community' /** * @DCI-Context diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index a4438f780..57155d8ff 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -35,7 +35,6 @@ export class HomeCommunityRole extends CommunityRole { public async store(): Promise { try { - console.log('store transaction: %s', JSON.stringify(this.transactionRecipe, null, 2)) return await getDataSource().transaction(async (transactionalEntityManager) => { const community = await transactionalEntityManager.save(this.self) await transactionalEntityManager.save(this.transactionRecipe) @@ -43,7 +42,6 @@ export class HomeCommunityRole extends CommunityRole { }) } catch (error) { logger.error('error saving home community into db: %s', error) - console.log(error) throw new TransactionError( TransactionErrorType.DB_ERROR, 'error saving home community into db', diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts index 9fb53f3be..3005ccb7a 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -36,7 +36,6 @@ export class TransactionRecipeRole { "couldn't found recipient user account in db", ) } - // create proto transaction body const transactionBodyBuilder = new TransactionBodyBuilder() .setSigningAccount(signingAccount) diff --git a/dlt-connector/src/utils/typeConverter.ts b/dlt-connector/src/utils/typeConverter.ts index 15d4e8826..d21e7964b 100644 --- a/dlt-connector/src/utils/typeConverter.ts +++ b/dlt-connector/src/utils/typeConverter.ts @@ -6,6 +6,9 @@ import { TransactionBody } from '@/data/proto/3_3/TransactionBody' import { logger } from '@/server/logger' import { TransactionError } from '@/graphql/model/TransactionError' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { AccountType } from '@/graphql/enum/AccountType' +import { AddressType } from '@/graphql/enum/AddressType' +import { LogError } from '@/server/LogError' export const uuid4ToBuffer = (uuid: string): Buffer => { // Remove dashes from the UUIDv4 string @@ -60,3 +63,45 @@ export const transactionBodyToBodyBytes = (transactionBody: TransactionBody): Bu ) } } + +export const accountTypeToAddressType = (accountType: AccountType): AddressType => { + switch (accountType) { + case AccountType.NONE: + return AddressType.NONE + case AccountType.COMMUNITY_HUMAN: + return AddressType.COMMUNITY_HUMAN + case AccountType.COMMUNITY_GMW: + return AddressType.COMMUNITY_GMW + case AccountType.COMMUNITY_AUF: + return AddressType.COMMUNITY_AUF + case AccountType.COMMUNITY_PROJECT: + return AddressType.COMMUNITY_PROJECT + case AccountType.SUBACCOUNT: + return AddressType.SUBACCOUNT + case AccountType.CRYPTO_ACCOUNT: + return AddressType.CRYPTO_ACCOUNT + default: + throw new LogError(`Unsupported AccountType: ${accountType}`) + } +} + +export const addressTypeToAccountType = (addressType: AddressType): AccountType => { + switch (addressType) { + case AddressType.NONE: + return AccountType.NONE + case AddressType.COMMUNITY_HUMAN: + return AccountType.COMMUNITY_HUMAN + case AddressType.COMMUNITY_GMW: + return AccountType.COMMUNITY_GMW + case AddressType.COMMUNITY_AUF: + return AccountType.COMMUNITY_AUF + case AddressType.COMMUNITY_PROJECT: + return AccountType.COMMUNITY_PROJECT + case AddressType.SUBACCOUNT: + return AccountType.SUBACCOUNT + case AddressType.CRYPTO_ACCOUNT: + return AccountType.CRYPTO_ACCOUNT + default: + throw new LogError(`Unsupported AddressType: ${addressType}`) + } +}