From 4c389635f14c09f0e77e971094de0f722865e4f3 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 23 Jan 2024 12:15:26 +0100 Subject: [PATCH 01/13] import code from dlt main branch, rename --- dlt-connector/src/client/IotaClient.ts | 21 +++++-- dlt-connector/src/data/const.ts | 1 + .../graphql/resolver/TransactionsResolver.ts | 6 +- dlt-connector/src/index.ts | 8 +++ .../community/HomeCommunity.role.ts | 6 +- .../transmitToIota/TransmitToIota.context.ts | 41 ++++++++++++ .../src/manager/InterruptiveSleepManager.ts | 63 +++++++++++++++++++ dlt-connector/src/tasks/transmitToIota.ts | 50 +++++++++++++++ dlt-connector/src/utils/InterruptiveSleep.ts | 31 +++++++++ 9 files changed, 219 insertions(+), 8 deletions(-) create mode 100644 dlt-connector/src/data/const.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts create mode 100644 dlt-connector/src/manager/InterruptiveSleepManager.ts create mode 100644 dlt-connector/src/tasks/transmitToIota.ts create mode 100644 dlt-connector/src/utils/InterruptiveSleep.ts diff --git a/dlt-connector/src/client/IotaClient.ts b/dlt-connector/src/client/IotaClient.ts index f6e6b1772..0e5f24739 100644 --- a/dlt-connector/src/client/IotaClient.ts +++ b/dlt-connector/src/client/IotaClient.ts @@ -2,17 +2,19 @@ import { ClientBuilder } from '@iota/client' import { MessageWrapper } from '@iota/client/lib/types' import { CONFIG } from '@/config' - const client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build() /** * send data message onto iota tangle - * use CONFIG.IOTA_COMMUNITY_ALIAS for index * @param {string | Uint8Array} message - the message as utf based string, will be converted to hex automatically from @iota/client + * @param {string | Uint8Array} topic - the iota topic to which the message will be sended * @return {Promise} the iota message typed */ -function sendMessage(message: string | Uint8Array): Promise { - return client.message().index(CONFIG.IOTA_COMMUNITY_ALIAS).data(message).submit() +function sendMessage( + message: string | Uint8Array, + topic: string | Uint8Array, +): Promise { + return client.message().index(topic).data(message).submit() } /** @@ -24,7 +26,16 @@ function receiveMessage(messageId: string): Promise { return client.getMessage().data(messageId) } -export { sendMessage, receiveMessage } +function receiveAllMessagesForTopic(topic: string | Uint8Array): Promise { + return client.getMessage().index(topic) +} + +async function getIotaMilestone(messageId: string): Promise { + const metadata = await client.getMessage().metadata(messageId) + return metadata.referencedByMilestoneIndex +} + +export { sendMessage, receiveMessage, receiveAllMessagesForTopic, getIotaMilestone } /** * example for message: diff --git a/dlt-connector/src/data/const.ts b/dlt-connector/src/data/const.ts new file mode 100644 index 000000000..82470e8d4 --- /dev/null +++ b/dlt-connector/src/data/const.ts @@ -0,0 +1 @@ +export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota' diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 6a5017fb1..5cd122644 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,12 +1,13 @@ +import { TransactionDraft } from '@input/TransactionDraft' import { Resolver, Arg, Mutation } from 'type-graphql' -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 { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view' import { logger } from '@/logging/logger' import { TransactionLoggingView } from '@/logging/TransactionLogging.view' +import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' import { LogError } from '@/server/LogError' import { TransactionError } from '../model/TransactionError' @@ -48,6 +49,7 @@ export class TransactionResolver { // we can store the transaction and with that automatic the backend transaction await transactionRecipe.save() } + InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) return new TransactionResult(new TransactionRecipe(transactionRecipe)) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index c72978b35..b998430fd 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -2,16 +2,24 @@ import { CONFIG } from '@/config' import createServer from './server/createServer' +import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota' async function main() { // eslint-disable-next-line no-console console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) const { app } = await createServer() + // loop run all the time, check for new transaction for sending to iota + void transmitToIota() app.listen(CONFIG.DLT_CONNECTOR_PORT, () => { // eslint-disable-next-line no-console console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`) }) + + process.on('exit', () => { + // Add shutdown logic here. + stopTransmitToIota() + }) } main().catch((e) => { diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 7a4798368..647c5d397 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -3,6 +3,7 @@ import { Transaction } from '@entity/Transaction' import { CONFIG } from '@/config' import { AccountFactory } from '@/data/Account.factory' +import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' import { KeyPair } from '@/data/KeyPair' import { Mnemonic } from '@/data/Mnemonic' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' @@ -10,6 +11,7 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionError } from '@/graphql/model/TransactionError' import { CommunityLoggingView } from '@/logging/CommunityLogging.view' import { logger } from '@/logging/logger' +import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' import { getDataSource } from '@/typeorm/DataSource' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' @@ -36,12 +38,14 @@ export class HomeCommunityRole extends CommunityRole { public async store(): Promise { try { - return await getDataSource().transaction(async (transactionalEntityManager) => { + const community = await getDataSource().transaction(async (transactionalEntityManager) => { const community = await transactionalEntityManager.save(this.self) await transactionalEntityManager.save(this.transactionRecipe) logger.debug('store home community', new CommunityLoggingView(community)) return community }) + InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) + return community } catch (error) { logger.error('error saving home community into db: %s', error) throw new TransactionError( diff --git a/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts new file mode 100644 index 000000000..fb38428a2 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts @@ -0,0 +1,41 @@ +import { Transaction } from '@entity/Transaction' + +import { logger } from '@/logging/logger' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' + +/** + * @DCI-Context + * Context for sending transaction recipe to iota + * send every transaction only once to iota! + */ +export class TransmitToIotaContext { + // eslint-disable-next-line no-useless-constructor + public constructor(private transaction: Transaction) { + + } + + public async run(): Promise { + logger.info('transmit to iota', new TransactionLoggingView(this.transaction)) + const recipeController = new TransactionRecipe(recipe) + const { transaction, body } = recipeController.getGradidoTransaction() + const messageBuffer = GradidoTransaction.encode(transaction).finish() + + if (body.type === CrossGroupType.LOCAL) { + const resultMessage = await iotaSendMessage( + messageBuffer, + Buffer.from(recipe.community.iotaTopic, 'hex'), + ) + recipe.iotaMessageId = Buffer.from(resultMessage.messageId, 'hex') + logger.info('transmitted Gradido Transaction to Iota', { + id: recipe.id, + messageId: resultMessage.messageId, + }) + await getDataSource().manager.save(recipe) + } else { + throw new TransactionError( + TransactionErrorType.NOT_IMPLEMENTED_YET, + 'other as crossGroupType Local not implemented yet', + ) + } + } +} diff --git a/dlt-connector/src/manager/InterruptiveSleepManager.ts b/dlt-connector/src/manager/InterruptiveSleepManager.ts new file mode 100644 index 000000000..3fc5a9b88 --- /dev/null +++ b/dlt-connector/src/manager/InterruptiveSleepManager.ts @@ -0,0 +1,63 @@ +import { LogError } from '@/server/LogError' + +import { InterruptiveSleep } from '../utils/InterruptiveSleep' + +// Source: https://refactoring.guru/design-patterns/singleton/typescript/example +// and ../federation/client/FederationClientFactory.ts +/** + * A Singleton class defines the `getInstance` method that lets clients access + * the unique singleton instance. + */ +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class InterruptiveSleepManager { + // eslint-disable-next-line no-use-before-define + private static instance: InterruptiveSleepManager + private interruptiveSleep: Map = new Map() + private stepSizeMilliseconds = 10 + + /** + * The Singleton's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the Singleton class while keeping + * just one instance of each subclass around. + */ + public static getInstance(): InterruptiveSleepManager { + if (!InterruptiveSleepManager.instance) { + InterruptiveSleepManager.instance = new InterruptiveSleepManager() + } + return InterruptiveSleepManager.instance + } + + /** + * only for new created InterruptiveSleepManager Entries! + * @param step size in ms in which new! InterruptiveSleepManager check if they where triggered + */ + public setStepSize(ms: number) { + this.stepSizeMilliseconds = ms + } + + public interrupt(key: string): void { + const interruptiveSleep = this.interruptiveSleep.get(key) + if (interruptiveSleep) { + interruptiveSleep.interrupt() + } + } + + public sleep(key: string, ms: number): Promise { + if (!this.interruptiveSleep.has(key)) { + this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds)) + } + const cond = this.interruptiveSleep.get(key) + if (!cond) { + throw new LogError('map entry not exist after setting it') + } + return cond.sleep(ms) + } +} diff --git a/dlt-connector/src/tasks/transmitToIota.ts b/dlt-connector/src/tasks/transmitToIota.ts new file mode 100644 index 000000000..5a0545bc8 --- /dev/null +++ b/dlt-connector/src/tasks/transmitToIota.ts @@ -0,0 +1,50 @@ +import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' +import { TransactionRepository } from '@/data/Transaction.repository' +import { TransmitToIotaContext } from '@/interactions/backendToDb/transmitToIota/TransmitToIota.context' +import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' + +import { logger } from '../logging/logger' + +function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +let running = true + +export const stopTransmitToIota = (): void => { + running = false +} +/** + * check for pending transactions: + * - if one found call TransmitToIotaContext + * - if not, wait 1000 ms and try again + * if a new transaction was added, the sleep will be interrupted + */ +export const transmitToIota = async (): Promise => { + logger.info('start iota message transmitter') + // eslint-disable-next-line no-unmodified-loop-condition + while (running) { + try { + while (true) { + const recipe = await TransactionRepository.getNextPendingTransaction() + if (!recipe) break + const transmitToIotaContext = new TransmitToIotaContext(recipe) + await transmitToIotaContext.run() + } + + await InterruptiveSleepManager.getInstance().sleep( + TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, + // 1000, + 1000, + ) + } catch (error) { + logger.error('error while transmitting to iota, retry in 10 seconds ', error) + await sleep(10000) + } + } + logger.info( + 'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!', + ) +} diff --git a/dlt-connector/src/utils/InterruptiveSleep.ts b/dlt-connector/src/utils/InterruptiveSleep.ts new file mode 100644 index 000000000..c21e57db9 --- /dev/null +++ b/dlt-connector/src/utils/InterruptiveSleep.ts @@ -0,0 +1,31 @@ +/** + * Sleep, that can be interrupted + * call sleep only for msSteps and than check if interrupt was called + */ +export class InterruptiveSleep { + private interruptSleep = false + private msSteps = 10 + + constructor(msSteps: number) { + this.msSteps = msSteps + } + + public interrupt(): void { + this.interruptSleep = true + } + + private static _sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) + } + + public async sleep(ms: number): Promise { + let waited = 0 + this.interruptSleep = false + while (waited < ms && !this.interruptSleep) { + await InterruptiveSleep._sleep(this.msSteps) + waited += this.msSteps + } + } +} From 35be61a9c865e32cb9356f73058aeac9288574e1 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 23 Jan 2024 18:40:34 +0100 Subject: [PATCH 02/13] create context and roles for transmit to iota --- dlt-connector/src/client/IotaClient.test.ts | 2 +- dlt-connector/src/data/KeyPair.ts | 5 +- dlt-connector/src/data/Transaction.logic.ts | 161 ++++++++++++++++++ .../src/data/proto/3_3/GradidoTransaction.ts | 13 +- .../src/data/proto/3_3/TransactionBody.ts | 15 ++ .../graphql/resolver/TransactionsResolver.ts | 3 +- .../transmitToIota/TransmitToIota.context.ts | 41 ----- .../AbstractTransactionRecipe.role.ts | 90 ++++++++++ .../InboundTransactionRecipe.role.ts | 40 +++++ .../LocalTransactionRecipe.role.ts | 25 +++ .../OutboundTransactionRecipeRole.ts | 6 + .../transmitToIota/TransmitToIota.context.ts | 42 +++++ dlt-connector/src/tasks/transmitToIota.ts | 5 +- .../Transaction.ts | 2 +- 14 files changed, 389 insertions(+), 61 deletions(-) create mode 100644 dlt-connector/src/data/Transaction.logic.ts delete mode 100644 dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts diff --git a/dlt-connector/src/client/IotaClient.test.ts b/dlt-connector/src/client/IotaClient.test.ts index 2ceaa085e..5bee71b2e 100644 --- a/dlt-connector/src/client/IotaClient.test.ts +++ b/dlt-connector/src/client/IotaClient.test.ts @@ -50,7 +50,7 @@ jest.mock('@iota/client', () => { describe('Iota Tests', () => { it('test mocked sendDataMessage', async () => { - const result = await sendMessage('Test Message') + const result = await sendMessage('Test Message', 'topic') expect(result).toBe('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710') }) diff --git a/dlt-connector/src/data/KeyPair.ts b/dlt-connector/src/data/KeyPair.ts index 59e9a5066..dc2dd1bab 100644 --- a/dlt-connector/src/data/KeyPair.ts +++ b/dlt-connector/src/data/KeyPair.ts @@ -6,6 +6,7 @@ import { LogError } from '@/server/LogError' import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519' import { Mnemonic } from './Mnemonic' +import { SignaturePair } from './proto/3_3/SignaturePair' /** * Class Managing Key Pair and also generate, sign and verify signature with it @@ -81,7 +82,7 @@ export class KeyPair { return sign(message, this.getExtendPrivateKey()) } - public verify(message: Buffer, signature: Buffer): boolean { - return verify(message, signature, this.getExtendPublicKey()) + public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean { + return verify(message, signature, pubKey) } } diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts new file mode 100644 index 000000000..62f7c3732 --- /dev/null +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -0,0 +1,161 @@ +import { Transaction } from '@entity/Transaction' +import { Not } from 'typeorm' + +import { logger } from '@/logging/logger' +import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' +import { LogError } from '@/server/LogError' + +import { CrossGroupType } from './proto/3_3/enum/CrossGroupType' +import { TransactionType } from './proto/3_3/enum/TransactionType' +import { TransactionBody } from './proto/3_3/TransactionBody' + +export class TransactionLogic { + protected transactionBody: TransactionBody | undefined + + // eslint-disable-next-line no-useless-constructor + public constructor(private self: Transaction) {} + + /** + * search for transaction pair for Cross Group Transaction + * @returns + */ + public async findPairTransaction(): Promise { + const type = this.getBody().type + if (type === CrossGroupType.LOCAL) { + throw new LogError("local transaction don't has a pairing transaction") + } + + // check if already on entity + if (this.self.paringTransaction) { + return this.self.paringTransaction + } + + if (this.self.paringTransactionId) { + const pairingTransaction = await Transaction.findOneBy({ id: this.self.paringTransactionId }) + if (pairingTransaction) { + return pairingTransaction + } + } + // check if we find some in db + const sameCreationDateTransactions = await Transaction.findBy({ + createdAt: this.self.createdAt, + id: Not(this.self.id), + }) + if ( + sameCreationDateTransactions.length === 1 && + this.isBelongTogether(sameCreationDateTransactions[0]) + ) { + return sameCreationDateTransactions[0] + } + // this approach only work if all entities get ids really incremented by one + if (type === CrossGroupType.OUTBOUND) { + const prevTransaction = await Transaction.findOneBy({ id: this.self.id - 1 }) + if (prevTransaction && this.isBelongTogether(prevTransaction)) { + return prevTransaction + } + } else if (type === CrossGroupType.INBOUND) { + const nextTransaction = await Transaction.findOneBy({ id: this.self.id + 1 }) + if (nextTransaction && this.isBelongTogether(nextTransaction)) { + return nextTransaction + } + } + throw new LogError("couldn't find valid paring transaction", { + id: this.self.id, + type: CrossGroupType[type], + transactionCountWithSameCreatedAt: sameCreationDateTransactions.length, + }) + } + + /** + * check if two transactions belong together + * are they pairs for a cross group transaction + * @param otherTransaction + */ + public isBelongTogether(otherTransaction: Transaction): boolean { + if (this.self.id === otherTransaction.id) { + logger.info('id is the same, it is the same transaction!') + return false + } + if ( + this.self.signingAccountId !== otherTransaction.signingAccountId || + this.self.recipientAccountId !== otherTransaction.recipientAccountId || + this.self.communityId !== otherTransaction.communityId || + this.self.otherCommunityId !== otherTransaction.otherCommunityId || + this.self.amount !== otherTransaction.amount || + this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation || + this.self.createdAt !== otherTransaction.createdAt + ) { + logger.debug('transaction a and b are not pairs', { + a: new TransactionLoggingView(this.self), + b: new TransactionLoggingView(otherTransaction), + }) + return false + } + const body = this.getBody() + const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes) + /** + * both can be Cross + * one can be OUTBOUND and one can be INBOUND + * no one can be LOCAL + */ + if ( + (body.type === otherBody.type && body.type !== CrossGroupType.CROSS) || + body.type === CrossGroupType.LOCAL || + otherBody.type === CrossGroupType.LOCAL + ) { + logger.info("cross group types don't match", { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + const type = body.getTransactionType() + const otherType = otherBody.getTransactionType() + if (!type || !otherType) { + throw new LogError("couldn't determine transaction type", { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + } + if (type !== otherType) { + logger.info("transaction types don't match", { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + if ( + [ + TransactionType.COMMUNITY_ROOT, + TransactionType.GRADIDO_CREATION, + TransactionType.GRADIDO_DEFERRED_TRANSFER, + ].includes(type) + ) { + logger.info(`TransactionType ${type} couldn't be a CrossGroup Transaction`) + return false + } + if (body.otherGroup === otherBody.otherGroup) { + logger.info('otherGroups are the same', { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + if (body.memo !== otherBody.memo) { + logger.info('memo differ', { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + return true + } + + public getBody(): TransactionBody { + if (!this.transactionBody) { + this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) + } + return this.transactionBody + } +} diff --git a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts index f38bcbd1f..f4274407b 100644 --- a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts +++ b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts @@ -1,8 +1,5 @@ import { Field, Message } from 'protobufjs' -import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' -import { TransactionError } from '@/graphql/model/TransactionError' -import { logger } from '@/logging/logger' import { LogError } from '@/server/LogError' import { SignatureMap } from './SignatureMap' @@ -46,14 +43,6 @@ export class GradidoTransaction extends Message { } getTransactionBody(): TransactionBody { - try { - return TransactionBody.decode(new Uint8Array(this.bodyBytes)) - } catch (error) { - logger.error('error decoding body from gradido transaction: %s', error) - throw new TransactionError( - TransactionErrorType.PROTO_DECODE_ERROR, - 'cannot decode body from gradido transaction', - ) - } + return TransactionBody.fromBodyBytes(this.bodyBytes) } } diff --git a/dlt-connector/src/data/proto/3_3/TransactionBody.ts b/dlt-connector/src/data/proto/3_3/TransactionBody.ts index 0c2733606..39d5602ec 100644 --- a/dlt-connector/src/data/proto/3_3/TransactionBody.ts +++ b/dlt-connector/src/data/proto/3_3/TransactionBody.ts @@ -1,8 +1,11 @@ import { Transaction } from '@entity/Transaction' 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 { TransactionError } from '@/graphql/model/TransactionError' +import { logger } from '@/logging/logger' import { LogError } from '@/server/LogError' import { timestampToDate } from '@/utils/typeConverter' @@ -36,6 +39,18 @@ export class TransactionBody extends Message { } } + public static fromBodyBytes(bodyBytes: Buffer) { + try { + return TransactionBody.decode(new Uint8Array(bodyBytes)) + } catch (error) { + logger.error('error decoding body from gradido transaction: %s', error) + throw new TransactionError( + TransactionErrorType.PROTO_DECODE_ERROR, + 'cannot decode body from gradido transaction', + ) + } + } + @Field.d(1, 'string') public memo: string diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 5cd122644..7cc619400 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,6 +1,7 @@ -import { TransactionDraft } from '@input/TransactionDraft' import { Resolver, Arg, Mutation } from 'type-graphql' +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' diff --git a/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts deleted file mode 100644 index fb38428a2..000000000 --- a/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Transaction } from '@entity/Transaction' - -import { logger } from '@/logging/logger' -import { TransactionLoggingView } from '@/logging/TransactionLogging.view' - -/** - * @DCI-Context - * Context for sending transaction recipe to iota - * send every transaction only once to iota! - */ -export class TransmitToIotaContext { - // eslint-disable-next-line no-useless-constructor - public constructor(private transaction: Transaction) { - - } - - public async run(): Promise { - logger.info('transmit to iota', new TransactionLoggingView(this.transaction)) - const recipeController = new TransactionRecipe(recipe) - const { transaction, body } = recipeController.getGradidoTransaction() - const messageBuffer = GradidoTransaction.encode(transaction).finish() - - if (body.type === CrossGroupType.LOCAL) { - const resultMessage = await iotaSendMessage( - messageBuffer, - Buffer.from(recipe.community.iotaTopic, 'hex'), - ) - recipe.iotaMessageId = Buffer.from(resultMessage.messageId, 'hex') - logger.info('transmitted Gradido Transaction to Iota', { - id: recipe.id, - messageId: resultMessage.messageId, - }) - await getDataSource().manager.save(recipe) - } else { - throw new TransactionError( - TransactionErrorType.NOT_IMPLEMENTED_YET, - 'other as crossGroupType Local not implemented yet', - ) - } - } -} diff --git a/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts new file mode 100644 index 000000000..21285705e --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts @@ -0,0 +1,90 @@ +import { Transaction } from '@entity/Transaction' + +import { sendMessage as iotaSendMessage } from '@/client/IotaClient' +import { KeyPair } from '@/data/KeyPair' +import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction' +import { SignaturePair } from '@/data/proto/3_3/SignaturePair' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { TransactionError } from '@/graphql/model/TransactionError' +import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLogging.view' +import { logger } from '@/logging/logger' + +export abstract class AbstractTransactionRecipeRole { + protected transactionBody: TransactionBody | undefined + // eslint-disable-next-line no-useless-constructor + public constructor(protected self: Transaction) {} + + public abstract transmitToIota(): Promise + + protected getGradidoTransaction(): GradidoTransaction { + this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) + const transaction = new GradidoTransaction(this.transactionBody) + if (!this.self.signature) { + throw new TransactionError( + TransactionErrorType.MISSING_PARAMETER, + 'missing signature in transaction recipe', + ) + } + const signaturePair = new SignaturePair() + if (this.self.signature.length !== 64) { + throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, "signature isn't 64 bytes") + } + signaturePair.signature = this.self.signature + if (this.transactionBody.communityRoot) { + const publicKey = this.self.community.rootPubkey + if (!publicKey) { + throw new TransactionError( + TransactionErrorType.MISSING_PARAMETER, + 'missing community public key for community root transaction', + ) + } + signaturePair.pubKey = publicKey + } else if (this.self.signingAccount) { + const publicKey = this.self.signingAccount.derive2Pubkey + if (!publicKey) { + throw new TransactionError( + TransactionErrorType.MISSING_PARAMETER, + 'missing signing account public key for transaction', + ) + } + signaturePair.pubKey = publicKey + } else { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "signingAccount not exist and it isn't a community root transaction", + ) + } + if (signaturePair.validate()) { + transaction.sigMap.sigPair.push(signaturePair) + } + if (!KeyPair.verify(transaction.bodyBytes, signaturePair)) { + logger.debug('invalid signature', new GradidoTransactionLoggingView(transaction)) + throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, 'signature is invalid') + } + return transaction + } + + /** + * + * @param gradidoTransaction + * @param topic + * @return iota message id + */ + protected async sendViaIota( + gradidoTransaction: GradidoTransaction, + topic: string, + ): Promise { + // protobuf serializing function + const messageBuffer = GradidoTransaction.encode(gradidoTransaction).finish() + const resultMessage = await iotaSendMessage( + messageBuffer, + Uint8Array.from(Buffer.from(topic, 'hex')), + ) + logger.info('transmitted Gradido Transaction to Iota', { + id: this.self.id, + messageId: resultMessage.messageId, + }) + return Buffer.from(resultMessage.messageId, 'hex') + } +} diff --git a/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts new file mode 100644 index 000000000..52e95c12c --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts @@ -0,0 +1,40 @@ +import { Transaction } from '@entity/Transaction' + +import { TransactionLogic } from '@/data/Transaction.logic' +import { logger } from '@/logging/logger' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' +import { LogError } from '@/server/LogError' + +import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role' + +/** + * Inbound Transaction on recipient community, mark the gradidos as received from another community + * need to set gradido id from OUTBOUND transaction! + */ +export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole { + public async transmitToIota(): Promise { + logger.debug('transmit INBOUND transaction to iota', new TransactionLoggingView(this.self)) + const gradidoTransaction = this.getGradidoTransaction() + const pairingTransaction = await new TransactionLogic(this.self).findPairTransaction() + if (!pairingTransaction.iotaMessageId || pairingTransaction.iotaMessageId.length !== 32) { + throw new LogError( + 'missing iota message id in pairing transaction, was it already send?', + new TransactionLoggingView(pairingTransaction), + ) + } + gradidoTransaction.parentMessageId = pairingTransaction.iotaMessageId + this.self.paringTransactionId = pairingTransaction.id + this.self.paringTransaction = pairingTransaction + pairingTransaction.paringTransactionId = this.self.id + + if (!this.self.otherCommunity) { + throw new LogError('missing other community') + } + + this.self.iotaMessageId = await this.sendViaIota( + gradidoTransaction, + this.self.otherCommunity.iotaTopic, + ) + return this.self + } +} diff --git a/dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts new file mode 100644 index 000000000..60cc58ff7 --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts @@ -0,0 +1,25 @@ +import { Transaction } from '@entity/Transaction' + +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { logger } from '@/logging/logger' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' + +import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role' + +export class LocalTransactionRecipeRole extends AbstractTransactionRecipeRole { + public async transmitToIota(): Promise { + let transactionCrossGroupTypeName = 'LOCAL' + if (this.transactionBody) { + transactionCrossGroupTypeName = CrossGroupType[this.transactionBody.type] + } + logger.debug( + `transmit ${transactionCrossGroupTypeName} transaction to iota`, + new TransactionLoggingView(this.self), + ) + this.self.iotaMessageId = await this.sendViaIota( + this.getGradidoTransaction(), + this.self.community.iotaTopic, + ) + return this.self + } +} diff --git a/dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts b/dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts new file mode 100644 index 000000000..a54cd8ec3 --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts @@ -0,0 +1,6 @@ +import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role' + +/** + * Outbound Transaction on sender community, mark the gradidos as sended out of community + */ +export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {} diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts new file mode 100644 index 000000000..8e0b32c6c --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts @@ -0,0 +1,42 @@ +import { Transaction } from '@entity/Transaction' + +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { LogError } from '@/server/LogError' + +import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role' +import { InboundTransactionRecipeRole } from './InboundTransactionRecipe.role' +import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role' +import { OutboundTransactionRecipeRole } from './OutboundTransactionRecipeRole' + +/** + * @DCI-Context + * Context for sending transaction recipe to iota + * send every transaction only once to iota! + */ +export class TransmitToIotaContext { + private transactionRecipeRole: AbstractTransactionRecipeRole + + public constructor(transaction: Transaction) { + const transactionBody = TransactionBody.fromBodyBytes(transaction.bodyBytes) + switch (transactionBody.type) { + case CrossGroupType.LOCAL: + this.transactionRecipeRole = new LocalTransactionRecipeRole(transaction) + break + case CrossGroupType.INBOUND: + this.transactionRecipeRole = new InboundTransactionRecipeRole(transaction) + break + case CrossGroupType.OUTBOUND: + this.transactionRecipeRole = new OutboundTransactionRecipeRole(transaction) + break + default: + throw new LogError('unknown cross group type', transactionBody.type) + } + } + + public async run(): Promise { + const transaction = await this.transactionRecipeRole.transmitToIota() + // store changes in db + await transaction.save() + } +} diff --git a/dlt-connector/src/tasks/transmitToIota.ts b/dlt-connector/src/tasks/transmitToIota.ts index 5a0545bc8..89236586e 100644 --- a/dlt-connector/src/tasks/transmitToIota.ts +++ b/dlt-connector/src/tasks/transmitToIota.ts @@ -1,6 +1,6 @@ import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' import { TransactionRepository } from '@/data/Transaction.repository' -import { TransmitToIotaContext } from '@/interactions/backendToDb/transmitToIota/TransmitToIota.context' +import { TransmitToIotaContext } from '@/interactions/transmitToIota/TransmitToIota.context' import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' import { logger } from '../logging/logger' @@ -36,7 +36,6 @@ export const transmitToIota = async (): Promise => { await InterruptiveSleepManager.getInstance().sleep( TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, - // 1000, 1000, ) } catch (error) { @@ -44,7 +43,7 @@ export const transmitToIota = async (): Promise => { await sleep(10000) } } - logger.info( + logger.error( 'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!', ) } diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts index 922bf81cd..4947c2a2d 100644 --- a/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts @@ -23,7 +23,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true }) iotaMessageId?: Buffer - @OneToOne(() => Transaction) + @OneToOne(() => Transaction, { cascade: ['update'] }) // eslint-disable-next-line no-use-before-define paringTransaction?: Transaction From 7de4a3d2840d707487f3d8989e2e36c2bc6c0dd8 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 23 Jan 2024 18:43:25 +0100 Subject: [PATCH 03/13] remove not used code --- dlt-connector/src/client/IotaClient.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/dlt-connector/src/client/IotaClient.ts b/dlt-connector/src/client/IotaClient.ts index 0e5f24739..3f2d318fa 100644 --- a/dlt-connector/src/client/IotaClient.ts +++ b/dlt-connector/src/client/IotaClient.ts @@ -26,16 +26,7 @@ function receiveMessage(messageId: string): Promise { return client.getMessage().data(messageId) } -function receiveAllMessagesForTopic(topic: string | Uint8Array): Promise { - return client.getMessage().index(topic) -} - -async function getIotaMilestone(messageId: string): Promise { - const metadata = await client.getMessage().metadata(messageId) - return metadata.referencedByMilestoneIndex -} - -export { sendMessage, receiveMessage, receiveAllMessagesForTopic, getIotaMilestone } +export { sendMessage, receiveMessage } /** * example for message: From e7b3cda2150b873a9a25c6f51879a45ed6bc2c7f Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Thu, 25 Jan 2024 13:20:02 +0100 Subject: [PATCH 04/13] change variable name --- dlt-connector/src/manager/InterruptiveSleepManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dlt-connector/src/manager/InterruptiveSleepManager.ts b/dlt-connector/src/manager/InterruptiveSleepManager.ts index 3fc5a9b88..7827c8fe9 100644 --- a/dlt-connector/src/manager/InterruptiveSleepManager.ts +++ b/dlt-connector/src/manager/InterruptiveSleepManager.ts @@ -54,10 +54,10 @@ export class InterruptiveSleepManager { if (!this.interruptiveSleep.has(key)) { this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds)) } - const cond = this.interruptiveSleep.get(key) - if (!cond) { + const interruptiveSleep = this.interruptiveSleep.get(key) + if (!interruptiveSleep) { throw new LogError('map entry not exist after setting it') } - return cond.sleep(ms) + return interruptiveSleep.sleep(ms) } } From 184fe3a2c2aa8664fda758a6344edb6c740b876f Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Thu, 25 Jan 2024 17:19:17 +0100 Subject: [PATCH 05/13] 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), From d959fdfbd823d677c06e899f919d7d9eec526671 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 12:23:08 +0100 Subject: [PATCH 06/13] finalize test --- .../CreateTransactionRecipe.context.test.ts | 281 ++++++++++++++---- .../src/logging/TransferAmountLogging.view.ts | 2 +- 2 files changed, 226 insertions(+), 57 deletions(-) diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index cfd022991..03474ff28 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -1,6 +1,7 @@ import 'reflect-metadata' import { Account } from '@entity/Account' import { Community } from '@entity/Community' +import { User } from '@entity/User' import { TestDB } from '@test/TestDB' import { Decimal } from 'decimal.js-light' import { v4 } from 'uuid' @@ -26,6 +27,7 @@ import { TransactionLoggingView } from '@/logging/TransactionLogging.view' import { AddCommunityContext } from '../community/AddCommunity.context' import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' jest.mock('@typeorm/DataSource', () => ({ getDataSource: jest.fn(() => TestDB.instance.dbConnect), @@ -35,6 +37,12 @@ CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa const homeCommunityUuid = v4() const foreignCommunityUuid = v4() +type UserSet = { + identifier: UserIdentifier + user: User + account: Account +} + function createUserIdentifier(userUuid: string, communityUuid: string): UserIdentifier { const user = new UserIdentifier() user.uuid = userUuid @@ -46,13 +54,6 @@ 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() @@ -75,6 +76,27 @@ function createUserAndAccount(userIdentifier: UserIdentifier): Account { return account } +function createUserSet(userUuid: string, communityUuid: string): UserSet { + const identifier = createUserIdentifier(userUuid, communityUuid) + const account = createUserAndAccount(identifier) + if (!account.user) { + throw Error('user missing') + } + return { + identifier, + account, + user: account.user, + } +} + +let moderator: UserSet +let firstUser: UserSet +let secondUser: UserSet +let foreignUser: UserSet + +const topic = iotaTopicFromCommunityUUID(homeCommunityUuid) +const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid) + describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => { beforeAll(async () => { await TestDB.instance.setupTestDB() @@ -97,13 +119,17 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context foreignKeyPair.fillInCommunityKeys(foreignCommunity) foreignCommunity.save() - const accounts = [ - createUserAndAccount(moderator), - createUserAndAccount(firstUser), - createUserAndAccount(secondUser), - createUserAndAccount(foreignUser), - ] - await Account.save(accounts) + moderator = createUserSet('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) + firstUser = createUserSet('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) + secondUser = createUserSet('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) + foreignUser = createUserSet('b0155716-5219-4c50-b3d3-0757721ae0d2', foreignCommunityUuid) + + await Account.save([ + moderator.account, + firstUser.account, + secondUser.account, + foreignUser.account, + ]) }) afterAll(async () => { @@ -115,8 +141,8 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context creationTransactionDraft.amount = new Decimal('2000') creationTransactionDraft.backendTransactionId = 1 creationTransactionDraft.createdAt = new Date().toISOString() - creationTransactionDraft.linkedUser = moderator - creationTransactionDraft.user = firstUser + creationTransactionDraft.linkedUser = moderator.identifier + creationTransactionDraft.user = firstUser.identifier creationTransactionDraft.type = InputTransactionType.CREATION creationTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(creationTransactionDraft) @@ -124,26 +150,19 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context 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', - ), + rootPubkey: keyPair.publicKey, foreign: 0, + iotaTopic: topic, + }, + signingAccount: { + derive2Pubkey: moderator.account.derive2Pubkey, + }, + recipientAccount: { + derive2Pubkey: firstUser.account.derive2Pubkey, }, amount: new Decimal(2000), backendTransactions: [ @@ -158,11 +177,8 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context 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(bodyReceiverPubkey.compare(firstUser.account.derive2Pubkey)).toBe(0) + expect(body).toMatchObject({ type: CrossGroupType.LOCAL, creation: { @@ -176,68 +192,221 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context it('local send transaction', async () => { const sendTransactionDraft = new TransactionDraft() sendTransactionDraft.amount = new Decimal('100') - sendTransactionDraft.backendTransactionId = 1 + sendTransactionDraft.backendTransactionId = 2 sendTransactionDraft.createdAt = new Date().toISOString() - sendTransactionDraft.linkedUser = secondUser - sendTransactionDraft.user = firstUser + sendTransactionDraft.linkedUser = secondUser.identifier + sendTransactionDraft.user = firstUser.identifier sendTransactionDraft.type = InputTransactionType.SEND sendTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(sendTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() + + // console.log(new TransactionLoggingView(transaction)) + expect(transaction).toMatchObject({ + type: TransactionType.GRADIDO_TRANSFER, + protocolVersion: '3.3', + community: { + rootPubkey: keyPair.publicKey, + foreign: 0, + iotaTopic: topic, + }, + signingAccount: { + derive2Pubkey: firstUser.account.derive2Pubkey, + }, + recipientAccount: { + derive2Pubkey: secondUser.account.derive2Pubkey, + }, + amount: new Decimal(100), + backendTransactions: [ + { + typeId: InputTransactionType.SEND, + }, + ], + }) + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) - console.log(new TransactionBodyLoggingView(body)) - console.log(new TransactionLoggingView(transaction)) + // console.log(new TransactionBodyLoggingView(body)) + expect(body.transfer).toBeDefined() + if (!body.transfer) throw new Error() + expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0) + expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe( + 0, + ) + expect(body).toMatchObject({ + type: CrossGroupType.LOCAL, + transfer: { + sender: { + amount: '100', + }, + }, + }) }) it('local recv transaction', async () => { const recvTransactionDraft = new TransactionDraft() recvTransactionDraft.amount = new Decimal('100') - recvTransactionDraft.backendTransactionId = 1 + recvTransactionDraft.backendTransactionId = 3 recvTransactionDraft.createdAt = new Date().toISOString() - recvTransactionDraft.linkedUser = secondUser - recvTransactionDraft.user = firstUser + recvTransactionDraft.linkedUser = firstUser.identifier + recvTransactionDraft.user = secondUser.identifier recvTransactionDraft.type = InputTransactionType.RECEIVE recvTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(recvTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() + // console.log(new TransactionLoggingView(transaction)) + expect(transaction).toMatchObject({ + type: TransactionType.GRADIDO_TRANSFER, + protocolVersion: '3.3', + community: { + rootPubkey: keyPair.publicKey, + foreign: 0, + iotaTopic: topic, + }, + signingAccount: { + derive2Pubkey: firstUser.account.derive2Pubkey, + }, + recipientAccount: { + derive2Pubkey: secondUser.account.derive2Pubkey, + }, + amount: new Decimal(100), + backendTransactions: [ + { + typeId: InputTransactionType.RECEIVE, + }, + ], + }) + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) - console.log(new TransactionBodyLoggingView(body)) - console.log(new TransactionLoggingView(transaction)) + // console.log(new TransactionBodyLoggingView(body)) + expect(body.transfer).toBeDefined() + if (!body.transfer) throw new Error() + expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0) + expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe( + 0, + ) + expect(body).toMatchObject({ + type: CrossGroupType.LOCAL, + transfer: { + sender: { + amount: '100', + }, + }, + }) }) it('cross group send transaction', async () => { const crossGroupSendTransactionDraft = new TransactionDraft() crossGroupSendTransactionDraft.amount = new Decimal('100') - crossGroupSendTransactionDraft.backendTransactionId = 1 + crossGroupSendTransactionDraft.backendTransactionId = 4 crossGroupSendTransactionDraft.createdAt = new Date().toISOString() - crossGroupSendTransactionDraft.linkedUser = foreignUser - crossGroupSendTransactionDraft.user = firstUser + crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier + crossGroupSendTransactionDraft.user = firstUser.identifier crossGroupSendTransactionDraft.type = InputTransactionType.SEND crossGroupSendTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() + // console.log(new TransactionLoggingView(transaction)) + expect(transaction).toMatchObject({ + type: TransactionType.GRADIDO_TRANSFER, + protocolVersion: '3.3', + community: { + rootPubkey: keyPair.publicKey, + foreign: 0, + iotaTopic: topic, + }, + otherCommunity: { + rootPubkey: foreignKeyPair.publicKey, + foreign: 1, + iotaTopic: foreignTopic, + }, + signingAccount: { + derive2Pubkey: firstUser.account.derive2Pubkey, + }, + recipientAccount: { + derive2Pubkey: foreignUser.account.derive2Pubkey, + }, + amount: new Decimal(100), + backendTransactions: [ + { + typeId: InputTransactionType.SEND, + }, + ], + }) const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) - console.log(new TransactionBodyLoggingView(body)) - console.log(new TransactionLoggingView(transaction)) + // console.log(new TransactionBodyLoggingView(body)) + expect(body.transfer).toBeDefined() + if (!body.transfer) throw new Error() + expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0) + expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe( + 0, + ) + expect(body).toMatchObject({ + type: CrossGroupType.OUTBOUND, + transfer: { + sender: { + amount: '100', + }, + }, + }) }) it('cross group recv transaction', async () => { const crossGroupRecvTransactionDraft = new TransactionDraft() crossGroupRecvTransactionDraft.amount = new Decimal('100') - crossGroupRecvTransactionDraft.backendTransactionId = 1 + crossGroupRecvTransactionDraft.backendTransactionId = 5 crossGroupRecvTransactionDraft.createdAt = new Date().toISOString() - crossGroupRecvTransactionDraft.linkedUser = foreignUser - crossGroupRecvTransactionDraft.user = firstUser + crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier + crossGroupRecvTransactionDraft.user = foreignUser.identifier crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE crossGroupRecvTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() + // console.log(new TransactionLoggingView(transaction)) + expect(transaction).toMatchObject({ + type: TransactionType.GRADIDO_TRANSFER, + protocolVersion: '3.3', + community: { + rootPubkey: keyPair.publicKey, + foreign: 0, + iotaTopic: topic, + }, + otherCommunity: { + rootPubkey: foreignKeyPair.publicKey, + foreign: 1, + iotaTopic: foreignTopic, + }, + signingAccount: { + derive2Pubkey: firstUser.account.derive2Pubkey, + }, + recipientAccount: { + derive2Pubkey: foreignUser.account.derive2Pubkey, + }, + amount: new Decimal(100), + backendTransactions: [ + { + typeId: InputTransactionType.RECEIVE, + }, + ], + }) const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) - console.log(new TransactionBodyLoggingView(body)) - console.log(new TransactionLoggingView(transaction)) + // console.log(new TransactionBodyLoggingView(body)) + expect(body.transfer).toBeDefined() + if (!body.transfer) throw new Error() + expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0) + expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe( + 0, + ) + expect(body).toMatchObject({ + type: CrossGroupType.INBOUND, + transfer: { + sender: { + amount: '100', + }, + }, + }) }) }) diff --git a/dlt-connector/src/logging/TransferAmountLogging.view.ts b/dlt-connector/src/logging/TransferAmountLogging.view.ts index 8d320b99f..2384bfdb4 100644 --- a/dlt-connector/src/logging/TransferAmountLogging.view.ts +++ b/dlt-connector/src/logging/TransferAmountLogging.view.ts @@ -10,7 +10,7 @@ export class TransferAmountLoggingView extends AbstractLoggingView { // eslint-disable-next-line @typescript-eslint/no-explicit-any public toJSON(): any { return { - publicKey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat), + pubkey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat), amount: this.self.amount, communityId: this.self.communityId, } From 8a135bd9835dd65590b8bf4a1024643c3914a989 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 15:42:14 +0100 Subject: [PATCH 07/13] add test for transmitToIota context, move some test init code into seeding --- dlt-connector/jest.config.js | 2 +- dlt-connector/src/data/Account.logic.ts | 35 ++++ dlt-connector/src/data/Transaction.logic.ts | 23 ++- .../src/graphql/input/CommunityDraft.ts | 3 +- .../CreateTransactionRecipe.context.test.ts | 99 ++--------- .../transaction/TransactionRecipe.role.ts | 5 +- .../AbstractTransactionRecipe.role.ts | 8 +- .../TransmitToIota.context.test.ts | 167 ++++++++++++++++++ .../transmitToIota/TransmitToIota.context.ts | 17 +- .../src/logging/TransactionLogging.view.ts | 15 +- dlt-connector/test/seeding/Community.seed.ts | 28 +++ dlt-connector/test/seeding/UserSet.seed.ts | 55 ++++++ 12 files changed, 355 insertions(+), 102 deletions(-) create mode 100644 dlt-connector/src/data/Account.logic.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts create mode 100644 dlt-connector/test/seeding/Community.seed.ts create mode 100644 dlt-connector/test/seeding/UserSet.seed.ts diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 69bc64bb2..2de18cf50 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: 66, + lines: 71, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/src/data/Account.logic.ts b/dlt-connector/src/data/Account.logic.ts new file mode 100644 index 000000000..9cff66070 --- /dev/null +++ b/dlt-connector/src/data/Account.logic.ts @@ -0,0 +1,35 @@ +import { Account } from '@entity/Account' + +import { LogError } from '@/server/LogError' + +import { KeyPair } from './KeyPair' +import { UserLogic } from './User.logic' + +export class AccountLogic { + // eslint-disable-next-line no-useless-constructor + public constructor(private self: Account) {} + + /** + * calculate account key pair starting from community key pair => derive user key pair => derive account key pair + * @param communityKeyPair + */ + public calculateKeyPair(communityKeyPair: KeyPair): KeyPair { + if (!this.self.user) { + throw new LogError('missing user') + } + const userLogic = new UserLogic(this.self.user) + const accountKeyPair = userLogic + .calculateKeyPair(communityKeyPair) + .derive([this.self.derivationIndex]) + + if ( + this.self.derive2Pubkey && + this.self.derive2Pubkey.compare(accountKeyPair.publicKey) !== 0 + ) { + throw new LogError( + 'The freshly derived public key does not correspond to the stored public key', + ) + } + return accountKeyPair + } +} diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index 62f7c3732..9ca6330ba 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -77,14 +77,14 @@ export class TransactionLogic { logger.info('id is the same, it is the same transaction!') return false } + if ( this.self.signingAccountId !== otherTransaction.signingAccountId || this.self.recipientAccountId !== otherTransaction.recipientAccountId || this.self.communityId !== otherTransaction.communityId || this.self.otherCommunityId !== otherTransaction.otherCommunityId || - this.self.amount !== otherTransaction.amount || this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation || - this.self.createdAt !== otherTransaction.createdAt + this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime() ) { logger.debug('transaction a and b are not pairs', { a: new TransactionLoggingView(this.self), @@ -135,6 +135,25 @@ export class TransactionLogic { logger.info(`TransactionType ${type} couldn't be a CrossGroup Transaction`) return false } + if ( + [ + TransactionType.GRADIDO_CREATION, + TransactionType.GRADIDO_TRANSFER, + TransactionType.GRADIDO_DEFERRED_TRANSFER, + ].includes(type) + ) { + if (!this.self.amount || !otherTransaction.amount) { + logger.info('missing amount') + return false + } + if (this.self.amount.cmp(otherTransaction.amount.toString())) { + logger.info('amounts mismatch', { + a: this.self.amount.toString(), + b: otherTransaction.amount.toString(), + }) + return false + } + } if (body.otherGroup === otherBody.otherGroup) { logger.info('otherGroups are the same', { a: new TransactionBodyLoggingView(body), diff --git a/dlt-connector/src/graphql/input/CommunityDraft.ts b/dlt-connector/src/graphql/input/CommunityDraft.ts index 665e10b75..0b26e68d0 100644 --- a/dlt-connector/src/graphql/input/CommunityDraft.ts +++ b/dlt-connector/src/graphql/input/CommunityDraft.ts @@ -1,10 +1,9 @@ // https://www.npmjs.com/package/@apollo/protobufjs +import { isValidDateString } from '@validator/DateString' import { IsBoolean, IsUUID } from 'class-validator' import { Field, InputType } from 'type-graphql' -import { isValidDateString } from '@validator/DateString' - @InputType() export class CommunityDraft { @Field(() => String) diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index 03474ff28..9ddbebd06 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -1,33 +1,25 @@ import 'reflect-metadata' import { Account } from '@entity/Account' -import { Community } from '@entity/Community' -import { User } from '@entity/User' 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 { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context' -import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' + +// eslint-disable-next-line import/order +import { communitySeed } from '@test/seeding/Community.seed' +// eslint-disable-next-line import/order +import { createUserSet, UserSet } from '@test/seeding/UserSet.seed' jest.mock('@typeorm/DataSource', () => ({ getDataSource: jest.fn(() => TestDB.instance.dbConnect), @@ -37,58 +29,11 @@ CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa const homeCommunityUuid = v4() const foreignCommunityUuid = v4() -type UserSet = { - identifier: UserIdentifier - user: User - account: Account -} - -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'), ) -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 -} - -function createUserSet(userUuid: string, communityUuid: string): UserSet { - const identifier = createUserIdentifier(userUuid, communityUuid) - const account = createUserAndAccount(identifier) - if (!account.user) { - throw Error('user missing') - } - return { - identifier, - account, - user: account.user, - } -} - let moderator: UserSet let firstUser: UserSet let secondUser: UserSet @@ -100,29 +45,13 @@ const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid) 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() + await communitySeed(homeCommunityUuid, false) + await communitySeed(foreignCommunityUuid, true, foreignKeyPair) - 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() - - moderator = createUserSet('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) - firstUser = createUserSet('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) - secondUser = createUserSet('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) - foreignUser = createUserSet('b0155716-5219-4c50-b3d3-0757721ae0d2', foreignCommunityUuid) + moderator = createUserSet(homeCommunityUuid, keyPair) + firstUser = createUserSet(homeCommunityUuid, keyPair) + secondUser = createUserSet(homeCommunityUuid, keyPair) + foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair) await Account.save([ moderator.account, @@ -197,7 +126,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context sendTransactionDraft.linkedUser = secondUser.identifier sendTransactionDraft.user = firstUser.identifier sendTransactionDraft.type = InputTransactionType.SEND - sendTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(sendTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() @@ -251,7 +179,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context recvTransactionDraft.linkedUser = firstUser.identifier recvTransactionDraft.user = secondUser.identifier recvTransactionDraft.type = InputTransactionType.RECEIVE - recvTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(recvTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() @@ -304,7 +231,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier crossGroupSendTransactionDraft.user = firstUser.identifier crossGroupSendTransactionDraft.type = InputTransactionType.SEND - crossGroupSendTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() @@ -361,7 +287,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier crossGroupRecvTransactionDraft.user = foreignUser.identifier crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE - crossGroupRecvTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts index d36aa98cc..7c6d3015d 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -1,5 +1,6 @@ import { Transaction } from '@entity/Transaction' +import { AccountLogic } from '@/data/Account.logic' import { KeyPair } from '@/data/KeyPair' import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' import { TransactionBuilder } from '@/data/Transaction.builder' @@ -57,9 +58,11 @@ export class TransactionRecipeRole { await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser) } const transaction = this.transactionBuilder.getTransaction() + const communityKeyPair = new KeyPair(this.transactionBuilder.getCommunity()) + const accountLogic = new AccountLogic(signingAccount) // sign this.transactionBuilder.setSignature( - new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes), + accountLogic.calculateKeyPair(communityKeyPair).sign(transaction.bodyBytes), ) return this } diff --git a/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts index 21285705e..23fd9d275 100644 --- a/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts @@ -11,14 +11,14 @@ import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLoggi import { logger } from '@/logging/logger' export abstract class AbstractTransactionRecipeRole { - protected transactionBody: TransactionBody | undefined - // eslint-disable-next-line no-useless-constructor - public constructor(protected self: Transaction) {} + protected transactionBody: TransactionBody + public constructor(protected self: Transaction) { + this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) + } public abstract transmitToIota(): Promise protected getGradidoTransaction(): GradidoTransaction { - this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) const transaction = new GradidoTransaction(this.transactionBody) if (!this.self.signature) { throw new TransactionError( diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts new file mode 100644 index 000000000..8e1ba4f33 --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts @@ -0,0 +1,167 @@ +import 'reflect-metadata' +import { Account } from '@entity/Account' +import { TestDB } from '@test/TestDB' +import { Decimal } from 'decimal.js-light' +import { v4 } from 'uuid' + +import { CONFIG } from '@/config' +import { KeyPair } from '@/data/KeyPair' +import { Mnemonic } from '@/data/Mnemonic' +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +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 { TransmitToIotaContext } from './TransmitToIota.context' + +// eslint-disable-next-line import/order +import { communitySeed } from '@test/seeding/Community.seed' +// eslint-disable-next-line import/order +import { createUserSet, UserSet } from '@test/seeding/UserSet.seed' + +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) + +jest.mock('@/client/IotaClient', () => { + return { + sendMessage: jest.fn().mockReturnValue({ + messageId: '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', + }), + } +}) + +CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285' +const homeCommunityUuid = v4() +const foreignCommunityUuid = v4() + +const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED)) +const foreignKeyPair = new KeyPair( + new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'), +) + +let moderator: UserSet +let firstUser: UserSet +let secondUser: UserSet +let foreignUser: UserSet + +const now = new Date() + +describe('interactions/transmitToIota/TransmitToIotaContext', () => { + beforeAll(async () => { + await TestDB.instance.setupTestDB() + await communitySeed(homeCommunityUuid, false) + await communitySeed(foreignCommunityUuid, true, foreignKeyPair) + + moderator = createUserSet(homeCommunityUuid, keyPair) + firstUser = createUserSet(homeCommunityUuid, keyPair) + secondUser = createUserSet(homeCommunityUuid, keyPair) + foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair) + + await Account.save([ + moderator.account, + firstUser.account, + secondUser.account, + foreignUser.account, + ]) + }) + + afterAll(async () => { + await TestDB.instance.teardownTestDB() + }) + + it('LOCAL transaction', async () => { + const creationTransactionDraft = new TransactionDraft() + creationTransactionDraft.amount = new Decimal('2000') + creationTransactionDraft.backendTransactionId = 1 + creationTransactionDraft.createdAt = new Date().toISOString() + creationTransactionDraft.linkedUser = moderator.identifier + creationTransactionDraft.user = firstUser.identifier + creationTransactionDraft.type = InputTransactionType.CREATION + creationTransactionDraft.targetDate = new Date().toISOString() + const transactionRecipeContext = new CreateTransactionRecipeContext(creationTransactionDraft) + await transactionRecipeContext.run() + const transaction = transactionRecipeContext.getTransactionRecipe() + + const context = new TransmitToIotaContext(transaction) + const debugSpy = jest.spyOn(logger, 'debug') + await context.run() + expect( + transaction.iotaMessageId?.compare( + Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'), + ), + ).toBe(0) + expect(debugSpy).toHaveBeenNthCalledWith( + 3, + expect.stringContaining('transmit LOCAL transaction to iota'), + expect.objectContaining({}), + ) + }) + + it('OUTBOUND transaction', async () => { + const crossGroupSendTransactionDraft = new TransactionDraft() + crossGroupSendTransactionDraft.amount = new Decimal('100') + crossGroupSendTransactionDraft.backendTransactionId = 4 + crossGroupSendTransactionDraft.createdAt = now.toISOString() + crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier + crossGroupSendTransactionDraft.user = firstUser.identifier + crossGroupSendTransactionDraft.type = InputTransactionType.SEND + const transactionRecipeContext = new CreateTransactionRecipeContext( + crossGroupSendTransactionDraft, + ) + await transactionRecipeContext.run() + const transaction = transactionRecipeContext.getTransactionRecipe() + await transaction.save() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + expect(body.type).toBe(CrossGroupType.OUTBOUND) + const context = new TransmitToIotaContext(transaction) + const debugSpy = jest.spyOn(logger, 'debug') + await context.run() + expect( + transaction.iotaMessageId?.compare( + Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'), + ), + ).toBe(0) + expect(debugSpy).toHaveBeenNthCalledWith( + 5, + expect.stringContaining('transmit OUTBOUND transaction to iota'), + expect.objectContaining({}), + ) + }) + + it('INBOUND transaction', async () => { + const crossGroupRecvTransactionDraft = new TransactionDraft() + crossGroupRecvTransactionDraft.amount = new Decimal('100') + crossGroupRecvTransactionDraft.backendTransactionId = 5 + crossGroupRecvTransactionDraft.createdAt = now.toISOString() + crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier + crossGroupRecvTransactionDraft.user = foreignUser.identifier + crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE + const transactionRecipeContext = new CreateTransactionRecipeContext( + crossGroupRecvTransactionDraft, + ) + await transactionRecipeContext.run() + const transaction = transactionRecipeContext.getTransactionRecipe() + await transaction.save() + // console.log(new TransactionLoggingView(transaction)) + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + expect(body.type).toBe(CrossGroupType.INBOUND) + + const context = new TransmitToIotaContext(transaction) + const debugSpy = jest.spyOn(logger, 'debug') + await context.run() + expect( + transaction.iotaMessageId?.compare( + Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'), + ), + ).toBe(0) + expect(debugSpy).toHaveBeenNthCalledWith( + 7, + expect.stringContaining('transmit INBOUND transaction to iota'), + expect.objectContaining({}), + ) + }) +}) diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts index 8e0b32c6c..f29d1742a 100644 --- a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts @@ -2,7 +2,10 @@ import { Transaction } from '@entity/Transaction' import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { logger } from '@/logging/logger' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' import { LogError } from '@/server/LogError' +import { getDataSource } from '@/typeorm/DataSource' import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role' import { InboundTransactionRecipeRole } from './InboundTransactionRecipe.role' @@ -36,7 +39,19 @@ export class TransmitToIotaContext { public async run(): Promise { const transaction = await this.transactionRecipeRole.transmitToIota() + logger.debug('transaction sended via iota', new TransactionLoggingView(transaction)) // store changes in db - await transaction.save() + // prevent endless loop + const paringTransaction = transaction.paringTransaction + if (paringTransaction) { + transaction.paringTransaction = undefined + await getDataSource().transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.save(transaction) + await transactionalEntityManager.save(paringTransaction) + }) + } else { + await transaction.save() + } + logger.info('sended transaction successfully updated in db') } } diff --git a/dlt-connector/src/logging/TransactionLogging.view.ts b/dlt-connector/src/logging/TransactionLogging.view.ts index 38443024d..d04d46556 100644 --- a/dlt-connector/src/logging/TransactionLogging.view.ts +++ b/dlt-connector/src/logging/TransactionLogging.view.ts @@ -18,7 +18,7 @@ export class TransactionLoggingView extends AbstractLoggingView { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public toJSON(showBackendTransactions = true): any { + public toJSON(showBackendTransactions = true, deep = 1): any { return { id: this.self.id, nr: this.self.nr, @@ -31,16 +31,23 @@ export class TransactionLoggingView extends AbstractLoggingView { community: new CommunityLoggingView(this.self.community).toJSON(), otherCommunity: this.self.otherCommunity ? new CommunityLoggingView(this.self.otherCommunity) - : undefined, + : { id: this.self.otherCommunityId }, iotaMessageId: this.self.iotaMessageId ? this.self.iotaMessageId.toString(this.bufferStringFormat) : undefined, signingAccount: this.self.signingAccount ? new AccountLoggingView(this.self.signingAccount) - : undefined, + : { id: this.self.signingAccountId }, recipientAccount: this.self.recipientAccount ? new AccountLoggingView(this.self.recipientAccount) - : undefined, + : { id: this.self.recipientAccountId }, + pairingTransaction: + this.self.paringTransaction && deep === 1 + ? new TransactionLoggingView(this.self.paringTransaction).toJSON( + showBackendTransactions, + deep + 1, + ) + : { id: this.self.paringTransactionId }, amount: this.decimalToString(this.self.amount), accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation), accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation), diff --git a/dlt-connector/test/seeding/Community.seed.ts b/dlt-connector/test/seeding/Community.seed.ts new file mode 100644 index 000000000..a1b042ef2 --- /dev/null +++ b/dlt-connector/test/seeding/Community.seed.ts @@ -0,0 +1,28 @@ +import { Community } from '@entity/Community' + +import { KeyPair } from '@/data/KeyPair' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' + +export const communitySeed = async ( + uuid: string, + foreign: boolean, + keyPair: KeyPair | undefined = undefined, +): Promise => { + const homeCommunityDraft = new CommunityDraft() + homeCommunityDraft.uuid = uuid + homeCommunityDraft.foreign = foreign + homeCommunityDraft.createdAt = new Date().toISOString() + const iotaTopic = iotaTopicFromCommunityUUID(uuid) + const addCommunityContext = new AddCommunityContext(homeCommunityDraft, iotaTopic) + await addCommunityContext.run() + + const community = await Community.findOneOrFail({ where: { iotaTopic } }) + if (foreign && keyPair) { + // that isn't entirely correct, normally only the public key from foreign community is know, and will be come form blockchain + keyPair.fillInCommunityKeys(community) + await community.save() + } + return community +} diff --git a/dlt-connector/test/seeding/UserSet.seed.ts b/dlt-connector/test/seeding/UserSet.seed.ts new file mode 100644 index 000000000..933b386ca --- /dev/null +++ b/dlt-connector/test/seeding/UserSet.seed.ts @@ -0,0 +1,55 @@ +import { Account } from '@entity/Account' +import { User } from '@entity/User' +import { v4 } from 'uuid' + +import { AccountFactory } from '@/data/Account.factory' +import { KeyPair } from '@/data/KeyPair' +import { UserFactory } from '@/data/User.factory' +import { UserLogic } from '@/data/User.logic' +import { AccountType } from '@/graphql/enum/AccountType' +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' + +export type UserSet = { + identifier: UserIdentifier + user: User + account: Account +} + +export const createUserIdentifier = (userUuid: string, communityUuid: string): UserIdentifier => { + const user = new UserIdentifier() + user.uuid = userUuid + user.communityUuid = communityUuid + return user +} + +export const createUserAndAccount = ( + userIdentifier: UserIdentifier, + communityKeyPair: KeyPair, +): Account => { + const accountDraft = new UserAccountDraft() + accountDraft.user = userIdentifier + accountDraft.createdAt = new Date().toISOString() + accountDraft.accountType = AccountType.COMMUNITY_HUMAN + const user = UserFactory.create(accountDraft, communityKeyPair) + const userLogic = new UserLogic(user) + const account = AccountFactory.createAccountFromUserAccountDraft( + accountDraft, + userLogic.calculateKeyPair(communityKeyPair), + ) + account.user = user + return account +} + +export const createUserSet = (communityUuid: string, communityKeyPair: KeyPair): UserSet => { + const identifier = createUserIdentifier(v4(), communityUuid) + const account = createUserAndAccount(identifier, communityKeyPair) + if (!account.user) { + throw Error('user missing') + } + return { + identifier, + account, + user: account.user, + } +} From 8445c0bec27e3df27838b56ecb7aea04cc16f954 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 17:23:09 +0100 Subject: [PATCH 08/13] test paring transaction validation --- .../src/data/Transaction.logic.test.ts | 323 ++++++++++++++++++ dlt-connector/src/data/Transaction.logic.ts | 62 ++-- 2 files changed, 364 insertions(+), 21 deletions(-) create mode 100644 dlt-connector/src/data/Transaction.logic.test.ts diff --git a/dlt-connector/src/data/Transaction.logic.test.ts b/dlt-connector/src/data/Transaction.logic.test.ts new file mode 100644 index 000000000..c652fe794 --- /dev/null +++ b/dlt-connector/src/data/Transaction.logic.test.ts @@ -0,0 +1,323 @@ +import { Community } from '@entity/Community' +import { Transaction } from '@entity/Transaction' +import { Decimal } from 'decimal.js-light' + +import { logger } from '@/logging/logger' + +import { CommunityRoot } from './proto/3_3/CommunityRoot' +import { CrossGroupType } from './proto/3_3/enum/CrossGroupType' +import { GradidoTransfer } from './proto/3_3/GradidoTransfer' +import { RegisterAddress } from './proto/3_3/RegisterAddress' +import { TransactionBody } from './proto/3_3/TransactionBody' +import { TransactionLogic } from './Transaction.logic' +import { GradidoCreation } from './proto/3_3/GradidoCreation' +import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer' + +let a: Transaction +let b: Transaction + +describe('data/transaction.logic', () => { + describe('isBelongTogether', () => { + beforeEach(() => { + const now = new Date() + let body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.transfer = new GradidoTransfer() + body.otherGroup = 'recipient group' + + a = new Transaction() + a.community = new Community() + a.communityId = 1 + a.otherCommunityId = 2 + a.id = 1 + a.signingAccountId = 1 + a.recipientAccountId = 2 + a.createdAt = now + a.amount = new Decimal('100') + a.signature = Buffer.alloc(64) + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + + body = new TransactionBody() + body.type = CrossGroupType.INBOUND + body.transfer = new GradidoTransfer() + body.otherGroup = 'sending group' + + b = new Transaction() + b.community = new Community() + b.communityId = 1 + b.otherCommunityId = 2 + b.id = 2 + b.signingAccountId = 1 + b.recipientAccountId = 2 + b.createdAt = now + b.amount = new Decimal('100') + b.signature = Buffer.alloc(64) + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + }) + + const spy = jest.spyOn(logger, 'info') + + it('true', () => { + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(true) + }) + + it('false because of same id', () => { + b.id = 1 + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith('id is the same, it is the same transaction!') + }) + + it('false because of different signing accounts', () => { + b.signingAccountId = 17 + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + 'transaction a and b are not pairs', + expect.objectContaining({}), + ) + }) + + it('false because of different recipient accounts', () => { + b.recipientAccountId = 21 + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + 'transaction a and b are not pairs', + expect.objectContaining({}), + ) + }) + + it('false because of different community ids', () => { + b.communityId = 6 + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + 'transaction a and b are not pairs', + expect.objectContaining({}), + ) + }) + + it('false because of different other community ids', () => { + b.otherCommunityId = 3 + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + 'transaction a and b are not pairs', + expect.objectContaining({}), + ) + }) + + it('false because of different createdAt', () => { + b.createdAt = new Date('2021-01-01T17:12') + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + 'transaction a and b are not pairs', + expect.objectContaining({}), + ) + }) + + describe('false because of mismatching cross group type', () => { + const body = new TransactionBody() + it('a is LOCAL', () => { + body.type = CrossGroupType.LOCAL + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenNthCalledWith(7, 'no one can be LOCAL') + expect(spy).toHaveBeenLastCalledWith( + "cross group types don't match", + expect.objectContaining({}), + ) + }) + + it('b is LOCAL', () => { + body.type = CrossGroupType.LOCAL + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenNthCalledWith(9, 'no one can be LOCAL') + expect(spy).toHaveBeenLastCalledWith( + "cross group types don't match", + expect.objectContaining({}), + ) + }) + + it('both are INBOUND', () => { + body.type = CrossGroupType.INBOUND + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "cross group types don't match", + expect.objectContaining({}), + ) + }) + + it('both are OUTBOUND', () => { + body.type = CrossGroupType.OUTBOUND + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "cross group types don't match", + expect.objectContaining({}), + ) + }) + + it('a is CROSS', () => { + body.type = CrossGroupType.CROSS + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "cross group types don't match", + expect.objectContaining({}), + ) + }) + + it('b is CROSS', () => { + body.type = CrossGroupType.CROSS + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "cross group types don't match", + expect.objectContaining({}), + ) + }) + + it('true with a as INBOUND and b as OUTBOUND', () => { + let body = TransactionBody.fromBodyBytes(a.bodyBytes) + body.type = CrossGroupType.INBOUND + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + body = TransactionBody.fromBodyBytes(b.bodyBytes) + body.type = CrossGroupType.OUTBOUND + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(true) + }) + }) + + describe('false because of transaction type not suitable for cross group transactions', () => { + const body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + it('without transaction type (broken TransactionBody)', () => { + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(() => logic.isBelongTogether(b)).toThrowError("couldn't determine transaction type") + }) + + it('not the same transaction types', () => { + const body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.registerAddress = new RegisterAddress() + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "transaction types don't match", + expect.objectContaining({}), + ) + }) + + it('community root cannot be a cross group transaction', () => { + let body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.communityRoot = new CommunityRoot() + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + body = new TransactionBody() + body.type = CrossGroupType.INBOUND + body.communityRoot = new CommunityRoot() + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "TransactionType COMMUNITY_ROOT couldn't be a CrossGroup Transaction", + ) + }) + + it('Gradido Creation cannot be a cross group transaction', () => { + let body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.creation = new GradidoCreation() + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + body = new TransactionBody() + body.type = CrossGroupType.INBOUND + body.creation = new GradidoCreation() + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "TransactionType GRADIDO_CREATION couldn't be a CrossGroup Transaction", + ) + }) + + it('Deferred Transfer cannot be a cross group transaction', () => { + let body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.deferredTransfer = new GradidoDeferredTransfer() + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + body = new TransactionBody() + body.type = CrossGroupType.INBOUND + body.deferredTransfer = new GradidoDeferredTransfer() + b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith( + "TransactionType GRADIDO_DEFERRED_TRANSFER couldn't be a CrossGroup Transaction", + ) + }) + }) + + describe('false because of wrong amount', () => { + it('amount missing on a', () => { + a.amount = undefined + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith('missing amount') + }) + + it('amount missing on b', () => { + b.amount = undefined + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith('missing amount') + }) + + it('amount not the same', () => { + a.amount = new Decimal('101') + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith('amounts mismatch', expect.objectContaining({})) + }) + }) + + it('false because otherGroup are the same', () => { + const body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.transfer = new GradidoTransfer() + body.otherGroup = 'sending group' + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith('otherGroups are the same', expect.objectContaining({})) + }) + + it('false because of different memos', () => { + const body = new TransactionBody() + body.type = CrossGroupType.OUTBOUND + body.transfer = new GradidoTransfer() + body.otherGroup = 'recipient group' + body.memo = 'changed memo' + a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish()) + const logic = new TransactionLogic(a) + expect(logic.isBelongTogether(b)).toBe(false) + expect(spy).toHaveBeenLastCalledWith('memo differ', expect.objectContaining({})) + }) + }) +}) diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index 9ca6330ba..f237e9b3b 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -83,30 +83,26 @@ export class TransactionLogic { this.self.recipientAccountId !== otherTransaction.recipientAccountId || this.self.communityId !== otherTransaction.communityId || this.self.otherCommunityId !== otherTransaction.otherCommunityId || - this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation || this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime() ) { - logger.debug('transaction a and b are not pairs', { - a: new TransactionLoggingView(this.self), - b: new TransactionLoggingView(otherTransaction), + logger.info('transaction a and b are not pairs', { + a: new TransactionLoggingView(this.self).toJSON(), + b: new TransactionLoggingView(otherTransaction).toJSON(), }) return false } const body = this.getBody() const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes) /** - * both can be Cross + * both must be Cross or * one can be OUTBOUND and one can be INBOUND * no one can be LOCAL */ - if ( - (body.type === otherBody.type && body.type !== CrossGroupType.CROSS) || - body.type === CrossGroupType.LOCAL || - otherBody.type === CrossGroupType.LOCAL - ) { + + if (!this.validCrossGroupTypes(body.type, otherBody.type)) { logger.info("cross group types don't match", { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) return false } @@ -114,14 +110,14 @@ export class TransactionLogic { const otherType = otherBody.getTransactionType() if (!type || !otherType) { throw new LogError("couldn't determine transaction type", { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) } if (type !== otherType) { logger.info("transaction types don't match", { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) return false } @@ -132,7 +128,7 @@ export class TransactionLogic { TransactionType.GRADIDO_DEFERRED_TRANSFER, ].includes(type) ) { - logger.info(`TransactionType ${type} couldn't be a CrossGroup Transaction`) + logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`) return false } if ( @@ -156,21 +152,45 @@ export class TransactionLogic { } if (body.otherGroup === otherBody.otherGroup) { logger.info('otherGroups are the same', { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) return false } if (body.memo !== otherBody.memo) { logger.info('memo differ', { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) return false } return true } + /** + * both must be CROSS or + * one can be OUTBOUND and one can be INBOUND + * no one can be LOCAL + * @return true if crossGroupTypes are valid + */ + protected validCrossGroupTypes(a: CrossGroupType, b: CrossGroupType): boolean { + logger.debug('compare ', { + a: CrossGroupType[a], + b: CrossGroupType[b], + }) + if (a === CrossGroupType.LOCAL || b === CrossGroupType.LOCAL) { + logger.info('no one can be LOCAL') + return false + } + if ( + (a === CrossGroupType.INBOUND && b === CrossGroupType.OUTBOUND) || + (a === CrossGroupType.OUTBOUND && b === CrossGroupType.INBOUND) + ) { + return true // One can be INBOUND and one can be OUTBOUND + } + return a === CrossGroupType.CROSS && b === CrossGroupType.CROSS + } + public getBody(): TransactionBody { if (!this.transactionBody) { this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) From 23843bdd14f9413cecbd19190f8852bac05f97a2 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 17:43:14 +0100 Subject: [PATCH 09/13] lint --- dlt-connector/jest.config.js | 2 +- dlt-connector/package.json | 3 +- .../src/data/Transaction.logic.test.ts | 4 +-- .../src/graphql/input/CommunityDraft.ts | 3 +- .../community/AddCommunity.context.test.ts | 1 + .../CreateTransactionRecipe.context.test.ts | 3 +- .../TransmitToIota.context.test.ts | 3 +- dlt-connector/src/utils/typeConverter.test.ts | 36 +++++++++++++++++-- dlt-connector/yarn.lock | 2 +- 9 files changed, 47 insertions(+), 10 deletions(-) diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 2de18cf50..3d731787f 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: 71, + lines: 75, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 8b5ae357c..7c8644150 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -39,7 +39,8 @@ "reflect-metadata": "^0.1.13", "sodium-native": "^4.0.4", "tsconfig-paths": "^4.1.2", - "type-graphql": "^2.0.0-beta.2" + "type-graphql": "^2.0.0-beta.2", + "uuid": "^9.0.1" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^3.2.1", diff --git a/dlt-connector/src/data/Transaction.logic.test.ts b/dlt-connector/src/data/Transaction.logic.test.ts index c652fe794..fe9c2c085 100644 --- a/dlt-connector/src/data/Transaction.logic.test.ts +++ b/dlt-connector/src/data/Transaction.logic.test.ts @@ -6,12 +6,12 @@ import { logger } from '@/logging/logger' import { CommunityRoot } from './proto/3_3/CommunityRoot' import { CrossGroupType } from './proto/3_3/enum/CrossGroupType' +import { GradidoCreation } from './proto/3_3/GradidoCreation' +import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer' import { GradidoTransfer } from './proto/3_3/GradidoTransfer' import { RegisterAddress } from './proto/3_3/RegisterAddress' import { TransactionBody } from './proto/3_3/TransactionBody' import { TransactionLogic } from './Transaction.logic' -import { GradidoCreation } from './proto/3_3/GradidoCreation' -import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer' let a: Transaction let b: Transaction diff --git a/dlt-connector/src/graphql/input/CommunityDraft.ts b/dlt-connector/src/graphql/input/CommunityDraft.ts index 0b26e68d0..665e10b75 100644 --- a/dlt-connector/src/graphql/input/CommunityDraft.ts +++ b/dlt-connector/src/graphql/input/CommunityDraft.ts @@ -1,9 +1,10 @@ // https://www.npmjs.com/package/@apollo/protobufjs -import { isValidDateString } from '@validator/DateString' import { IsBoolean, IsUUID } from 'class-validator' import { Field, InputType } from 'type-graphql' +import { isValidDateString } from '@validator/DateString' + @InputType() export class CommunityDraft { @Field(() => String) diff --git a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts index fec2273b6..d7ec4e9c6 100644 --- a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts @@ -1,5 +1,6 @@ import 'reflect-metadata' import { Community } from '@entity/Community' + import { TestDB } from '@test/TestDB' import { CONFIG } from '@/config' diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index 9ddbebd06..0382632b3 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -1,9 +1,10 @@ import 'reflect-metadata' import { Account } from '@entity/Account' -import { TestDB } from '@test/TestDB' import { Decimal } from 'decimal.js-light' import { v4 } from 'uuid' +import { TestDB } from '@test/TestDB' + import { CONFIG } from '@/config' import { KeyPair } from '@/data/KeyPair' import { Mnemonic } from '@/data/Mnemonic' diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts index 8e1ba4f33..94a8e4f9d 100644 --- a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts @@ -1,9 +1,10 @@ import 'reflect-metadata' import { Account } from '@entity/Account' -import { TestDB } from '@test/TestDB' import { Decimal } from 'decimal.js-light' import { v4 } from 'uuid' +import { TestDB } from '@test/TestDB' + import { CONFIG } from '@/config' import { KeyPair } from '@/data/KeyPair' import { Mnemonic } from '@/data/Mnemonic' diff --git a/dlt-connector/src/utils/typeConverter.test.ts b/dlt-connector/src/utils/typeConverter.test.ts index d9b1c2356..4caee94bb 100644 --- a/dlt-connector/src/utils/typeConverter.test.ts +++ b/dlt-connector/src/utils/typeConverter.test.ts @@ -1,13 +1,45 @@ import 'reflect-metadata' + import { Timestamp } from '@/data/proto/3_3/Timestamp' -import { timestampToDate } from './typeConverter' +import { + base64ToBuffer, + iotaTopicFromCommunityUUID, + timestampSecondsToDate, + timestampToDate, + uuid4ToBuffer, +} from './typeConverter' describe('utils/typeConverter', () => { + it('uuid4ToBuffer', () => { + expect(uuid4ToBuffer('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toStrictEqual( + Buffer.from('4f28e0815c394ddeb6a43bde71de8d65', 'hex'), + ) + }) + + it('iotaTopicFromCommunityUUID', () => { + expect(iotaTopicFromCommunityUUID('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toBe( + '3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd', + ) + }) + it('timestampToDate', () => { - const now = new Date('Thu, 05 Oct 2023 11:55:18 +0000') + const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000') const timestamp = new Timestamp(now) expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000)) expect(timestampToDate(timestamp)).toEqual(now) }) + + it('timestampSecondsToDate', () => { + const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000') + const timestamp = new Timestamp(now) + expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000)) + expect(timestampSecondsToDate(timestamp)).toEqual(new Date('Thu, 05 Oct 2023 11:55:18 +0000')) + }) + + it('base64ToBuffer', () => { + expect(base64ToBuffer('MTizWQMR/fCoI+FzyqlIe30nXCP6sHEGtLE2TLA4r/0=')).toStrictEqual( + Buffer.from('3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd', 'hex'), + ) + }) }) diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 6d50426b1..7f46d88bc 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -6388,7 +6388,7 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: +uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== From 95b4810631c72b2e25c16eaff4c938deb16c24e0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 3 Feb 2024 16:30:07 +0100 Subject: [PATCH 10/13] change logic, transaction.community is now community on which blockchain it is, not longer signerCommunity, transaction.otherCommunity is now the same like TransactionBody.otherGroup --- dlt-connector/src/data/Transaction.builder.ts | 16 +++++----- .../src/data/Transaction.logic.test.ts | 4 +-- dlt-connector/src/data/Transaction.logic.ts | 4 +-- .../src/graphql/input/TransactionDraft.ts | 7 ++--- .../src/graphql/input/UserIdentifier.ts | 4 +-- .../transaction/AbstractTransaction.role.ts | 9 ++++-- .../CreateTransactionRecipe.context.test.ts | 12 ++++---- .../transaction/CreationTransaction.role.ts | 29 +++++++++++++++++++ .../transaction/TransactionRecipe.role.ts | 22 ++++++++++++-- dlt-connector/tsconfig.json | 2 +- 10 files changed, 79 insertions(+), 30 deletions(-) diff --git a/dlt-connector/src/data/Transaction.builder.ts b/dlt-connector/src/data/Transaction.builder.ts index 115391e91..f46f02a29 100644 --- a/dlt-connector/src/data/Transaction.builder.ts +++ b/dlt-connector/src/data/Transaction.builder.ts @@ -59,6 +59,10 @@ export class TransactionBuilder { return this.transaction.community } + public getOtherCommunity(): Community | undefined { + return this.transaction.otherCommunity + } + public setSigningAccount(signingAccount: Account): TransactionBuilder { this.transaction.signingAccount = signingAccount return this @@ -103,22 +107,18 @@ export class TransactionBuilder { return this } - public async setSenderCommunityFromSenderUser( - senderUser: UserIdentifier, - ): Promise { + public async setCommunityFromUser(user: UserIdentifier): Promise { // get sender community - const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser) + const community = await CommunityRepository.getCommunityForUserIdentifier(user) if (!community) { throw new LogError("couldn't find community for transaction") } return this.setCommunity(community) } - public async setOtherCommunityFromRecipientUser( - recipientUser: UserIdentifier, - ): Promise { + public async setOtherCommunityFromUser(user: UserIdentifier): Promise { // get recipient community - const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser) + const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user) return this.setOtherCommunity(otherCommunity) } diff --git a/dlt-connector/src/data/Transaction.logic.test.ts b/dlt-connector/src/data/Transaction.logic.test.ts index fe9c2c085..b5ef73de2 100644 --- a/dlt-connector/src/data/Transaction.logic.test.ts +++ b/dlt-connector/src/data/Transaction.logic.test.ts @@ -44,8 +44,8 @@ describe('data/transaction.logic', () => { b = new Transaction() b.community = new Community() - b.communityId = 1 - b.otherCommunityId = 2 + b.communityId = 2 + b.otherCommunityId = 1 b.id = 2 b.signingAccountId = 1 b.recipientAccountId = 2 diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index f237e9b3b..f61c9a6e9 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -81,8 +81,8 @@ export class TransactionLogic { if ( this.self.signingAccountId !== otherTransaction.signingAccountId || this.self.recipientAccountId !== otherTransaction.recipientAccountId || - this.self.communityId !== otherTransaction.communityId || - this.self.otherCommunityId !== otherTransaction.otherCommunityId || + this.self.communityId !== otherTransaction.otherCommunityId || + this.self.otherCommunityId !== otherTransaction.communityId || this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime() ) { logger.info('transaction a and b are not pairs', { diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index 541797565..c69a7dea3 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -1,11 +1,10 @@ // https://www.npmjs.com/package/@apollo/protobufjs -import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' -import { Decimal } from 'decimal.js-light' -import { InputType, Field, Int } from 'type-graphql' - import { InputTransactionType } from '@enum/InputTransactionType' import { isValidDateString } from '@validator/DateString' import { IsPositiveDecimal } from '@validator/Decimal' +import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' +import { Decimal } from 'decimal.js-light' +import { InputType, Field, Int } from 'type-graphql' import { UserIdentifier } from './UserIdentifier' diff --git a/dlt-connector/src/graphql/input/UserIdentifier.ts b/dlt-connector/src/graphql/input/UserIdentifier.ts index 12f2e5889..7d9035b93 100644 --- a/dlt-connector/src/graphql/input/UserIdentifier.ts +++ b/dlt-connector/src/graphql/input/UserIdentifier.ts @@ -9,9 +9,9 @@ export class UserIdentifier { @IsUUID('4') uuid: string - @Field(() => String, { nullable: true }) + @Field(() => String) @IsUUID('4') - communityUuid?: string + communityUuid: string @Field(() => Int, { defaultValue: 1, nullable: true }) @IsPositive() diff --git a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts index 62fcf90de..b23d381cd 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts @@ -1,8 +1,11 @@ +import { Community } from '@entity/Community' + import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { TransactionDraft } from '@/graphql/input/TransactionDraft' import { UserIdentifier } from '@/graphql/input/UserIdentifier' import { TransactionError } from '@/graphql/model/TransactionError' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' export abstract class AbstractTransactionRole { // eslint-disable-next-line no-useless-constructor @@ -26,7 +29,7 @@ export abstract class AbstractTransactionRole { * OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2' * INBOUND: goes to receiver, stored on receiver community blockchain * INBOUND: stored on 'gdd2', otherGroup: 'gdd1' - * @returns + * @returns iota topic */ public getOtherGroup(): string { let user: UserIdentifier @@ -42,7 +45,7 @@ export abstract class AbstractTransactionRole { 'missing sender/signing user community id for cross group transaction', ) } - return user.communityUuid + return iotaTopicFromCommunityUUID(user.communityUuid) case CrossGroupType.OUTBOUND: user = this.getRecipientUser() if (!user.communityUuid) { @@ -51,7 +54,7 @@ export abstract class AbstractTransactionRole { 'missing recipient user community id for cross group transaction', ) } - return user.communityUuid + return iotaTopicFromCommunityUUID(user.communityUuid) default: throw new TransactionError( TransactionErrorType.NOT_IMPLEMENTED_YET, diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index 0382632b3..e5535f3f7 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -272,6 +272,7 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context ) expect(body).toMatchObject({ type: CrossGroupType.OUTBOUND, + otherGroup: foreignTopic, transfer: { sender: { amount: '100', @@ -296,15 +297,15 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context type: TransactionType.GRADIDO_TRANSFER, protocolVersion: '3.3', community: { - rootPubkey: keyPair.publicKey, - foreign: 0, - iotaTopic: topic, - }, - otherCommunity: { rootPubkey: foreignKeyPair.publicKey, foreign: 1, iotaTopic: foreignTopic, }, + otherCommunity: { + rootPubkey: keyPair.publicKey, + foreign: 0, + iotaTopic: topic, + }, signingAccount: { derive2Pubkey: firstUser.account.derive2Pubkey, }, @@ -328,6 +329,7 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context ) expect(body).toMatchObject({ type: CrossGroupType.INBOUND, + otherGroup: topic, transfer: { sender: { amount: '100', diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts index 7b82f8805..f11518d02 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts @@ -1,5 +1,12 @@ +import { Community } from '@entity/Community' + +import { CommunityRepository } from '@/data/Community.repository' import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { TransactionError } from '@/graphql/model/TransactionError' +import { logger } from '@/logging/logger' +import { UserIdentifierLoggingView } from '@/logging/UserIdentifierLogging.view' import { AbstractTransactionRole } from './AbstractTransaction.role' @@ -15,4 +22,26 @@ export class CreationTransactionRole extends AbstractTransactionRole { public getCrossGroupType(): CrossGroupType { return CrossGroupType.LOCAL } + + public async getCommunity(): Promise { + if (this.self.user.communityUuid !== this.self.linkedUser.communityUuid) { + throw new TransactionError( + TransactionErrorType.LOGIC_ERROR, + 'mismatch community uuids on creation transaction', + ) + } + const community = await CommunityRepository.getCommunityForUserIdentifier(this.self.user) + if (!community) { + logger.error( + 'missing community for user identifier', + new UserIdentifierLoggingView(this.self.user), + ) + throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't find community for user") + } + return community + } + + public async getOtherCommunity(): Promise { + return null + } } diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts index 7c6d3015d..f1be50c75 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -1,7 +1,9 @@ +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' @@ -53,12 +55,15 @@ export class TransactionRecipeRole { this.transactionBuilder .fromTransactionBodyBuilder(transactionBodyBuilder) .addBackendTransaction(transactionDraft) - await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser) + + await this.transactionBuilder.setCommunityFromUser(transactionDraft.user) if (recipientUser.communityUuid !== signingUser.communityUuid) { - await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser) + await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser) } const transaction = this.transactionBuilder.getTransaction() - const communityKeyPair = new KeyPair(this.transactionBuilder.getCommunity()) + const communityKeyPair = new KeyPair( + this.getSigningCommunity(transactionTypeRole.getCrossGroupType()), + ) const accountLogic = new AccountLogic(signingAccount) // sign this.transactionBuilder.setSignature( @@ -67,6 +72,17 @@ export class TransactionRecipeRole { return this } + public getSigningCommunity(crossGroupType: CrossGroupType): Community { + if (crossGroupType === CrossGroupType.INBOUND) { + const otherCommunity = this.transactionBuilder.getOtherCommunity() + if (!otherCommunity) { + throw new TransactionError(TransactionErrorType.NOT_FOUND, 'missing other community') + } + return otherCommunity + } + return this.transactionBuilder.getCommunity() + } + public getTransaction(): Transaction { return this.transactionBuilder.getTransaction() } diff --git a/dlt-connector/tsconfig.json b/dlt-connector/tsconfig.json index e37b2a7a0..32525c013 100644 --- a/dlt-connector/tsconfig.json +++ b/dlt-connector/tsconfig.json @@ -63,7 +63,7 @@ "@entity/*": ["../dlt-database/entity/*", "../../dlt-database/build/entity/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["node_modules/@types", "@types"], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ From b2f77ddfbf01982357364e784701dec812972f4e Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 3 Feb 2024 16:43:19 +0100 Subject: [PATCH 11/13] fix linting --- dlt-connector/src/graphql/input/TransactionDraft.ts | 7 ++++--- .../backendToDb/transaction/AbstractTransaction.role.ts | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index c69a7dea3..541797565 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -1,11 +1,12 @@ // https://www.npmjs.com/package/@apollo/protobufjs -import { InputTransactionType } from '@enum/InputTransactionType' -import { isValidDateString } from '@validator/DateString' -import { IsPositiveDecimal } from '@validator/Decimal' import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' import { Decimal } from 'decimal.js-light' import { InputType, Field, Int } from 'type-graphql' +import { InputTransactionType } from '@enum/InputTransactionType' +import { isValidDateString } from '@validator/DateString' +import { IsPositiveDecimal } from '@validator/Decimal' + import { UserIdentifier } from './UserIdentifier' @InputType() diff --git a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts index b23d381cd..89bdbbedf 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts @@ -1,5 +1,3 @@ -import { Community } from '@entity/Community' - import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { TransactionDraft } from '@/graphql/input/TransactionDraft' From 07d2e6c846fccd94e696af5363e1d5b2846e8837 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 3 Feb 2024 18:54:42 +0100 Subject: [PATCH 12/13] rename paringTransaction to pairingTransaction --- dlt-connector/src/data/Transaction.logic.ts | 10 +- .../InboundTransactionRecipe.role.ts | 6 +- .../transmitToIota/TransmitToIota.context.ts | 8 +- .../src/logging/TransactionLogging.view.ts | 6 +- .../entity/0004-fix_spelling/Transaction.ts | 128 ++++++++++++++++++ dlt-database/entity/Transaction.ts | 2 +- dlt-database/migrations/0004-fix_spelling.ts | 15 ++ 7 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 dlt-database/entity/0004-fix_spelling/Transaction.ts create mode 100644 dlt-database/migrations/0004-fix_spelling.ts diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index f61c9a6e9..ec13bf753 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -26,13 +26,13 @@ export class TransactionLogic { throw new LogError("local transaction don't has a pairing transaction") } - // check if already on entity - if (this.self.paringTransaction) { - return this.self.paringTransaction + // check if already was loaded from db + if (this.self.pairingTransaction) { + return this.self.pairingTransaction } - if (this.self.paringTransactionId) { - const pairingTransaction = await Transaction.findOneBy({ id: this.self.paringTransactionId }) + if (this.self.pairingTransaction) { + const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction }) if (pairingTransaction) { return pairingTransaction } diff --git a/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts index 52e95c12c..2f18b48ac 100644 --- a/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts @@ -23,9 +23,9 @@ export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole ) } gradidoTransaction.parentMessageId = pairingTransaction.iotaMessageId - this.self.paringTransactionId = pairingTransaction.id - this.self.paringTransaction = pairingTransaction - pairingTransaction.paringTransactionId = this.self.id + this.self.pairingTransactionId = pairingTransaction.id + this.self.pairingTransaction = pairingTransaction + pairingTransaction.pairingTransactionId = this.self.id if (!this.self.otherCommunity) { throw new LogError('missing other community') diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts index f29d1742a..e58cd7e89 100644 --- a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts @@ -42,12 +42,12 @@ export class TransmitToIotaContext { logger.debug('transaction sended via iota', new TransactionLoggingView(transaction)) // store changes in db // prevent endless loop - const paringTransaction = transaction.paringTransaction - if (paringTransaction) { - transaction.paringTransaction = undefined + const pairingTransaction = transaction.pairingTransaction + if (pairingTransaction) { + transaction.pairingTransaction = undefined await getDataSource().transaction(async (transactionalEntityManager) => { await transactionalEntityManager.save(transaction) - await transactionalEntityManager.save(paringTransaction) + await transactionalEntityManager.save(pairingTransaction) }) } else { await transaction.save() diff --git a/dlt-connector/src/logging/TransactionLogging.view.ts b/dlt-connector/src/logging/TransactionLogging.view.ts index d04d46556..1bb59cc55 100644 --- a/dlt-connector/src/logging/TransactionLogging.view.ts +++ b/dlt-connector/src/logging/TransactionLogging.view.ts @@ -42,12 +42,12 @@ export class TransactionLoggingView extends AbstractLoggingView { ? new AccountLoggingView(this.self.recipientAccount) : { id: this.self.recipientAccountId }, pairingTransaction: - this.self.paringTransaction && deep === 1 - ? new TransactionLoggingView(this.self.paringTransaction).toJSON( + this.self.pairingTransaction && deep === 1 + ? new TransactionLoggingView(this.self.pairingTransaction).toJSON( showBackendTransactions, deep + 1, ) - : { id: this.self.paringTransactionId }, + : { id: this.self.pairingTransaction }, amount: this.decimalToString(this.self.amount), accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation), accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation), diff --git a/dlt-database/entity/0004-fix_spelling/Transaction.ts b/dlt-database/entity/0004-fix_spelling/Transaction.ts new file mode 100644 index 000000000..4d5a304da --- /dev/null +++ b/dlt-database/entity/0004-fix_spelling/Transaction.ts @@ -0,0 +1,128 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToOne, + JoinColumn, + BaseEntity, + OneToMany, +} from 'typeorm' +import { Decimal } from 'decimal.js-light' + +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Account } from '../Account' +import { Community } from '../Community' +import { BackendTransaction } from '../BackendTransaction' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' }) + id: number + + @Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true }) + iotaMessageId?: Buffer + + @OneToOne(() => Transaction, { cascade: ['update'] }) + // eslint-disable-next-line no-use-before-define + pairingTransaction?: Transaction + + @Column({ name: 'pairing_transaction_id', type: 'bigint', unsigned: true, nullable: true }) + pairingTransactionId?: number + + // if transaction has a sender than it is also the sender account + @ManyToOne(() => Account, (account) => account.transactionSigning) + @JoinColumn({ name: 'signing_account_id' }) + signingAccount?: Account + + @Column({ name: 'signing_account_id', type: 'int', unsigned: true, nullable: true }) + signingAccountId?: number + + @ManyToOne(() => Account, (account) => account.transactionRecipient) + @JoinColumn({ name: 'recipient_account_id' }) + recipientAccount?: Account + + @Column({ name: 'recipient_account_id', type: 'int', unsigned: true, nullable: true }) + recipientAccountId?: number + + @ManyToOne(() => Community, (community) => community.transactions, { + eager: true, + }) + @JoinColumn({ name: 'community_id' }) + community: Community + + @Column({ name: 'community_id', type: 'int', unsigned: true }) + communityId: number + + @ManyToOne(() => Community, (community) => community.friendCommunitiesTransactions) + @JoinColumn({ name: 'other_community_id' }) + otherCommunity?: Community + + @Column({ name: 'other_community_id', type: 'int', unsigned: true, nullable: true }) + otherCommunityId?: number + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + amount?: Decimal + + // account balance for sender based on creation date + @Column({ + name: 'account_balance_on_creation', + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + accountBalanceOnCreation?: Decimal + + @Column({ type: 'tinyint' }) + type: number + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + @Column({ name: 'body_bytes', type: 'blob' }) + bodyBytes: Buffer + + @Column({ type: 'binary', length: 64, unique: true }) + signature: Buffer + + @Column({ name: 'protocol_version', type: 'varchar', length: 255, default: '1' }) + protocolVersion: string + + @Column({ type: 'bigint', nullable: true }) + nr?: number + + @Column({ name: 'running_hash', type: 'binary', length: 48, nullable: true }) + runningHash?: Buffer + + // account balance for sender based on confirmation date (iota milestone) + @Column({ + name: 'account_balance_on_confirmation', + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + accountBalanceOnConfirmation?: Decimal + + @Column({ name: 'iota_milestone', type: 'bigint', nullable: true }) + iotaMilestone?: number + + // use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here + @Column({ name: 'confirmed_at', type: 'datetime', nullable: true }) + confirmedAt?: Date + + @OneToMany(() => BackendTransaction, (backendTransaction) => backendTransaction.transaction, { + cascade: ['insert', 'update'], + }) + @JoinColumn({ name: 'transaction_id' }) + backendTransactions: BackendTransaction[] +} diff --git a/dlt-database/entity/Transaction.ts b/dlt-database/entity/Transaction.ts index 113eb3450..9db8e6747 100644 --- a/dlt-database/entity/Transaction.ts +++ b/dlt-database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0003-refactor_transaction_recipe/Transaction' +export { Transaction } from './0004-fix_spelling/Transaction' diff --git a/dlt-database/migrations/0004-fix_spelling.ts b/dlt-database/migrations/0004-fix_spelling.ts new file mode 100644 index 000000000..3b2153a7d --- /dev/null +++ b/dlt-database/migrations/0004-fix_spelling.ts @@ -0,0 +1,15 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + ALTER TABLE \`transactions\` + RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`, + ; + `) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + ALTER TABLE \`transactions\` + RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`, + ; + `) +} From a60aa7eb4ed52f57629d4d205286a8c6530ccabc Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 13 Feb 2024 12:16:41 +0100 Subject: [PATCH 13/13] typo --- dlt-connector/src/data/Transaction.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index ec13bf753..c62f78f50 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -60,7 +60,7 @@ export class TransactionLogic { return nextTransaction } } - throw new LogError("couldn't find valid paring transaction", { + throw new LogError("couldn't find valid pairing transaction", { id: this.self.id, type: CrossGroupType[type], transactionCountWithSameCreatedAt: sameCreationDateTransactions.length,