diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 3b910b313..ac9334977 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -2,7 +2,6 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, getHomeCommunity, - getReachableCommunities, } from 'database' import { IsNull } from 'typeorm' @@ -16,9 +15,6 @@ import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { ApiVersionType } from 'core' -import { CONFIG } from '@/config' -import * as path from 'node:path' -import * as fs from 'node:fs' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`) @@ -87,8 +83,6 @@ export async function validateCommunities(): Promise { logger.error(`Error:`, err) } } - // export communities for gradido dlt node server - await exportCommunitiesToDltNodeServer() } export async function writeJwtKeyPairInHomeCommunity(): Promise { @@ -148,48 +142,3 @@ async function writeForeignCommunity( await DbCommunity.save(com) } } - -// prototype, later add api call to gradido dlt node server for adding/updating communities -type CommunityForDltNodeServer = { - communityId: string - hieroTopicId: string - alias: string - folder: string -} -async function exportCommunitiesToDltNodeServer(): Promise { - if (!CONFIG.DLT_CONNECTOR) { - return Promise.resolve() - } - const folder = CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER - try { - fs.accessSync(folder, fs.constants.R_OK | fs.constants.W_OK) - } catch (err) { - logger.error(`Error: home folder for DLT Gradido Node Server ${folder} does not exist`) - return - } - - const dbComs = await getReachableCommunities(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER * 4) - const communitiesForDltNodeServer: CommunityForDltNodeServer[] = [] - // make sure communityName is unique - const communityName = new Set() - dbComs.forEach((com) => { - if (!com.communityUuid || !com.hieroTopicId) { - return - } - let alias = com.name - if (!alias || communityName.has(alias)) { - alias = com.communityUuid - } - communityName.add(alias) - communitiesForDltNodeServer.push({ - communityId: com.communityUuid, - hieroTopicId: com.hieroTopicId, - alias, - // use only alpha-numeric chars for folder name - folder: alias.replace(/[^a-zA-Z0-9]/g, '_') - }) - }) - const dltNodeServerCommunitiesFile = path.join(folder, 'communities.json') - fs.writeFileSync(dltNodeServerCommunitiesFile, JSON.stringify(communitiesForDltNodeServer, null, 2)) - logger.debug(`Written communitiesForDltNodeServer to ${dltNodeServerCommunitiesFile}`) -} diff --git a/dlt-connector/.gitignore b/dlt-connector/.gitignore index 5435dd5ff..4c6422640 100644 --- a/dlt-connector/.gitignore +++ b/dlt-connector/.gitignore @@ -7,3 +7,4 @@ package-json.lock coverage # emacs *~ +gradido_node \ No newline at end of file diff --git a/dlt-connector/bun.lock b/dlt-connector/bun.lock index 7ea3a4eab..46e566b54 100644 --- a/dlt-connector/bun.lock +++ b/dlt-connector/bun.lock @@ -11,8 +11,11 @@ "@hashgraph/sdk": "^2.70.0", "@sinclair/typebox": "^0.34.33", "@sinclair/typemap": "^0.10.1", + "@types/adm-zip": "^0.5.7", "@types/bun": "^1.2.17", "@types/uuid": "^8.3.4", + "adm-zip": "^0.5.16", + "async-mutex": "^0.5.0", "dotenv": "^10.0.0", "elysia": "1.3.8", "graphql-request": "^7.2.0", @@ -248,6 +251,8 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/adm-zip": ["@types/adm-zip@0.5.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -284,6 +289,8 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], @@ -306,6 +313,8 @@ "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], + "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 7dc4e590d..e1bef07c9 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -24,8 +24,11 @@ "@hashgraph/sdk": "^2.70.0", "@sinclair/typebox": "^0.34.33", "@sinclair/typemap": "^0.10.1", + "@types/adm-zip": "^0.5.7", "@types/bun": "^1.2.17", "@types/uuid": "^8.3.4", + "adm-zip": "^0.5.16", + "async-mutex": "^0.5.0", "dotenv": "^10.0.0", "elysia": "1.3.8", "graphql-request": "^7.2.0", diff --git a/dlt-connector/src/bootstrap/init.ts b/dlt-connector/src/bootstrap/init.ts index c41553594..13b77783c 100644 --- a/dlt-connector/src/bootstrap/init.ts +++ b/dlt-connector/src/bootstrap/init.ts @@ -8,6 +8,7 @@ import { SendToHieroContext } from '../interactions/sendToHiero/SendToHiero.cont import { Community, communitySchema } from '../schemas/transaction.schema' import { isPortOpenRetry } from '../utils/network' import { type AppContext, type AppContextClients } from './appContext' +import { initGradidoNode } from './initGradidoNode' export function loadConfig(): Logger { // configure log4js @@ -74,6 +75,9 @@ export async function checkGradidoNode( logger: Logger, homeCommunity: Community, ): Promise { + // check if gradido node is running, if not setup and start it + await initGradidoNode(clients) + // ask gradido node if community blockchain was created try { if ( diff --git a/dlt-connector/src/bootstrap/initGradidoNode.ts b/dlt-connector/src/bootstrap/initGradidoNode.ts new file mode 100644 index 000000000..7c652a667 --- /dev/null +++ b/dlt-connector/src/bootstrap/initGradidoNode.ts @@ -0,0 +1,89 @@ +import { execSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import AdmZip from 'adm-zip' +import { getLogger } from 'log4js' +import { exportCommunities } from '../client/GradidoNode/communities' +import { GradidoNodeProcess } from '../client/GradidoNode/GradidoNodeProcess' +import { HieroClient } from '../client/hiero/HieroClient' +import { CONFIG } from '../config' +import { + GRADIDO_NODE_HOME_FOLDER_NAME, + GRADIDO_NODE_RUNTIME_PATH, + LOG4JS_BASE_CATEGORY, +} from '../config/const' +import { checkFileExist, checkPathExist } from '../utils/filesystem' +import { isPortOpen } from '../utils/network' +import { AppContextClients } from './appContext' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.bootstrap.initGradidoNode`) + +export async function initGradidoNode(clients: AppContextClients): Promise { + const url = `http://localhost:${CONFIG.DLT_NODE_SERVER_PORT}` + const isOpen = await isPortOpen(url) + if (isOpen) { + logger.info(`GradidoNode is already running on ${url}`) + return + } + + const gradidoNodeHomeFolder = path.join( + CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, + GRADIDO_NODE_HOME_FOLDER_NAME, + ) + // check folder, create when missing + checkPathExist(gradidoNodeHomeFolder, true) + + await Promise.all([ + // write Hedera Address Book + exportHederaAddressbooks(gradidoNodeHomeFolder, clients.hiero), + // check GradidoNode Runtime, download when missing + ensureGradidoNodeRuntimeAvailable(GRADIDO_NODE_RUNTIME_PATH), + // export communities to GradidoNode Folder + exportCommunities(gradidoNodeHomeFolder, clients.backend), + ]) + GradidoNodeProcess.getInstance().start() +} + +async function exportHederaAddressbooks( + homeFolder: string, + hieroClient: HieroClient, +): Promise { + const networkName = CONFIG.HIERO_HEDERA_NETWORK + const addressBook = await hieroClient.downloadAddressBook() + const addressBookPath = path.join(homeFolder, 'addressbook', `${networkName}.pb`) + checkPathExist(path.dirname(addressBookPath), true) + fs.writeFileSync(addressBookPath, addressBook.toBytes()) +} + +async function ensureGradidoNodeRuntimeAvailable(runtimeFileName: string): Promise { + const runtimeFolder = path.dirname(runtimeFileName) + checkPathExist(runtimeFolder, true) + if (!checkFileExist(runtimeFileName)) { + const runtimeArchiveFilename = createGradidoNodeRuntimeArchiveFilename() + const downloadUrl = new URL( + `https://github.com/gradido/gradido_node/releases/download/v${CONFIG.DLT_GRADIDO_NODE_SERVER_VERSION}/${runtimeArchiveFilename}`, + ) + logger.debug(`download GradidoNode Runtime from ${downloadUrl}`) + const archive = await fetch(downloadUrl) + if (!archive.ok) { + throw new Error(`Failed to download GradidoNode Runtime: ${archive.statusText}`) + } + const compressedBuffer = await archive.arrayBuffer() + if (process.platform === 'win32') { + const zip = new AdmZip(Buffer.from(compressedBuffer)) + zip.extractAllTo(runtimeFolder, true) + } else { + const archivePath = path.join(runtimeFolder, runtimeArchiveFilename) + logger.debug(`GradidoNode Runtime Archive: ${archivePath}`) + fs.writeFileSync(archivePath, Buffer.from(compressedBuffer)) + execSync(`tar -xzf ${archivePath}`, { cwd: runtimeFolder }) + } + } +} + +function createGradidoNodeRuntimeArchiveFilename(): string { + const version = CONFIG.DLT_GRADIDO_NODE_SERVER_VERSION + const platform: string = process.platform + const fileEnding = platform === 'win32' ? 'zip' : 'tar.gz' + return `gradido_node-v${version}-${platform}-${process.arch}.${fileEnding}` +} diff --git a/dlt-connector/src/bootstrap/shutdown.ts b/dlt-connector/src/bootstrap/shutdown.ts index 781430008..415742d38 100644 --- a/dlt-connector/src/bootstrap/shutdown.ts +++ b/dlt-connector/src/bootstrap/shutdown.ts @@ -1,4 +1,5 @@ import { Logger } from 'log4js' +import { GradidoNodeProcess } from '../client/GradidoNode/GradidoNodeProcess' import { type AppContextClients } from './appContext' export function setupGracefulShutdown(logger: Logger, clients: AppContextClients) { @@ -25,4 +26,5 @@ export function setupGracefulShutdown(logger: Logger, clients: AppContextClients async function gracefulShutdown(logger: Logger, clients: AppContextClients) { logger.info('graceful shutdown') await clients.hiero.waitForPendingPromises() + await GradidoNodeProcess.getInstance().exit() } diff --git a/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts b/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts index 3bd3bab51..d66a58887 100644 --- a/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts @@ -40,7 +40,7 @@ export class GradidoNodeClient { private constructor() { this.logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNodeClient`) - this.urlValue = `http://localhost:${CONFIG.DLT_NODE_SERVER_PORT}` + this.urlValue = `http://localhost:${CONFIG.DLT_NODE_SERVER_PORT}/api` this.logger.addContext('url', this.urlValue) this.client = new JsonRpcClient({ url: this.urlValue, diff --git a/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts new file mode 100644 index 000000000..eb2bf6b66 --- /dev/null +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts @@ -0,0 +1,121 @@ +import { Subprocess, spawn } from 'bun' +import { getLogger, Logger } from 'log4js' +import { CONFIG } from '../../config' +import { + GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS, + GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS, + GRADIDO_NODE_RUNTIME_PATH, + LOG4JS_BASE_CATEGORY, +} from '../../config/const' + +/** + * A Singleton class defines the `getInstance` method that lets clients access + * the unique singleton instance. + * + * Singleton Managing GradidoNode if started as subprocess + * will restart GradidoNode if it exits more than `GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS` milliseconds after start + * if exit was called, it will first try to exit graceful with SIGTERM and then kill with SIGKILL after `GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS` milliseconds + */ +export class GradidoNodeProcess { + private static instance: GradidoNodeProcess | null = null + private proc: Subprocess | null = null + private logger: Logger + private lastStarted: Date | null = null + private exitCalled: boolean = false + + private constructor() { + // constructor is private to prevent instantiation from outside + // of the class except from the static getInstance method. + this.logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNodeProcess`) + } + + /** + * Static method that returns the singleton instance of the class. + * @returns the singleton instance of the class. + */ + public static getInstance(): GradidoNodeProcess { + if (!GradidoNodeProcess.instance) { + GradidoNodeProcess.instance = new GradidoNodeProcess() + } + return GradidoNodeProcess.instance + } + + public start() { + if (this.proc) { + this.logger.warn('GradidoNodeProcess already running.') + return + } + this.logger.info(`starting GradidoNodeProcess with path: ${GRADIDO_NODE_RUNTIME_PATH}`) + this.lastStarted = new Date() + const logger = this.logger + this.proc = spawn([GRADIDO_NODE_RUNTIME_PATH], { + env: { + CLIENTS_HIERO_NETWORKTYPE: CONFIG.HIERO_HEDERA_NETWORK, + SERVER_JSON_RPC_PORT: CONFIG.DLT_NODE_SERVER_PORT.toString(), + USERPROFILE: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, + HOME: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, + }, + onExit(proc, exitCode, signalCode, error) { + logger.warn(`GradidoNodeProcess exited with code ${exitCode} and signalCode ${signalCode}`) + if (error) { + logger.error(`GradidoNodeProcess exit error: ${error}`) + /*if (logger.isDebugEnabled() && proc.stderr) { + // print error messages from GradidoNode in our own log if debug is enabled + proc.stderr + .getReader() + .read() + .then((chunk) => { + logger.debug(chunk.value?.toString()) + }) + }*/ + } + logger.debug(`ressource usage: ${JSON.stringify(proc?.resourceUsage(), null, 2)}`) + const gradidoNodeProcess = GradidoNodeProcess.getInstance() + gradidoNodeProcess.proc = null + if ( + !gradidoNodeProcess.exitCalled && + gradidoNodeProcess.lastStarted && + Date.now() - gradidoNodeProcess.lastStarted.getTime() > + GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS + ) { + // restart only if enough time was passed since last start to prevent restart loop + gradidoNodeProcess.start() + } + }, + /*stdout: 'ignore', + stderr: logger.isDebugEnabled() ? 'pipe' : 'ignore',*/ + stdout: 'inherit', + stderr: 'inherit', + }) + } + + public async restart() { + if (this.proc) { + await this.exit() + this.exitCalled = false + this.start() + } + } + + public async exit(): Promise { + this.exitCalled = true + if (this.proc) { + this.proc.kill('SIGTERM') + const timeout = setTimeout(() => { + this.logger.warn( + `GradidoNode couldn't exit graceful after ${GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS} milliseconds with SIGTERM, killing with SIGKILL`, + ) + this.proc?.kill('SIGKILL') + }, GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS) + try { + await this.proc.exited + } catch (error) { + this.logger.error(`GradidoNodeProcess exit error: ${error}`) + } finally { + clearTimeout(timeout) + } + } else { + return Promise.resolve() + } + } +} diff --git a/dlt-connector/src/client/GradidoNode/communities.ts b/dlt-connector/src/client/GradidoNode/communities.ts new file mode 100644 index 000000000..d25ed4dd4 --- /dev/null +++ b/dlt-connector/src/client/GradidoNode/communities.ts @@ -0,0 +1,84 @@ +import fs from 'node:fs' +import path from 'node:path' +import { Mutex } from 'async-mutex' +import { getLogger } from 'log4js' +import { CONFIG } from '../../config' +import { GRADIDO_NODE_HOME_FOLDER_NAME, LOG4JS_BASE_CATEGORY } from '../../config/const' +import { HieroId } from '../../schemas/typeGuard.schema' +import { checkFileExist, checkPathExist } from '../../utils/filesystem' +import { BackendClient } from '../backend/BackendClient' +import { GradidoNodeProcess } from './GradidoNodeProcess' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNode.communities`) +const ensureCommunitiesAvailableMutex: Mutex = new Mutex() + +// prototype, later add api call to gradido dlt node server for adding/updating communities +type CommunityForDltNodeServer = { + communityId: string + hieroTopicId: string + alias: string + folder: string +} + +export async function ensureCommunitiesAvailable(communityTopicIds: HieroId[]): Promise { + const release = await ensureCommunitiesAvailableMutex.acquire() + try { + const homeFolder = path.join( + CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, + GRADIDO_NODE_HOME_FOLDER_NAME, + ) + if (!checkCommunityAvailable(communityTopicIds, homeFolder)) { + await exportCommunities(homeFolder, BackendClient.getInstance()) + return GradidoNodeProcess.getInstance().restart() + } + } finally { + release() + } +} + +export async function exportCommunities(homeFolder: string, client: BackendClient): Promise { + const communities = await client.getReachableCommunities() + const communitiesPath = path.join(homeFolder, 'communities.json') + checkPathExist(path.dirname(communitiesPath), true) + // make sure communityName is unique + const communityName = new Set() + const communitiesForDltNodeServer: CommunityForDltNodeServer[] = [] + for (const com of communities) { + if (!com.uuid || !com.hieroTopicId) { + continue + } + // use name as alias if not empty and unique, otherwise use uuid + let alias = com.name + if (!alias || communityName.has(alias)) { + alias = com.uuid + } + communityName.add(alias) + communitiesForDltNodeServer.push({ + communityId: com.uuid, + hieroTopicId: com.hieroTopicId, + alias, + // use only alpha-numeric chars for folder name + folder: alias.replace(/[^a-zA-Z0-9]/g, '_'), + }) + } + fs.writeFileSync(communitiesPath, JSON.stringify(communitiesForDltNodeServer, null, 2)) + logger.info(`exported ${communitiesForDltNodeServer.length} communities to ${communitiesPath}`) +} + +export function checkCommunityAvailable(communityTopicIds: HieroId[], homeFolder: string): boolean { + const communitiesPath = path.join(homeFolder, 'communities.json') + if (!checkFileExist(communitiesPath)) { + return false + } + const communities = JSON.parse(fs.readFileSync(communitiesPath, 'utf-8')) + let foundCount = 0 + for (const community of communities) { + if (communityTopicIds.includes(community.hieroTopicId)) { + foundCount++ + if (foundCount >= communityTopicIds.length) { + return true + } + } + } + return false +} diff --git a/dlt-connector/src/client/backend/BackendClient.ts b/dlt-connector/src/client/backend/BackendClient.ts index cee82769a..7f779fd73 100644 --- a/dlt-connector/src/client/backend/BackendClient.ts +++ b/dlt-connector/src/client/backend/BackendClient.ts @@ -5,7 +5,11 @@ import * as v from 'valibot' import { CONFIG } from '../../config' import { LOG4JS_BASE_CATEGORY } from '../../config/const' import { HieroId, Uuidv4 } from '../../schemas/typeGuard.schema' -import { homeCommunityGraphqlQuery, setHomeCommunityTopicId } from './graphql' +import { + getReachableCommunities, + homeCommunityGraphqlQuery, + setHomeCommunityTopicId, +} from './graphql' import { type Community, communitySchema } from './output.schema' // Source: https://refactoring.guru/design-patterns/singleton/typescript/example @@ -42,7 +46,7 @@ export class BackendClient { } public get url(): string { - return this.url + return this.urlValue } /** @@ -84,6 +88,19 @@ export class BackendClient { return v.parse(communitySchema, data.updateHomeCommunity) } + public async getReachableCommunities(): Promise { + this.logger.info('get reachable communities on backend') + const { data, errors } = await this.client.rawRequest<{ reachableCommunities: Community[] }>( + getReachableCommunities, + {}, + await this.getRequestHeader(), + ) + if (errors) { + throw errors[0] + } + return v.parse(v.array(communitySchema), data.reachableCommunities) + } + private async getRequestHeader(): Promise<{ authorization: string }> { diff --git a/dlt-connector/src/client/backend/graphql.ts b/dlt-connector/src/client/backend/graphql.ts index 11d1eb099..fafd77fcd 100644 --- a/dlt-connector/src/client/backend/graphql.ts +++ b/dlt-connector/src/client/backend/graphql.ts @@ -4,17 +4,24 @@ import { gql } from 'graphql-request' * Schema Definitions for graphql requests */ +const communityFragment = gql` + fragment Community_common on Community { + uuid + name + hieroTopicId + foreign + creationDate + } +` + // graphql query for getting home community in tune with community schema export const homeCommunityGraphqlQuery = gql` query { homeCommunity { - uuid - name - hieroTopicId - foreign - creationDate + ...Community_common } } + ${communityFragment} ` export const setHomeCommunityTopicId = gql` @@ -28,3 +35,12 @@ export const setHomeCommunityTopicId = gql` } } ` + +export const getReachableCommunities = gql` + query { + reachableCommunities { + ...Community_common + } + } + ${communityFragment} +` diff --git a/dlt-connector/src/client/hiero/HieroClient.ts b/dlt-connector/src/client/hiero/HieroClient.ts index e48eefc92..33a208b78 100644 --- a/dlt-connector/src/client/hiero/HieroClient.ts +++ b/dlt-connector/src/client/hiero/HieroClient.ts @@ -1,8 +1,11 @@ import { AccountBalance, AccountBalanceQuery, + AddressBookQuery, Client, + FileId, LocalProvider, + NodeAddressBook, PrivateKey, TopicCreateTransaction, TopicId, @@ -166,6 +169,16 @@ export class HieroClient { return v.parse(hieroIdSchema, createReceipt.topicId?.toString()) } + public async downloadAddressBook(): Promise { + const query = new AddressBookQuery().setFileId(FileId.ADDRESS_BOOK) + try { + return await query.execute(this.client) + } catch (e) { + this.logger.error(e) + throw e + } + } + public async updateTopic(topicId: HieroId): Promise { this.logger.addContext('topicId', topicId.toString()) let transaction = new TopicUpdateTransaction() diff --git a/dlt-connector/src/config/const.ts b/dlt-connector/src/config/const.ts index 6b2a3b1a2..dc4b0177c 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -1,5 +1,21 @@ +import path from 'node:path' + export const LOG4JS_BASE_CATEGORY = 'dlt' // 7 days export const MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE = 1000 * 60 * 60 * 24 * 7 // 10 minutes export const MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE = 1000 * 60 * 10 + +export const GRADIDO_NODE_RUNTIME_PATH = path.join( + __dirname, + '..', + '..', + 'gradido_node', + 'bin', + 'GradidoNode', +) +// if last start was less than this time, do not restart +export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS = 1000 * 30 +export const GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS = 10000 +// currently hard coded in gradido node, update in future +export const GRADIDO_NODE_HOME_FOLDER_NAME = '.gradido' diff --git a/dlt-connector/src/config/schema.ts b/dlt-connector/src/config/schema.ts index 2ce01dcee..67a43383d 100644 --- a/dlt-connector/src/config/schema.ts +++ b/dlt-connector/src/config/schema.ts @@ -1,3 +1,4 @@ +import path from 'node:path' import { MemoryBlock } from 'gradido-blockchain-js' import * as v from 'valibot' @@ -78,6 +79,14 @@ export const configSchema = v.object({ ), '8340', ), + DLT_GRADIDO_NODE_SERVER_VERSION: v.optional( + v.string('The version of the DLT node server'), + '0.9.0', + ), + DLT_GRADIDO_NODE_SERVER_HOME_FOLDER: v.optional( + v.string('The home folder for the gradido dlt node server'), + path.join(__dirname, '..', '..', 'gradido_node'), + ), PORT: v.optional( v.pipe( v.string('A valid port on which the backend server is running'), diff --git a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts index 14427cacf..5b5f1f629 100644 --- a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts +++ b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts @@ -7,6 +7,7 @@ import { } from 'gradido-blockchain-js' import { getLogger } from 'log4js' import * as v from 'valibot' +import { ensureCommunitiesAvailable } from '../../client/GradidoNode/communities' import { HieroClient } from '../../client/hiero/HieroClient' import { LOG4JS_BASE_CATEGORY } from '../../config/const' import { InputTransactionType } from '../../data/InputTransactionType.enum' @@ -40,7 +41,7 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.interactions.sendToHiero.SendT export async function SendToHieroContext( input: TransactionInput | CommunityInput, ): Promise { - const role = chooseCorrectRole(input) + const role = await chooseCorrectRole(input) const builder = await role.getGradidoTransactionBuilder() if (builder.isCrossCommunityTransaction()) { // build cross group transaction @@ -107,9 +108,13 @@ async function sendViaHiero( } // choose correct role based on transaction type and input type -function chooseCorrectRole(input: TransactionInput | CommunityInput): AbstractTransactionRole { +async function chooseCorrectRole( + input: TransactionInput | CommunityInput, +): Promise { const communityParsingResult = v.safeParse(communitySchema, input) if (communityParsingResult.success) { + // make sure gradido node knows community + await ensureCommunitiesAvailable([communityParsingResult.output.hieroTopicId]) return new CommunityRootTransactionRole(communityParsingResult.output) } @@ -121,6 +126,12 @@ function chooseCorrectRole(input: TransactionInput | CommunityInput): AbstractTr }) throw new Error('invalid input') } + // make sure gradido node knows communities + const communityTopicIds = [ + transactionParsingResult.output.user.communityTopicId, + transactionParsingResult.output.linkedUser?.communityTopicId, + ].filter((id): id is HieroId => id !== undefined) + await ensureCommunitiesAvailable(communityTopicIds) const transaction = transactionParsingResult.output switch (transaction.type) { diff --git a/dlt-connector/src/server/index.test.ts b/dlt-connector/src/server/index.test.ts index 9ec7f236a..4b6b8be76 100644 --- a/dlt-connector/src/server/index.test.ts +++ b/dlt-connector/src/server/index.test.ts @@ -25,6 +25,12 @@ mock.module('../KeyPairCacheManager', () => { } }) +mock.module('../client/GradidoNode/communities', () => ({ + ensureCommunitiesAvailable: () => { + return Promise.resolve() + }, +})) + mock.module('../client/hiero/HieroClient', () => ({ HieroClient: { getInstance: () => ({ diff --git a/dlt-connector/src/server/index.ts b/dlt-connector/src/server/index.ts index 406d04d06..191e990d3 100644 --- a/dlt-connector/src/server/index.ts +++ b/dlt-connector/src/server/index.ts @@ -3,6 +3,7 @@ import { Elysia, status, t } from 'elysia' import { AddressType_NONE } from 'gradido-blockchain-js' import { getLogger } from 'log4js' import * as v from 'valibot' +import { ensureCommunitiesAvailable } from '../client/GradidoNode/communities' import { GradidoNodeClient } from '../client/GradidoNode/GradidoNodeClient' import { LOG4JS_BASE_CATEGORY } from '../config/const' import { KeyPairIdentifierLogic } from '../data/KeyPairIdentifier.logic' @@ -125,6 +126,8 @@ async function isAccountExist(identifierAccount: IdentifierAccountInput): Promis // check and prepare input const startTime = Date.now() const identifierAccountParsed = v.parse(identifierAccountSchema, identifierAccount) + // make sure gradido node knows community + await ensureCommunitiesAvailable([identifierAccountParsed.communityTopicId]) const accountKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic(identifierAccountParsed)) const publicKey = accountKeyPair.getPublicKey() if (!publicKey) { diff --git a/dlt-connector/src/utils/filesystem.ts b/dlt-connector/src/utils/filesystem.ts new file mode 100644 index 000000000..ea2d64e39 --- /dev/null +++ b/dlt-connector/src/utils/filesystem.ts @@ -0,0 +1,30 @@ +import fs from 'node:fs' +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY } from '../config/const' + +const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.utils.filesystem`) + +export function checkFileExist(filePath: string): boolean { + try { + fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK) + return true + } catch (_err) { + // logger.debug(`file ${filePath} does not exist: ${_err}`) + return false + } +} + +export function checkPathExist(path: string, createIfMissing: boolean = false): boolean { + const exists = checkFileExist(path) + if (exists) { + return true + } + if (createIfMissing) { + logger.info(`create folder ${path}`) + fs.mkdirSync(path, { recursive: true }) + if (!checkPathExist(path)) { + throw new Error(`Failed to create path ${path}`) + } + } + return false +}