diff --git a/backend/.env.dist b/backend/.env.dist index b6216f8e9..9844d8c4a 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -21,6 +21,10 @@ KLICKTIPP_PASSWORD=secret321 KLICKTIPP_APIKEY_DE=SomeFakeKeyDE KLICKTIPP_APIKEY_EN=SomeFakeKeyEN +# DltConnector +DLT_CONNECTOR=true +DLT_CONNECTOR_URL=http://localhost:6010 + # Community COMMUNITY_NAME=Gradido Entwicklung COMMUNITY_URL=http://localhost/ diff --git a/backend/.env.template b/backend/.env.template index 6c32b728d..06bf81088 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -22,6 +22,10 @@ KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN +# DltConnector +DLT_CONNECTOR=$DLT_CONNECTOR +DLT_CONNECTOR_URL=$DLT_CONNECTOR_URL + # Community COMMUNITY_NAME=$COMMUNITY_NAME COMMUNITY_URL=$COMMUNITY_URL diff --git a/backend/src/apis/DltConnectorClient.test.ts b/backend/src/apis/DltConnectorClient.test.ts new file mode 100644 index 000000000..56fa3d13f --- /dev/null +++ b/backend/src/apis/DltConnectorClient.test.ts @@ -0,0 +1,174 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable security/detect-object-injection */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { Connection } from '@dbTools/typeorm' +import { Transaction as DbTransaction } from '@entity/Transaction' +import { Decimal } from 'decimal.js-light' + +import { cleanDB, testEnvironment } from '@test/helpers' + +import { CONFIG } from '@/config' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +import { DltConnectorClient } from './DltConnectorClient' + +let con: Connection + +let testEnv: { + con: Connection +} + +// Mock the GraphQLClient +jest.mock('graphql-request', () => { + const originalModule = jest.requireActual('graphql-request') + + let testCursor = 0 + + return { + __esModule: true, + ...originalModule, + GraphQLClient: jest.fn().mockImplementation((url: string) => { + if (url === 'invalid') { + throw new Error('invalid url') + } + return { + // why not using mockResolvedValueOnce or mockReturnValueOnce? + // I have tried, but it didn't work and return every time the first value + request: jest.fn().mockImplementation(() => { + testCursor++ + if (testCursor === 4) { + return Promise.resolve( + // invalid, is 33 Bytes long as binary + { + transmitTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A', + }, + }, + ) + } else if (testCursor === 5) { + throw Error('Connection error') + } else { + return Promise.resolve( + // valid, is 32 Bytes long as binary + { + transmitTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + }, + }, + ) + } + }), + } + }), + } +}) + +describe('undefined DltConnectorClient', () => { + it('invalid url', () => { + CONFIG.DLT_CONNECTOR_URL = 'invalid' + CONFIG.DLT_CONNECTOR = true + const result = DltConnectorClient.getInstance() + expect(result).toBeUndefined() + CONFIG.DLT_CONNECTOR_URL = 'http://dlt-connector:6010' + }) + + it('DLT_CONNECTOR is false', () => { + CONFIG.DLT_CONNECTOR = false + const result = DltConnectorClient.getInstance() + expect(result).toBeUndefined() + CONFIG.DLT_CONNECTOR = true + }) +}) + +/* +describe.skip('transmitTransaction, without db connection', () => { + const transaction = new DbTransaction() + transaction.typeId = 2 // Example transaction type ID + transaction.amount = new Decimal('10.00') // Example amount + transaction.balanceDate = new Date() // Example creation date + transaction.id = 1 // Example transaction ID + + it('cannot query for transaction id', async () => { + const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction) + expect(result).toBe(false) + }) +}) +*/ + +describe('transmitTransaction', () => { + beforeAll(async () => { + testEnv = await testEnvironment(logger) + con = testEnv.con + await cleanDB() + }) + + afterAll(async () => { + await cleanDB() + await con.close() + }) + + const transaction = new DbTransaction() + transaction.typeId = 2 // Example transaction type ID + transaction.amount = new Decimal('10.00') // Example amount + transaction.balanceDate = new Date() // Example creation date + transaction.id = 1 // Example transaction ID + + // data needed to let save succeed + transaction.memo = "I'm a dummy memo" + transaction.userId = 1 + transaction.userGradidoID = 'dummy gradido id' + + /* + it.skip('cannot find transaction in db', async () => { + const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction) + expect(result).toBe(false) + }) + */ + + it('invalid transaction type', async () => { + const localTransaction = new DbTransaction() + localTransaction.typeId = 12 + try { + await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction) + } catch (e) { + expect(e).toMatchObject( + new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()), + ) + } + }) + + /* + it.skip('should transmit the transaction and update the dltTransactionId in the database', async () => { + await transaction.save() + + const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction) + expect(result).toBe(true) + }) + + it.skip('invalid dltTransactionId (maximal 32 Bytes in Binary)', async () => { + await transaction.save() + + const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction) + expect(result).toBe(false) + }) + */ +}) + +/* +describe.skip('try transmitTransaction but graphql request failed', () => { + it('graphql request should throw', async () => { + const transaction = new DbTransaction() + transaction.typeId = 2 // Example transaction type ID + transaction.amount = new Decimal('10.00') // Example amount + transaction.balanceDate = new Date() // Example creation date + transaction.id = 1 // Example transaction ID + const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction) + expect(result).toBe(false) + }) +}) +*/ diff --git a/backend/src/apis/DltConnectorClient.ts b/backend/src/apis/DltConnectorClient.ts new file mode 100644 index 000000000..593072eef --- /dev/null +++ b/backend/src/apis/DltConnectorClient.ts @@ -0,0 +1,104 @@ +import { Transaction as DbTransaction } from '@entity/Transaction' +import { gql, GraphQLClient } from 'graphql-request' + +import { CONFIG } from '@/config' +import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +const sendTransaction = gql` + mutation ($input: TransactionInput!) { + sendTransaction(data: $input) { + dltTransactionIdHex + } + } +` + +// from ChatGPT +function getTransactionTypeString(id: TransactionTypeId): string { + const key = Object.keys(TransactionTypeId).find( + (key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id, + ) + if (key === undefined) { + throw new LogError('invalid transaction type id: ' + id.toString()) + } + return key +} + +// 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 DltConnectorClient { + // eslint-disable-next-line no-use-before-define + private static instance: DltConnectorClient + client: GraphQLClient + /** + * 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(): DltConnectorClient | undefined { + if (!CONFIG.DLT_CONNECTOR || !CONFIG.DLT_CONNECTOR_URL) { + logger.info(`dlt-connector are disabled via config...`) + return + } + if (!DltConnectorClient.instance) { + DltConnectorClient.instance = new DltConnectorClient() + } + if (!DltConnectorClient.instance.client) { + try { + DltConnectorClient.instance.client = new GraphQLClient(CONFIG.DLT_CONNECTOR_URL, { + method: 'GET', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } catch (e) { + logger.error("couldn't connect to dlt-connector: ", e) + return + } + } + return DltConnectorClient.instance + } + + /** + * transmit transaction via dlt-connector to iota + * and update dltTransactionId of transaction in db with iota message id + */ + public async transmitTransaction(transaction?: DbTransaction | null): Promise { + if (transaction) { + const typeString = getTransactionTypeString(transaction.typeId) + const secondsSinceEpoch = Math.round(transaction.balanceDate.getTime() / 1000) + const amountString = transaction.amount.toString() + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(sendTransaction, { + input: { + type: typeString, + amount: amountString, + createdAt: secondsSinceEpoch, + }, + }) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return data.sendTransaction.dltTransactionIdHex + } catch (e) { + throw new LogError('Error send sending transaction to dlt-connector: ', e) + } + } else { + throw new LogError('parameter transaction not set...') + } + } +} diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index f4a3795ba..99b25ce1f 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,14 +12,14 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0069-add_user_roles_table', + DB_VERSION: '0070-add_dlt_transactions_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v17.2023-07-03', + EXPECTED: 'v18.2023-07-10', CURRENT: '', }, } @@ -51,6 +51,11 @@ const klicktipp = { KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN', } +const dltConnector = { + DLT_CONNECTOR: process.env.DLT_CONNECTOR === 'true' || false, + DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? 'http://localhost:6010', +} + const community = { COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung', COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/', @@ -126,6 +131,7 @@ export const CONFIG = { ...server, ...database, ...klicktipp, + ...dltConnector, ...community, ...email, ...loginServer, diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index adf0f03e9..5c103f188 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -55,6 +55,7 @@ import { } from './util/creations' import { findContributions } from './util/findContributions' import { getLastTransaction } from './util/getLastTransaction' +import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' @Resolver() export class ContributionResolver { @@ -514,6 +515,10 @@ export class ContributionResolver { await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution) await queryRunner.commitTransaction() + + // trigger to send transaction via dlt-connector + void sendTransactionsToDltConnector() + logger.info('creation commited successfuly.') void sendContributionConfirmedEmail({ firstName: user.firstName, diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 282e6662a..fa91e4bdd 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -41,6 +41,7 @@ import { calculateBalance } from '@/util/validate' import { executeTransaction } from './TransactionResolver' import { getUserCreation, validateContribution } from './util/creations' import { getLastTransaction } from './util/getLastTransaction' +import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' import { transactionLinkList } from './util/transactionLinkList' // TODO: do not export, test it inside the resolver @@ -290,6 +291,7 @@ export class TransactionLinkResolver { await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution) await queryRunner.commitTransaction() + await EVENT_CONTRIBUTION_LINK_REDEEM( user, transaction, @@ -306,6 +308,8 @@ export class TransactionLinkResolver { } finally { releaseLock() } + // trigger to send transaction via dlt-connector + void sendTransactionsToDltConnector() return true } else { const now = new Date() diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 60445e239..380386795 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { Connection } from '@dbTools/typeorm' +import { Connection, In } from '@dbTools/typeorm' +import { DltTransaction } from '@entity/DltTransaction' import { Event as DbEvent } from '@entity/Event' import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' @@ -371,7 +372,6 @@ describe('send coins', () => { memo: 'unrepeatable memo', }, }) - await expect(DbEvent.find()).resolves.toContainEqual( expect.objectContaining({ type: EventType.TRANSACTION_RECEIVE, @@ -382,6 +382,52 @@ describe('send coins', () => { }), ) }) + + describe('sendTransactionsToDltConnector', () => { + let transaction: Transaction[] + let dltTransactions: DltTransaction[] + beforeAll(async () => { + // Find the previous created transactions of sendCoin mutation + transaction = await Transaction.find({ + where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + + // and read aslong as all async created dlt-transactions are finished + do { + dltTransactions = await DltTransaction.find({ + where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + // order: { createdAt: 'ASC', id: 'ASC' }, + }) + } while (transaction.length > dltTransactions.length) + }) + + it('has wait till sendTransactionsToDltConnector created all dlt-transactions', () => { + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: transaction[0].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transaction[1].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + }) + }) }) describe('send coins via gradido ID', () => { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 5f42fb36e..ba5d6e155 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -37,6 +37,7 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' +import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' import { transactionLinkSummary } from './util/transactionLinkSummary' export const executeTransaction = async ( @@ -150,6 +151,9 @@ export const executeTransaction = async ( transactionReceive, transactionReceive.amount, ) + + // trigger to send transaction via dlt-connector + void sendTransactionsToDltConnector() } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Transaction was not successful', e) diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts new file mode 100644 index 000000000..871c31a89 --- /dev/null +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts @@ -0,0 +1,778 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { Connection } from '@dbTools/typeorm' +import { DltTransaction } from '@entity/DltTransaction' +import { Transaction } from '@entity/Transaction' +import { ApolloServerTestClient } from 'apollo-server-testing' +import { Decimal } from 'decimal.js-light' +// import { GraphQLClient } from 'graphql-request' +// import { Response } from 'graphql-request/dist/types' +import { GraphQLClient } from 'graphql-request' +import { Response } from 'graphql-request/dist/types' + +import { testEnvironment, cleanDB } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' + +import { CONFIG } from '@/config' +import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' + +import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector' + +/* +// Mock the GraphQLClient +jest.mock('graphql-request', () => { + const originalModule = jest.requireActual('graphql-request') + + let testCursor = 0 + + return { + __esModule: true, + ...originalModule, + GraphQLClient: jest.fn().mockImplementation((url: string) => { + if (url === 'invalid') { + throw new Error('invalid url') + } + return { + // why not using mockResolvedValueOnce or mockReturnValueOnce? + // I have tried, but it didn't work and return every time the first value + request: jest.fn().mockImplementation(() => { + testCursor++ + if (testCursor === 4) { + return Promise.resolve( + // invalid, is 33 Bytes long as binary + { + transmitTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A', + }, + }, + ) + } else if (testCursor === 5) { + throw Error('Connection error') + } else { + return Promise.resolve( + // valid, is 32 Bytes long as binary + { + transmitTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + }, + }, + ) + } + }), + } + }), + } +}) +let mutate: ApolloServerTestClient['mutate'], + query: ApolloServerTestClient['query'], + con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} +*/ + +async function createTxCREATION1(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(1000) + tx.balance = new Decimal(100) + tx.balanceDate = new Date('01.01.2023 00:00:00') + tx.memo = 'txCREATION1' + tx.typeId = TransactionTypeId.CREATION + tx.userGradidoID = 'txCREATION1.userGradidoID' + tx.userId = 1 + tx.userName = 'txCREATION 1' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('01.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('01.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxCREATION2(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(1000) + tx.balance = new Decimal(200) + tx.balanceDate = new Date('02.01.2023 00:00:00') + tx.memo = 'txCREATION2' + tx.typeId = TransactionTypeId.CREATION + tx.userGradidoID = 'txCREATION2.userGradidoID' + tx.userId = 2 + tx.userName = 'txCREATION 2' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('02.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('02.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxCREATION3(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(1000) + tx.balance = new Decimal(300) + tx.balanceDate = new Date('03.01.2023 00:00:00') + tx.memo = 'txCREATION3' + tx.typeId = TransactionTypeId.CREATION + tx.userGradidoID = 'txCREATION3.userGradidoID' + tx.userId = 3 + tx.userName = 'txCREATION 3' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('03.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('03.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxSend1ToReceive2(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(100) + tx.balance = new Decimal(1000) + tx.balanceDate = new Date('11.01.2023 00:00:00') + tx.memo = 'txSEND1 to txRECEIVE2' + tx.typeId = TransactionTypeId.SEND + tx.userGradidoID = 'txSEND1.userGradidoID' + tx.userId = 1 + tx.userName = 'txSEND 1' + tx.linkedUserGradidoID = 'txRECEIVE2.linkedUserGradidoID' + tx.linkedUserId = 2 + tx.linkedUserName = 'txRECEIVE 2' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('11.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a1' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('11.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxReceive2FromSend1(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(100) + tx.balance = new Decimal(1300) + tx.balanceDate = new Date('11.01.2023 00:00:00') + tx.memo = 'txSEND1 to txRECEIVE2' + tx.typeId = TransactionTypeId.RECEIVE + tx.userGradidoID = 'txRECEIVE2.linkedUserGradidoID' + tx.userId = 2 + tx.userName = 'txRECEIVE 2' + tx.linkedUserGradidoID = 'txSEND1.userGradidoID' + tx.linkedUserId = 1 + tx.linkedUserName = 'txSEND 1' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('11.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b2' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('11.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +/* +async function createTxSend2ToReceive3(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(200) + tx.balance = new Decimal(1100) + tx.balanceDate = new Date('23.01.2023 00:00:00') + tx.memo = 'txSEND2 to txRECEIVE3' + tx.typeId = TransactionTypeId.SEND + tx.userGradidoID = 'txSEND2.userGradidoID' + tx.userId = 2 + tx.userName = 'txSEND 2' + tx.linkedUserGradidoID = 'txRECEIVE3.linkedUserGradidoID' + tx.linkedUserId = 3 + tx.linkedUserName = 'txRECEIVE 3' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('23.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a2' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('23.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxReceive3FromSend2(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(200) + tx.balance = new Decimal(1500) + tx.balanceDate = new Date('23.01.2023 00:00:00') + tx.memo = 'txSEND2 to txRECEIVE3' + tx.typeId = TransactionTypeId.RECEIVE + tx.userGradidoID = 'txRECEIVE3.linkedUserGradidoID' + tx.userId = 3 + tx.userName = 'txRECEIVE 3' + tx.linkedUserGradidoID = 'txSEND2.userGradidoID' + tx.linkedUserId = 2 + tx.linkedUserName = 'txSEND 2' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('23.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b3' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('23.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxSend3ToReceive1(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(300) + tx.balance = new Decimal(1200) + tx.balanceDate = new Date('31.01.2023 00:00:00') + tx.memo = 'txSEND3 to txRECEIVE1' + tx.typeId = TransactionTypeId.SEND + tx.userGradidoID = 'txSEND3.userGradidoID' + tx.userId = 3 + tx.userName = 'txSEND 3' + tx.linkedUserGradidoID = 'txRECEIVE1.linkedUserGradidoID' + tx.linkedUserId = 1 + tx.linkedUserName = 'txRECEIVE 1' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('31.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a3' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('31.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} + +async function createTxReceive1FromSend3(verified: boolean): Promise { + let tx = Transaction.create() + tx.amount = new Decimal(300) + tx.balance = new Decimal(1300) + tx.balanceDate = new Date('31.01.2023 00:00:00') + tx.memo = 'txSEND3 to txRECEIVE1' + tx.typeId = TransactionTypeId.RECEIVE + tx.userGradidoID = 'txRECEIVE1.linkedUserGradidoID' + tx.userId = 1 + tx.userName = 'txRECEIVE 1' + tx.linkedUserGradidoID = 'txSEND3.userGradidoID' + tx.linkedUserId = 3 + tx.linkedUserName = 'txSEND 3' + tx = await Transaction.save(tx) + + if (verified) { + const dlttx = DltTransaction.create() + dlttx.createdAt = new Date('31.01.2023 00:00:10') + dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b1' + dlttx.transactionId = tx.id + dlttx.verified = true + dlttx.verifiedAt = new Date('31.01.2023 00:01:10') + await DltTransaction.save(dlttx) + } + return tx +} +*/ + +let con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('create and send Transactions to DltConnector', () => { + let txCREATION1: Transaction + let txCREATION2: Transaction + let txCREATION3: Transaction + let txSEND1to2: Transaction + let txRECEIVE2From1: Transaction + // let txSEND2To3: Transaction + // let txRECEIVE3From2: Transaction + // let txSEND3To1: Transaction + // let txRECEIVE1From3: Transaction + + beforeEach(() => { + jest.clearAllMocks() + }) + + afterEach(async () => { + await cleanDB() + }) + + describe('with 3 creations but inactive dlt-connector', () => { + it('found 3 dlt-transactions', async () => { + txCREATION1 = await createTxCREATION1(false) + txCREATION2 = await createTxCREATION2(false) + txCREATION3 = await createTxCREATION3(false) + + CONFIG.DLT_CONNECTOR = false + await sendTransactionsToDltConnector() + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + // Find the previous created transactions of sendCoin mutation + const transactions = await Transaction.find({ + // where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + + const dltTransactions = await DltTransaction.find({ + // where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[0].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[1].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[2].id, + messageId: null, + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + + expect(logger.info).nthCalledWith(3, 'sending to DltConnector currently not configured...') + }) + }) + + describe('with 3 creations and active dlt-connector', () => { + it('found 3 dlt-transactions', async () => { + txCREATION1 = await createTxCREATION1(false) + txCREATION2 = await createTxCREATION2(false) + txCREATION3 = await createTxCREATION3(false) + + CONFIG.DLT_CONNECTOR = true + + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + sendTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + }, + }, + } as Response + }) + + await sendTransactionsToDltConnector() + + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + // Find the previous created transactions of sendCoin mutation + const transactions = await Transaction.find({ + // where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + + const dltTransactions = await DltTransaction.find({ + // where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[0].id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[1].id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: transactions[2].id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + }) + }) + + describe('with 3 verified creations, 1 sendCoins and active dlt-connector', () => { + it('found 3 dlt-transactions', async () => { + txCREATION1 = await createTxCREATION1(true) + txCREATION2 = await createTxCREATION2(true) + txCREATION3 = await createTxCREATION3(true) + + txSEND1to2 = await createTxSend1ToReceive2(false) + txRECEIVE2From1 = await createTxReceive2FromSend1(false) + + /* + txSEND2To3 = await createTxSend2ToReceive3() + txRECEIVE3From2 = await createTxReceive3FromSend2() + txSEND3To1 = await createTxSend3ToReceive1() + txRECEIVE1From3 = await createTxReceive1FromSend3() + */ + + CONFIG.DLT_CONNECTOR = true + + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + sendTransaction: { + dltTransactionIdHex: + '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + }, + }, + } as Response + }) + + await sendTransactionsToDltConnector() + + expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...') + + // Find the previous created transactions of sendCoin mutation + /* + const transactions = await Transaction.find({ + // where: { memo: 'unrepeatable memo' }, + order: { balanceDate: 'ASC', id: 'ASC' }, + }) + */ + + const dltTransactions = await DltTransaction.find({ + // where: { transactionId: In([transaction[0].id, transaction[1].id]) }, + // relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + + expect(dltTransactions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + transactionId: txCREATION1.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1', + verified: true, + createdAt: new Date('01.01.2023 00:00:10'), + verifiedAt: new Date('01.01.2023 00:01:10'), + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txCREATION2.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2', + verified: true, + createdAt: new Date('02.01.2023 00:00:10'), + verifiedAt: new Date('02.01.2023 00:01:10'), + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txCREATION3.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3', + verified: true, + createdAt: new Date('03.01.2023 00:00:10'), + verifiedAt: new Date('03.01.2023 00:01:10'), + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txSEND1to2.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + transactionId: txRECEIVE2From1.id, + messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + verified: false, + createdAt: expect.any(Date), + verifiedAt: null, + }), + ]), + ) + }) + /* + describe('with one Community of api 1_0 and not matching pubKey', () => { + beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + getPublicKey: { + publicKey: 'somePubKey', + }, + }, + } as Response + }) + const variables1 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '1_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables1) + .orUpdate({ + // eslint-disable-next-line camelcase + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + + jest.clearAllMocks() + // await validateCommunities() + }) + + it('logs one community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs not matching publicKeys', () => { + expect(logger.warn).toBeCalledWith( + 'Federation: received not matching publicKey:', + 'somePubKey', + expect.stringMatching('11111111111111111111111111111111'), + ) + }) + }) + describe('with one Community of api 1_0 and matching pubKey', () => { + beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + getPublicKey: { + publicKey: '11111111111111111111111111111111', + }, + }, + } as Response + }) + const variables1 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '1_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables1) + .orUpdate({ + // eslint-disable-next-line camelcase + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + // await validateCommunities() + }) + + it('logs one community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs community pubKey verified', () => { + expect(logger.info).toHaveBeenNthCalledWith( + 3, + 'Federation: verified community with', + 'http//localhost:5001/api/', + ) + }) + }) + describe('with two Communities of api 1_0 and 1_1', () => { + beforeEach(async () => { + jest.clearAllMocks() + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + getPublicKey: { + publicKey: '11111111111111111111111111111111', + }, + }, + } as Response + }) + const variables2 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '1_1', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables2) + .orUpdate({ + // eslint-disable-next-line camelcase + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + // await validateCommunities() + }) + it('logs two communities found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs requestGetPublicKey for community api 1_1 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_1/', + ) + }) + }) + describe('with three Communities of api 1_0, 1_1 and 2_0', () => { + let dbCom: DbFederatedCommunity + beforeEach(async () => { + const variables3 = { + publicKey: Buffer.from('11111111111111111111111111111111'), + apiVersion: '2_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables3) + .orUpdate({ + // eslint-disable-next-line camelcase + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + dbCom = await DbFederatedCommunity.findOneOrFail({ + where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, + }) + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + // await validateCommunities() + }) + it('logs three community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs requestGetPublicKey for community api 1_1 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_1/', + ) + }) + it('logs unsupported api for community with api 2_0 ', () => { + expect(logger.warn).toBeCalledWith( + 'Federation: dbCom with unsupported apiVersion', + dbCom.endPoint, + '2_0', + ) + }) + }) + */ + }) +}) diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts new file mode 100644 index 000000000..98ea255c1 --- /dev/null +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts @@ -0,0 +1,80 @@ +import { IsNull } from '@dbTools/typeorm' +import { DltTransaction } from '@entity/DltTransaction' +import { Transaction } from '@entity/Transaction' + +import { DltConnectorClient } from '@/apis/DltConnectorClient' +import { backendLogger as logger } from '@/server/logger' +import { Monitor, MonitorNames } from '@/util/Monitor' + +export async function sendTransactionsToDltConnector(): Promise { + logger.info('sendTransactionsToDltConnector...') + // check if this logic is still occupied, no concurrecy allowed + if (!Monitor.isLocked(MonitorNames.SEND_DLT_TRANSACTIONS)) { + // mark this block for occuption to prevent concurrency + Monitor.lockIt(MonitorNames.SEND_DLT_TRANSACTIONS) + + try { + await createDltTransactions() + const dltConnector = DltConnectorClient.getInstance() + if (dltConnector) { + logger.debug('with sending to DltConnector...') + const dltTransactions = await DltTransaction.find({ + where: { messageId: IsNull() }, + relations: ['transaction'], + order: { createdAt: 'ASC', id: 'ASC' }, + }) + for (const dltTx of dltTransactions) { + try { + const messageId = await dltConnector.transmitTransaction(dltTx.transaction) + const dltMessageId = Buffer.from(messageId, 'hex') + if (dltMessageId.length !== 32) { + logger.error( + 'Error dlt message id is invalid: %s, should by 32 Bytes long in binary after converting from hex', + dltMessageId, + ) + return + } + dltTx.messageId = dltMessageId.toString('hex') + await DltTransaction.save(dltTx) + logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id) + } catch (e) { + logger.error( + `error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`, + e, + ) + } + } + } else { + logger.info('sending to DltConnector currently not configured...') + } + } catch (e) { + logger.error('error on sending transactions to dlt-connector.', e) + } finally { + // releae Monitor occupation + Monitor.releaseIt(MonitorNames.SEND_DLT_TRANSACTIONS) + } + } else { + logger.info('sendTransactionsToDltConnector currently locked by monitor...') + } +} + +async function createDltTransactions(): Promise { + const dltqb = DltTransaction.createQueryBuilder().select('transactions_id') + const newTransactions: Transaction[] = await Transaction.createQueryBuilder() + .select('id') + .addSelect('balance_date') + .where('id NOT IN (' + dltqb.getSql() + ')') + // eslint-disable-next-line camelcase + .orderBy({ balance_date: 'ASC', id: 'ASC' }) + .getRawMany() + + const dltTxArray: DltTransaction[] = [] + let idx = 0 + while (newTransactions.length > dltTxArray.length) { + // timing problems with for(let idx = 0; idx < newTransactions.length; idx++) { + const dltTx = DltTransaction.create() + dltTx.transactionId = newTransactions[idx++].id + await DltTransaction.save(dltTx) + dltTxArray.push(dltTx) + } +} diff --git a/backend/src/util/Monitor.ts b/backend/src/util/Monitor.ts new file mode 100644 index 000000000..3489eff4d --- /dev/null +++ b/backend/src/util/Monitor.ts @@ -0,0 +1,50 @@ +import { registerEnumType } from 'type-graphql' + +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +export enum MonitorNames { + SEND_DLT_TRANSACTIONS = 1, +} + +registerEnumType(MonitorNames, { + name: 'MonitorNames', // this one is mandatory + description: 'Name of Monitor-keys', // this one is optional +}) + +/* @typescript-eslint/no-extraneous-class */ +export class Monitor { + private static locks = new Map() + + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + private _dummy = `to avoid unexpected class with only static properties` + public get dummy() { + return this._dummy + } + + public static isLocked(key: MonitorNames): boolean | undefined { + if (this.locks.has(key)) { + logger.debug(`Monitor isLocked key=${key} = `, this.locks.get(key)) + return this.locks.get(key) + } + logger.debug(`Monitor isLocked key=${key} not exists`) + return false + } + + public static lockIt(key: MonitorNames): void { + logger.debug(`Monitor lockIt key=`, key) + if (this.locks.has(key)) { + throw new LogError('still existing Monitor with key=', key) + } + this.locks.set(key, true) + } + + public static releaseIt(key: MonitorNames): void { + logger.debug(`Monitor releaseIt key=`, key) + if (this.locks.has(key)) { + this.locks.delete(key) + } + } +} diff --git a/database/entity/0070-add_dlt_transactions_table/DltTransaction.ts b/database/entity/0070-add_dlt_transactions_table/DltTransaction.ts new file mode 100644 index 000000000..2251a6a42 --- /dev/null +++ b/database/entity/0070-add_dlt_transactions_table/DltTransaction.ts @@ -0,0 +1,33 @@ +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' +import { Transaction } from '../Transaction' + +@Entity('dlt_transactions', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class DltTransaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'transactions_id', type: 'int', unsigned: true, nullable: false }) + transactionId: number + + @Column({ + name: 'message_id', + length: 64, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + messageId: string + + @Column({ name: 'verified', type: 'bool', nullable: false, default: false }) + verified: boolean + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false }) + createdAt: Date + + @Column({ name: 'verified_at', nullable: true, default: null, type: 'datetime' }) + verifiedAt: Date | null + + @OneToOne(() => Transaction, (transaction) => transaction.dltTransaction) + @JoinColumn({ name: 'transactions_id' }) + transaction?: Transaction | null +} diff --git a/database/entity/0070-add_dlt_transactions_table/Transaction.ts b/database/entity/0070-add_dlt_transactions_table/Transaction.ts new file mode 100644 index 000000000..0684bc3db --- /dev/null +++ b/database/entity/0070-add_dlt_transactions_table/Transaction.ts @@ -0,0 +1,145 @@ +/* eslint-disable no-use-before-define */ +import { Decimal } from 'decimal.js-light' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Contribution } from '../Contribution' +import { DltTransaction } from './DltTransaction' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null }) + previous: number | null + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + @Column({ + name: 'transaction_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + transactionLinkId?: number | null + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + balance: Decimal + + @Column({ + name: 'balance_date', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + decay: Decimal + + @Column({ + name: 'decay_start', + type: 'datetime', + nullable: true, + default: null, + }) + decayStart: Date | null + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null }) + creationDate: Date | null + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ + name: 'user_gradido_id', + type: 'varchar', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + userGradidoID: string + + @Column({ + name: 'user_name', + type: 'varchar', + length: 512, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + userName: string | null + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_user_gradido_id', + type: 'varchar', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserGradidoID: string | null + + @Column({ + name: 'linked_user_name', + type: 'varchar', + length: 512, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserName: string | null + + @Column({ + name: 'linked_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedTransactionId?: number | null + + @OneToOne(() => Contribution, (contribution) => contribution.transaction) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + contribution?: Contribution | null + + @OneToOne(() => DltTransaction, (dlt) => dlt.transactionId) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + dltTransaction?: DltTransaction | null + + @OneToOne(() => Transaction) + @JoinColumn({ name: 'previous' }) + previousTransaction?: Transaction | null +} diff --git a/database/entity/DltTransaction.ts b/database/entity/DltTransaction.ts new file mode 100644 index 000000000..d9c03306c --- /dev/null +++ b/database/entity/DltTransaction.ts @@ -0,0 +1 @@ +export { DltTransaction } from './0070-add_dlt_transactions_table/DltTransaction' diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index 4000e3c85..d08c84667 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction' +export { Transaction } from './0070-add_dlt_transactions_table/Transaction' diff --git a/database/entity/index.ts b/database/entity/index.ts index b27ac4d61..a5c37efa9 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -12,12 +12,14 @@ import { ContributionMessage } from './ContributionMessage' import { Community } from './Community' import { FederatedCommunity } from './FederatedCommunity' import { UserRole } from './UserRole' +import { DltTransaction } from './DltTransaction' export const entities = [ Community, Contribution, ContributionLink, ContributionMessage, + DltTransaction, Event, FederatedCommunity, LoginElopageBuys, diff --git a/database/migrations/0070-add_dlt_transactions_table.ts b/database/migrations/0070-add_dlt_transactions_table.ts new file mode 100644 index 000000000..4249edf0f --- /dev/null +++ b/database/migrations/0070-add_dlt_transactions_table.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE dlt_transactions ( + id int unsigned NOT NULL AUTO_INCREMENT, + transactions_id int(10) unsigned NOT NULL, + message_id varchar(64) NULL DEFAULT NULL, + verified tinyint(4) NOT NULL DEFAULT 0, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + verified_at datetime(3), + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`DROP TABLE dlt_transactions;`) +} diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 03048b624..2b06094f6 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0069-add_user_roles_table', + DB_VERSION: '0070-add_dlt_transactions_table', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 72da74aaa..5402d6d96 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -11,7 +11,7 @@ Decimal.set({ */ const constants = { - DB_VERSION: '0069-add_user_roles_table', + DB_VERSION: '0070-add_dlt_transactions_table', // DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info