From 184fe3a2c2aa8664fda758a6344edb6c740b876f Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Thu, 25 Jan 2024 17:19:17 +0100 Subject: [PATCH] write unit test, small improvements --- dlt-connector/src/config/index.ts | 2 +- dlt-connector/src/data/Mnemonic.ts | 23 ++ dlt-connector/src/data/User.logic.ts | 2 +- dlt-connector/src/index.ts | 4 + .../community/AddCommunity.context.test.ts | 64 +++++ .../CreateTransactionRecipe.context.test.ts | 243 ++++++++++++++++++ .../src/logging/AccountLogging.view.ts | 2 +- 7 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index e6febb482..db26d9f37 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -31,7 +31,7 @@ const database = { const iota = { IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org', IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt2', - IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED ?? null, + IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED?.substring(0, 32) ?? null, } const dltConnector = { diff --git a/dlt-connector/src/data/Mnemonic.ts b/dlt-connector/src/data/Mnemonic.ts index 8f15c1046..e23864e60 100644 --- a/dlt-connector/src/data/Mnemonic.ts +++ b/dlt-connector/src/data/Mnemonic.ts @@ -3,10 +3,13 @@ import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39' // eslint-disable-next-line camelcase import { randombytes_buf } from 'sodium-native' +import { LogError } from '@/server/LogError' + export class Mnemonic { private _passphrase = '' public constructor(seed?: Buffer | string) { if (seed) { + Mnemonic.validateSeed(seed) this._passphrase = entropyToMnemonic(seed) return } @@ -22,4 +25,24 @@ export class Mnemonic { public get seed(): Buffer { return mnemonicToSeedSync(this._passphrase) } + + public static validateSeed(seed: Buffer | string): void { + let seedBuffer: Buffer + if (!Buffer.isBuffer(seed)) { + seedBuffer = Buffer.from(seed, 'hex') + } else { + seedBuffer = seed + } + if (seedBuffer.length < 16 || seedBuffer.length > 32 || seedBuffer.length % 4 !== 0) { + throw new LogError( + 'invalid seed, must be in binary between 16 and 32 Bytes, Power of 4, for more infos: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic', + { + seedBufferHex: seedBuffer.toString('hex'), + toShort: seedBuffer.length < 16, + toLong: seedBuffer.length > 32, + powerOf4: seedBuffer.length % 4, + }, + ) + } + } } diff --git a/dlt-connector/src/data/User.logic.ts b/dlt-connector/src/data/User.logic.ts index 0a906682d..8bffe326e 100644 --- a/dlt-connector/src/data/User.logic.ts +++ b/dlt-connector/src/data/User.logic.ts @@ -12,7 +12,7 @@ export class UserLogic { /** * - * @param parentKeys if undefined use home community key pair + * @param parentKeys from home community for own user * @returns */ diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index b998430fd..4e5ef9639 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { CONFIG } from '@/config' +import { Mnemonic } from './data/Mnemonic' import createServer from './server/createServer' import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota' async function main() { + if (CONFIG.IOTA_HOME_COMMUNITY_SEED) { + Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED) + } // eslint-disable-next-line no-console console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) const { app } = await createServer() diff --git a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts new file mode 100644 index 000000000..fec2273b6 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts @@ -0,0 +1,64 @@ +import 'reflect-metadata' +import { Community } from '@entity/Community' +import { TestDB } from '@test/TestDB' + +import { CONFIG } from '@/config' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' + +import { AddCommunityContext } from './AddCommunity.context' + +CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285' + +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) + +describe('interactions/backendToDb/community/AddCommunity Context Test', () => { + beforeAll(async () => { + await TestDB.instance.setupTestDB() + }) + + afterAll(async () => { + await TestDB.instance.teardownTestDB() + }) + + const homeCommunityDraft = new CommunityDraft() + homeCommunityDraft.uuid = 'a2fd0fee-f3ba-4bef-a62a-10a34b0e2754' + homeCommunityDraft.foreign = false + homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z' + // calculated from a2fd0fee-f3ba-4bef-a62a-10a34b0e2754 with iotaTopicFromCommunityUUID + const iotaTopic = '7be2ad83f279a3aaf6d62371cb6be301e2e3c7a3efda9c89984e8f6a7865d9ce' + + const foreignCommunityDraft = new CommunityDraft() + foreignCommunityDraft.uuid = '70df8de5-0fb7-4153-a124-4ff86965be9a' + foreignCommunityDraft.foreign = true + foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z' + + it('with home community, without iota topic', async () => { + const context = new AddCommunityContext(homeCommunityDraft) + await context.run() + const homeCommunity = await Community.findOneOrFail({ where: { iotaTopic } }) + expect(homeCommunity).toMatchObject({ + id: 1, + iotaTopic, + foreign: 0, + rootPubkey: Buffer.from( + '07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0', + 'hex', + ), + createdAt: new Date('2024-01-25T13:09:55.339Z'), + }) + }) + + it('with foreign community', async () => { + const context = new AddCommunityContext(foreignCommunityDraft, 'randomTopic') + await context.run() + const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } }) + expect(foreignCommunity).toMatchObject({ + id: 2, + iotaTopic: 'randomTopic', + foreign: 1, + createdAt: new Date('2024-01-25T13:34:28.020Z'), + }) + }) +}) diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts new file mode 100644 index 000000000..cfd022991 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -0,0 +1,243 @@ +import 'reflect-metadata' +import { Account } from '@entity/Account' +import { Community } from '@entity/Community' +import { TestDB } from '@test/TestDB' +import { Decimal } from 'decimal.js-light' +import { v4 } from 'uuid' + +import { CONFIG } from '@/config' +import { AccountFactory } from '@/data/Account.factory' +import { KeyPair } from '@/data/KeyPair' +import { Mnemonic } from '@/data/Mnemonic' +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { TransactionType } from '@/data/proto/3_3/enum/TransactionType' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { UserFactory } from '@/data/User.factory' +import { UserLogic } from '@/data/User.logic' +import { AccountType } from '@/graphql/enum/AccountType' +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 { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' + +import { AddCommunityContext } from '../community/AddCommunity.context' + +import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context' + +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) + +CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285' +const homeCommunityUuid = v4() +const foreignCommunityUuid = v4() + +function createUserIdentifier(userUuid: string, communityUuid: string): UserIdentifier { + const user = new UserIdentifier() + user.uuid = userUuid + user.communityUuid = communityUuid + return user +} + +const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED)) +const foreignKeyPair = new KeyPair( + new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'), +) +const moderator = createUserIdentifier('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) +const firstUser = createUserIdentifier('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) +const secondUser = createUserIdentifier('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) +const foreignUser = createUserIdentifier( + 'b0155716-5219-4c50-b3d3-0757721ae0d2', + foreignCommunityUuid, +) + +function createUserAndAccount(userIdentifier: UserIdentifier): Account { + const accountDraft = new UserAccountDraft() + accountDraft.user = userIdentifier + accountDraft.createdAt = new Date().toISOString() + accountDraft.accountType = AccountType.COMMUNITY_HUMAN + let _keyPair: KeyPair + if (userIdentifier.communityUuid === homeCommunityUuid) { + _keyPair = keyPair + } else { + _keyPair = foreignKeyPair + } + const user = UserFactory.create(accountDraft, _keyPair) + const userLogic = new UserLogic(user) + const account = AccountFactory.createAccountFromUserAccountDraft( + accountDraft, + userLogic.calculateKeyPair(_keyPair), + ) + account.user = user + return account +} + +describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => { + beforeAll(async () => { + await TestDB.instance.setupTestDB() + const homeCommunityDraft = new CommunityDraft() + homeCommunityDraft.uuid = homeCommunityUuid + homeCommunityDraft.foreign = false + homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z' + let addCommunityContext = new AddCommunityContext(homeCommunityDraft) + await addCommunityContext.run() + + const foreignCommunityDraft = new CommunityDraft() + foreignCommunityDraft.uuid = foreignCommunityUuid + foreignCommunityDraft.foreign = true + foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z' + addCommunityContext = new AddCommunityContext(foreignCommunityDraft) + await addCommunityContext.run() + + const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } }) + // that isn't entirely correct, normally only the public key from foreign community is know, and will be come form blockchain + foreignKeyPair.fillInCommunityKeys(foreignCommunity) + foreignCommunity.save() + + const accounts = [ + createUserAndAccount(moderator), + createUserAndAccount(firstUser), + createUserAndAccount(secondUser), + createUserAndAccount(foreignUser), + ] + await Account.save(accounts) + }) + + afterAll(async () => { + await TestDB.instance.teardownTestDB() + }) + + it('creation transaction', async () => { + const creationTransactionDraft = new TransactionDraft() + creationTransactionDraft.amount = new Decimal('2000') + creationTransactionDraft.backendTransactionId = 1 + creationTransactionDraft.createdAt = new Date().toISOString() + creationTransactionDraft.linkedUser = moderator + creationTransactionDraft.user = firstUser + creationTransactionDraft.type = InputTransactionType.CREATION + creationTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(creationTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + + // console.log(new TransactionLoggingView(transaction)) + expect( + transaction.signingAccount?.derive2Pubkey.compare( + Buffer.from('19ea7313abc54f120ee0041e5b3b63e34562b0a19b96fa3e6e23cc9bff827a36', 'hex'), + ), + ).toBe(0) + expect( + transaction.recipientAccount?.derive2Pubkey.compare( + Buffer.from('5875e1a5e101301cc774b7462566ec2d1a0b04a091dab2e32cecd713b3346224', 'hex'), + ), + ).toBe(0) + + expect(transaction).toMatchObject({ + type: TransactionType.GRADIDO_CREATION, + protocolVersion: '3.3', + community: { + rootPubkey: Buffer.from( + '07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0', + 'hex', + ), + foreign: 0, + }, + amount: new Decimal(2000), + backendTransactions: [ + { + typeId: InputTransactionType.CREATION, + }, + ], + }) + + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + // console.log(new TransactionBodyLoggingView(body)) + expect(body.creation).toBeDefined() + if (!body.creation) throw new Error() + const bodyReceiverPubkey = Buffer.from(body.creation.recipient.pubkey) + expect( + bodyReceiverPubkey.compare( + Buffer.from('5875e1a5e101301cc774b7462566ec2d1a0b04a091dab2e32cecd713b3346224', 'hex'), + ), + ).toBe(0) + expect(body).toMatchObject({ + type: CrossGroupType.LOCAL, + creation: { + recipient: { + amount: '2000', + }, + }, + }) + }) + + it('local send transaction', async () => { + const sendTransactionDraft = new TransactionDraft() + sendTransactionDraft.amount = new Decimal('100') + sendTransactionDraft.backendTransactionId = 1 + sendTransactionDraft.createdAt = new Date().toISOString() + sendTransactionDraft.linkedUser = secondUser + sendTransactionDraft.user = firstUser + sendTransactionDraft.type = InputTransactionType.SEND + sendTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(sendTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) + + it('local recv transaction', async () => { + const recvTransactionDraft = new TransactionDraft() + recvTransactionDraft.amount = new Decimal('100') + recvTransactionDraft.backendTransactionId = 1 + recvTransactionDraft.createdAt = new Date().toISOString() + recvTransactionDraft.linkedUser = secondUser + recvTransactionDraft.user = firstUser + recvTransactionDraft.type = InputTransactionType.RECEIVE + recvTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(recvTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) + + it('cross group send transaction', async () => { + const crossGroupSendTransactionDraft = new TransactionDraft() + crossGroupSendTransactionDraft.amount = new Decimal('100') + crossGroupSendTransactionDraft.backendTransactionId = 1 + crossGroupSendTransactionDraft.createdAt = new Date().toISOString() + crossGroupSendTransactionDraft.linkedUser = foreignUser + crossGroupSendTransactionDraft.user = firstUser + crossGroupSendTransactionDraft.type = InputTransactionType.SEND + crossGroupSendTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) + + it('cross group recv transaction', async () => { + const crossGroupRecvTransactionDraft = new TransactionDraft() + crossGroupRecvTransactionDraft.amount = new Decimal('100') + crossGroupRecvTransactionDraft.backendTransactionId = 1 + crossGroupRecvTransactionDraft.createdAt = new Date().toISOString() + crossGroupRecvTransactionDraft.linkedUser = foreignUser + crossGroupRecvTransactionDraft.user = firstUser + crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE + crossGroupRecvTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) +}) diff --git a/dlt-connector/src/logging/AccountLogging.view.ts b/dlt-connector/src/logging/AccountLogging.view.ts index 76ff7b891..0c97ce469 100644 --- a/dlt-connector/src/logging/AccountLogging.view.ts +++ b/dlt-connector/src/logging/AccountLogging.view.ts @@ -16,7 +16,7 @@ export class AccountLoggingView extends AbstractLoggingView { id: this.account.id, user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null, derivationIndex: this.account.derivationIndex, - derive2pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat), + derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat), type: getEnumValue(AddressType, this.account.type), createdAt: this.dateToString(this.account.createdAt), confirmedAt: this.dateToString(this.account.confirmedAt),