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)