From e2cda2c297d60fd12b26322bd6c85088962596f9 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 23 Oct 2025 09:53:34 +0200 Subject: [PATCH 01/11] update constructing of backend and gradido node server urls --- dlt-connector/.env.dist | 4 ++-- dlt-connector/src/bootstrap/init.ts | 6 ++--- .../client/GradidoNode/GradidoNodeClient.ts | 12 ++++++++-- dlt-connector/src/config/schema.ts | 22 ++++++++++++++----- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/dlt-connector/.env.dist b/dlt-connector/.env.dist index ff40bd5d1..8253699ca 100644 --- a/dlt-connector/.env.dist +++ b/dlt-connector/.env.dist @@ -13,12 +13,12 @@ IOTA_HOME_COMMUNITY_SEED=aabbccddeeff00112233445566778899aabbccddeeff00112233445 DLT_CONNECTOR_PORT=6010 # Gradido Node Server URL -NODE_SERVER_URL=http://localhost:8340/api +DLT_NODE_SERVER_PORT=8340 # Gradido Blockchain GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=21ffbbc616fe GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=a51ef8ac7ef1abf162fb7a65261acd7a # Route to Backend -BACKEND_SERVER_URL=http://localhost:4000 +PORT=4000 JWT_SECRET=secret123 \ No newline at end of file diff --git a/dlt-connector/src/bootstrap/init.ts b/dlt-connector/src/bootstrap/init.ts index 1d70f4003..c41553594 100644 --- a/dlt-connector/src/bootstrap/init.ts +++ b/dlt-connector/src/bootstrap/init.ts @@ -36,7 +36,7 @@ export async function checkHomeCommunity( const { backend, hiero } = appContext.clients // wait for backend server - await isPortOpenRetry(CONFIG.BACKEND_SERVER_URL) + await isPortOpenRetry(backend.url) // ask backend for home community let homeCommunity = await backend.getHomeCommunityDraft() // on missing topicId, create one @@ -64,8 +64,8 @@ export async function checkHomeCommunity( } appContext.cache.setHomeCommunityTopicId(homeCommunity.hieroTopicId) logger.info(`home community topic: ${homeCommunity.hieroTopicId}`) - logger.info(`gradido node server: ${CONFIG.NODE_SERVER_URL}`) - logger.info(`gradido backend server: ${CONFIG.BACKEND_SERVER_URL}`) + logger.info(`gradido node server: ${appContext.clients.gradidoNode.url}`) + logger.info(`gradido backend server: ${appContext.clients.backend.url}`) return v.parse(communitySchema, homeCommunity) } diff --git a/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts b/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts index efbfc6d22..3bd3bab51 100644 --- a/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeClient.ts @@ -36,13 +36,21 @@ export class GradidoNodeClient { private static instance: GradidoNodeClient client: JsonRpcClient logger: Logger + urlValue: string private constructor() { this.logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNodeClient`) + this.urlValue = `http://localhost:${CONFIG.DLT_NODE_SERVER_PORT}` + this.logger.addContext('url', this.urlValue) this.client = new JsonRpcClient({ - url: CONFIG.NODE_SERVER_URL, + url: this.urlValue, }) } + + public get url(): string { + return this.urlValue + } + public static getInstance(): GradidoNodeClient { if (!GradidoNodeClient.instance) { GradidoNodeClient.instance = new GradidoNodeClient() @@ -233,7 +241,7 @@ export class GradidoNodeClient { // template rpcCall, check first if port is open before executing json rpc 2.0 request protected async rpcCall(method: string, parameter: any): Promise> { this.logger.debug('call %s with %s', method, parameter) - await isPortOpenRetry(CONFIG.NODE_SERVER_URL) + await isPortOpenRetry(this.url) return this.client.exec(method, parameter) } diff --git a/dlt-connector/src/config/schema.ts b/dlt-connector/src/config/schema.ts index 08eeb2b80..2ce01dcee 100644 --- a/dlt-connector/src/config/schema.ts +++ b/dlt-connector/src/config/schema.ts @@ -69,12 +69,22 @@ export const configSchema = v.object({ ), 500, ), - NODE_SERVER_URL: v.optional( - v.string('The URL of the gradido node server'), - 'http://localhost:6010', + DLT_NODE_SERVER_PORT: v.optional( + v.pipe( + v.string('A valid port on which the DLT node server is running'), + v.transform((input: string) => Number(input)), + v.minValue(1), + v.maxValue(65535), + ), + '8340', ), - BACKEND_SERVER_URL: v.optional( - v.string('The URL of the gradido backend server'), - 'http://localhost:6010', + PORT: v.optional( + v.pipe( + v.string('A valid port on which the backend server is running'), + v.transform((input: string) => Number(input)), + v.minValue(1), + v.maxValue(65535), + ), + '4000', ), }) From 4b59cf2377a63840a6dba35610600138dc306bcb Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 24 Oct 2025 07:20:49 +0200 Subject: [PATCH 02/11] add missing change --- dlt-connector/src/client/backend/BackendClient.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dlt-connector/src/client/backend/BackendClient.ts b/dlt-connector/src/client/backend/BackendClient.ts index d5cc3be9e..cee82769a 100644 --- a/dlt-connector/src/client/backend/BackendClient.ts +++ b/dlt-connector/src/client/backend/BackendClient.ts @@ -18,6 +18,7 @@ export class BackendClient { private static instance: BackendClient client: GraphQLClient logger: Logger + urlValue: string /** * The Singleton's constructor should always be private to prevent direct @@ -25,8 +26,10 @@ export class BackendClient { */ private constructor() { this.logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.BackendClient`) - this.logger.addContext('url', CONFIG.BACKEND_SERVER_URL) - this.client = new GraphQLClient(CONFIG.BACKEND_SERVER_URL, { + this.urlValue = `http://localhost:${CONFIG.PORT}` + this.logger.addContext('url', this.urlValue) + + this.client = new GraphQLClient(this.urlValue, { headers: { 'content-type': 'application/json', }, @@ -38,6 +41,10 @@ export class BackendClient { }) } + public get url(): string { + return this.url + } + /** * The static method that controls the access to the singleton instance. * From 567fbbaf66eae1bff00f2d884229884e7564e6fa Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 24 Oct 2025 07:49:06 +0200 Subject: [PATCH 03/11] check for valid recipient community topic before sending outbound cross group transaction --- dlt-connector/src/config/const.ts | 2 ++ .../sendToHiero/SendToHiero.context.ts | 6 +++++- dlt-connector/src/utils/hiero.ts | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 dlt-connector/src/utils/hiero.ts diff --git a/dlt-connector/src/config/const.ts b/dlt-connector/src/config/const.ts index e0dfc82a3..fa66eb6e7 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -1,3 +1,5 @@ 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 \ No newline at end of file diff --git a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts index 1dd74976c..f510be3bd 100644 --- a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts +++ b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts @@ -28,7 +28,7 @@ import { DeferredTransferTransactionRole } from './DeferredTransferTransaction.r import { RedeemDeferredTransferTransactionRole } from './RedeemDeferredTransferTransaction.role' import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role' import { TransferTransactionRole } from './TransferTransaction.role' - +import { isTopicStillOpen } from '../../utils/hiero' const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.interactions.sendToHiero.SendToHieroContext`) /** @@ -46,6 +46,10 @@ export async function SendToHieroContext( const outboundTransaction = builder.buildOutbound() validate(outboundTransaction) + if (!isTopicStillOpen(role.getRecipientCommunityTopicId())) { + throw new Error('recipient topic is not open long enough for sending messages') + } + // send outbound transaction to hiero at first, because we need the transaction id for inbound transaction const outboundHieroTransactionIdString = await sendViaHiero( outboundTransaction, diff --git a/dlt-connector/src/utils/hiero.ts b/dlt-connector/src/utils/hiero.ts new file mode 100644 index 000000000..9ea62042f --- /dev/null +++ b/dlt-connector/src/utils/hiero.ts @@ -0,0 +1,16 @@ +import { HieroId } from '../schemas/typeGuard.schema' +import { HieroClient } from '../client/hiero/HieroClient' +import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE } from '../config/const' + +/** + * Checks whether the given topic in the Hedera network will remain open + * for sending messages for at least `MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE` milliseconds. + * + * @param {HieroId} hieroTopicId - The topic ID to check. + * @returns {Promise} `true` if the topic is still open long enough, otherwise `false`. + */ +export async function isTopicStillOpen(hieroTopicId: HieroId): Promise { + const hieroClient = HieroClient.getInstance() + const topicInfo = await hieroClient.getTopicInfo(hieroTopicId) + return topicInfo.expirationTime.getTime() > new Date().getTime() + MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE +} \ No newline at end of file From 8983bc52ebf72f9a2cbf7363e770c5e68eced729 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 24 Oct 2025 08:28:53 +0200 Subject: [PATCH 04/11] add config option, if dlt-connector is enabled, write communities list for dlt gradido node server out in its home folder as communities.json on each validate communities run --- backend/src/config/index.ts | 1 + backend/src/config/schema.ts | 4 ++ .../client/1_0/model/PublicCommunityInfo.ts | 1 + backend/src/federation/validateCommunities.ts | 52 +++++++++++++++++++ deployment/bare_metal/.env.dist | 1 + .../1_0/model/GetPublicCommunityInfoResult.ts | 4 ++ 6 files changed, 63 insertions(+) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index aa0a7dbf2..b583156b5 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -43,6 +43,7 @@ const DLT_CONNECTOR_PORT = process.env.DLT_CONNECTOR_PORT ?? 6010 const dltConnector = { DLT_CONNECTOR: process.env.DLT_CONNECTOR === 'true' || false, DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? `${COMMUNITY_URL}:${DLT_CONNECTOR_PORT}`, + DLT_GRADIDO_NODE_SERVER_HOME_FOLDER: process.env.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER ?? '~/.gradido', } const community = { diff --git a/backend/src/config/schema.ts b/backend/src/config/schema.ts index 144608992..f4e6033ea 100644 --- a/backend/src/config/schema.ts +++ b/backend/src/config/schema.ts @@ -79,6 +79,10 @@ export const schema = Joi.object({ .when('DLT_CONNECTOR', { is: true, then: Joi.required() }) .description('The URL for GDT API endpoint'), + DLT_GRADIDO_NODE_SERVER_HOME_FOLDER: Joi.string() + .default('~/.gradido') + .description('The home folder for the gradido dlt node server'), + EMAIL: Joi.boolean() .default(false) .description('Enable or disable email functionality') diff --git a/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts b/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts index 1abbeb9e7..7c228c799 100644 --- a/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts +++ b/backend/src/federation/client/1_0/model/PublicCommunityInfo.ts @@ -4,4 +4,5 @@ export interface PublicCommunityInfo { creationDate: Date publicKey: string publicJwtKey: string + hieroTopicId: string | null } diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 10088cf35..3b910b313 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -2,6 +2,7 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, getHomeCommunity, + getReachableCommunities, } from 'database' import { IsNull } from 'typeorm' @@ -15,6 +16,9 @@ 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`) @@ -83,6 +87,8 @@ export async function validateCommunities(): Promise { logger.error(`Error:`, err) } } + // export communities for gradido dlt node server + await exportCommunitiesToDltNodeServer() } export async function writeJwtKeyPairInHomeCommunity(): Promise { @@ -138,6 +144,52 @@ async function writeForeignCommunity( com.publicKey = dbCom.publicKey com.publicJwtKey = pubInfo.publicJwtKey com.url = dbCom.endPoint + com.hieroTopicId = pubInfo.hieroTopicId 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/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 11f5e1d76..729fc84aa 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -88,6 +88,7 @@ GDT_ACTIVE=false # DLT-Connector (still in develop) DLT_CONNECTOR=false DLT_CONNECTOR_PORT=6010 +DLT_GRADIDO_NODE_SERVER_HOME_FOLDER=/home/gradido/.gradido # used for combining a newsletter on klicktipp with this gradido community # if used, user will be subscribed on register and can unsubscribe in his account diff --git a/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts b/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts index 55292cee2..1c7c5587c 100644 --- a/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts +++ b/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts @@ -12,6 +12,7 @@ export class GetPublicCommunityInfoResult { this.name = dbCom.name this.description = dbCom.description this.creationDate = dbCom.creationDate + this.hieroTopicId = dbCom.hieroTopicId } @Field(() => String) @@ -28,4 +29,7 @@ export class GetPublicCommunityInfoResult { @Field(() => String) publicJwtKey: string + + @Field(() => String) + hieroTopicId: string | null } From b9d51269ca062cd55cf7b3d68d6d7dedd965fa84 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 24 Oct 2025 08:36:07 +0200 Subject: [PATCH 05/11] fix lint --- dlt-connector/src/config/const.ts | 2 +- .../interactions/sendToHiero/SendToHiero.context.ts | 3 ++- dlt-connector/src/utils/hiero.ts | 11 +++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/dlt-connector/src/config/const.ts b/dlt-connector/src/config/const.ts index fa66eb6e7..6b2a3b1a2 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -2,4 +2,4 @@ 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 \ No newline at end of file +export const MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE = 1000 * 60 * 10 diff --git a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts index f510be3bd..14427cacf 100644 --- a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts +++ b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts @@ -21,6 +21,7 @@ import { HieroTransactionIdString, hieroTransactionIdStringSchema, } from '../../schemas/typeGuard.schema' +import { isTopicStillOpen } from '../../utils/hiero' import { AbstractTransactionRole } from './AbstractTransaction.role' import { CommunityRootTransactionRole } from './CommunityRootTransaction.role' import { CreationTransactionRole } from './CreationTransaction.role' @@ -28,7 +29,7 @@ import { DeferredTransferTransactionRole } from './DeferredTransferTransaction.r import { RedeemDeferredTransferTransactionRole } from './RedeemDeferredTransferTransaction.role' import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role' import { TransferTransactionRole } from './TransferTransaction.role' -import { isTopicStillOpen } from '../../utils/hiero' + const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.interactions.sendToHiero.SendToHieroContext`) /** diff --git a/dlt-connector/src/utils/hiero.ts b/dlt-connector/src/utils/hiero.ts index 9ea62042f..bd1501e83 100644 --- a/dlt-connector/src/utils/hiero.ts +++ b/dlt-connector/src/utils/hiero.ts @@ -1,9 +1,9 @@ -import { HieroId } from '../schemas/typeGuard.schema' import { HieroClient } from '../client/hiero/HieroClient' import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE } from '../config/const' +import { HieroId } from '../schemas/typeGuard.schema' /** - * Checks whether the given topic in the Hedera network will remain open + * Checks whether the given topic in the Hedera network will remain open * for sending messages for at least `MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE` milliseconds. * * @param {HieroId} hieroTopicId - The topic ID to check. @@ -12,5 +12,8 @@ import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE } from '../config/const' export async function isTopicStillOpen(hieroTopicId: HieroId): Promise { const hieroClient = HieroClient.getInstance() const topicInfo = await hieroClient.getTopicInfo(hieroTopicId) - return topicInfo.expirationTime.getTime() > new Date().getTime() + MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE -} \ No newline at end of file + return ( + topicInfo.expirationTime.getTime() > + new Date().getTime() + MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE + ) +} From f962baf1a1be6663200c6b1f2c2ca44acbd7678a Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 24 Oct 2025 12:57:16 +0200 Subject: [PATCH 06/11] add process handler for starting and managing GradidoNode as subprocess --- dlt-connector/src/bootstrap/shutdown.ts | 2 + .../client/GradidoNode/GradidoNodeProcess.ts | 117 ++++++++++++++++++ dlt-connector/src/config/const.ts | 7 ++ dlt-connector/src/config/schema.ts | 1 + 4 files changed, 127 insertions(+) create mode 100644 dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts 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/GradidoNodeProcess.ts b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts new file mode 100644 index 000000000..ad25c2461 --- /dev/null +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts @@ -0,0 +1,117 @@ +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(), + }, + 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: ${proc?.resourceUsage()}`) + 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', + }) + } + + 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/config/const.ts b/dlt-connector/src/config/const.ts index 6b2a3b1a2..0547f0c8a 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -1,5 +1,12 @@ +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 = 1000 diff --git a/dlt-connector/src/config/schema.ts b/dlt-connector/src/config/schema.ts index 2ce01dcee..027dfc364 100644 --- a/dlt-connector/src/config/schema.ts +++ b/dlt-connector/src/config/schema.ts @@ -78,6 +78,7 @@ export const configSchema = v.object({ ), '8340', ), + DLT_NODE_SERVER_VERSION: v.optional(v.string('The version of the DLT node server'), '0.9.0'), PORT: v.optional( v.pipe( v.string('A valid port on which the backend server is running'), From 3bdc99b203a5d7f0b78518ce76be310e3c4e97ce Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 25 Oct 2025 14:43:47 +0200 Subject: [PATCH 07/11] setup gradido node --- dlt-connector/.gitignore | 1 + dlt-connector/bun.lock | 3 + dlt-connector/package.json | 1 + dlt-connector/src/bootstrap/init.ts | 4 + .../src/bootstrap/initGradidoNode.ts | 89 +++++++++++++++++++ .../client/GradidoNode/GradidoNodeClient.ts | 2 +- .../client/GradidoNode/GradidoNodeProcess.ts | 13 +-- .../src/client/GradidoNode/communities.ts | 84 +++++++++++++++++ .../src/client/backend/BackendClient.ts | 21 ++++- dlt-connector/src/client/backend/graphql.ts | 33 ++++--- dlt-connector/src/client/hiero/HieroClient.ts | 13 +++ dlt-connector/src/config/const.ts | 11 ++- dlt-connector/src/config/schema.ts | 10 ++- .../sendToHiero/SendToHiero.context.ts | 15 +++- dlt-connector/src/server/index.test.ts | 6 ++ dlt-connector/src/server/index.ts | 3 + dlt-connector/src/utils/filesystem.ts | 30 +++++++ 17 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 dlt-connector/src/bootstrap/initGradidoNode.ts create mode 100644 dlt-connector/src/client/GradidoNode/communities.ts create mode 100644 dlt-connector/src/utils/filesystem.ts 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..a60a02798 100644 --- a/dlt-connector/bun.lock +++ b/dlt-connector/bun.lock @@ -13,6 +13,7 @@ "@sinclair/typemap": "^0.10.1", "@types/bun": "^1.2.17", "@types/uuid": "^8.3.4", + "async-mutex": "^0.5.0", "dotenv": "^10.0.0", "elysia": "1.3.8", "graphql-request": "^7.2.0", @@ -306,6 +307,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..da30cf58d 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -26,6 +26,7 @@ "@sinclair/typemap": "^0.10.1", "@types/bun": "^1.2.17", "@types/uuid": "^8.3.4", + "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..3be2e130c --- /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 { gunzipSync } from 'node:zlib' +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) + logger.debug(`GradidoNode Runtime: ${runtimeFileName}`) + 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') { + fs.writeFileSync(runtimeFileName, gunzipSync(Buffer.from(compressedBuffer))) + } 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/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 index ad25c2461..d6f5237bf 100644 --- a/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts @@ -52,12 +52,13 @@ export class GradidoNodeProcess { env: { CLIENTS_HIERO_NETWORKTYPE: CONFIG.HIERO_HEDERA_NETWORK, SERVER_JSON_RPC_PORT: CONFIG.DLT_NODE_SERVER_PORT.toString(), + 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) { + /*if (logger.isDebugEnabled() && proc.stderr) { // print error messages from GradidoNode in our own log if debug is enabled proc.stderr .getReader() @@ -65,9 +66,9 @@ export class GradidoNodeProcess { .then((chunk) => { logger.debug(chunk.value?.toString()) }) - } + }*/ } - logger.debug(`ressource usage: ${proc?.resourceUsage()}`) + logger.debug(`ressource usage: ${JSON.stringify(proc?.resourceUsage(), null, 2)}`) const gradidoNodeProcess = GradidoNodeProcess.getInstance() gradidoNodeProcess.proc = null if ( @@ -80,8 +81,10 @@ export class GradidoNodeProcess { gradidoNodeProcess.start() } }, - stdout: 'ignore', - stderr: logger.isDebugEnabled() ? 'pipe' : 'ignore', + /*stdout: 'ignore', + stderr: logger.isDebugEnabled() ? 'pipe' : 'ignore',*/ + stdout: 'inherit', + stderr: 'inherit', }) } 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..03a4a3544 100644 --- a/dlt-connector/src/client/backend/graphql.ts +++ b/dlt-connector/src/client/backend/graphql.ts @@ -4,27 +4,40 @@ 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` mutation ($uuid: String!, $hieroTopicId: String){ updateHomeCommunity(uuid: $uuid, hieroTopicId: $hieroTopicId) { - uuid - name - hieroTopicId - foreign - creationDate + ...Community_common } } + ${communityFragment} +` + +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 0547f0c8a..9e0b93b46 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -6,7 +6,16 @@ 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') +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 = 1000 +// 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 027dfc364..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,7 +79,14 @@ export const configSchema = v.object({ ), '8340', ), - DLT_NODE_SERVER_VERSION: v.optional(v.string('The version of the DLT node server'), '0.9.0'), + 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..639c1b8a4 --- /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 +} From 04af2ebec9252a563e6ca88308187a255ce7c95c Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 25 Oct 2025 14:48:40 +0200 Subject: [PATCH 08/11] give it more time for gracefull exit --- dlt-connector/src/config/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlt-connector/src/config/const.ts b/dlt-connector/src/config/const.ts index 9e0b93b46..dc4b0177c 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -16,6 +16,6 @@ export const GRADIDO_NODE_RUNTIME_PATH = path.join( ) // 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 = 1000 +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' From 181c1d28fc4d99312108e5265f591225f4fa744b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 25 Oct 2025 15:28:21 +0200 Subject: [PATCH 09/11] fix for windows --- dlt-connector/bun.lock | 3 +++ dlt-connector/package.json | 1 + dlt-connector/src/bootstrap/initGradidoNode.ts | 6 +++--- dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts | 1 + dlt-connector/src/client/backend/graphql.ts | 7 +++++-- dlt-connector/src/utils/filesystem.ts | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/dlt-connector/bun.lock b/dlt-connector/bun.lock index a60a02798..b1929bed8 100644 --- a/dlt-connector/bun.lock +++ b/dlt-connector/bun.lock @@ -13,6 +13,7 @@ "@sinclair/typemap": "^0.10.1", "@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", @@ -285,6 +286,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=="], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index da30cf58d..1271726ec 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -26,6 +26,7 @@ "@sinclair/typemap": "^0.10.1", "@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", diff --git a/dlt-connector/src/bootstrap/initGradidoNode.ts b/dlt-connector/src/bootstrap/initGradidoNode.ts index 3be2e130c..263bed249 100644 --- a/dlt-connector/src/bootstrap/initGradidoNode.ts +++ b/dlt-connector/src/bootstrap/initGradidoNode.ts @@ -1,7 +1,6 @@ import { execSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' -import { gunzipSync } from 'node:zlib' import { getLogger } from 'log4js' import { exportCommunities } from '../client/GradidoNode/communities' import { GradidoNodeProcess } from '../client/GradidoNode/GradidoNodeProcess' @@ -15,6 +14,7 @@ import { import { checkFileExist, checkPathExist } from '../utils/filesystem' import { isPortOpen } from '../utils/network' import { AppContextClients } from './appContext' +import AdmZip from 'adm-zip' const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.bootstrap.initGradidoNode`) @@ -58,7 +58,6 @@ async function exportHederaAddressbooks( async function ensureGradidoNodeRuntimeAvailable(runtimeFileName: string): Promise { const runtimeFolder = path.dirname(runtimeFileName) checkPathExist(runtimeFolder, true) - logger.debug(`GradidoNode Runtime: ${runtimeFileName}`) if (!checkFileExist(runtimeFileName)) { const runtimeArchiveFilename = createGradidoNodeRuntimeArchiveFilename() const downloadUrl = new URL( @@ -71,7 +70,8 @@ async function ensureGradidoNodeRuntimeAvailable(runtimeFileName: string): Promi } const compressedBuffer = await archive.arrayBuffer() if (process.platform === 'win32') { - fs.writeFileSync(runtimeFileName, gunzipSync(Buffer.from(compressedBuffer))) + const zip = new AdmZip(Buffer.from(compressedBuffer)) + zip.extractAllTo(runtimeFolder, true) } else { const archivePath = path.join(runtimeFolder, runtimeArchiveFilename) logger.debug(`GradidoNode Runtime Archive: ${archivePath}`) diff --git a/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts index d6f5237bf..eb2bf6b66 100644 --- a/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts @@ -52,6 +52,7 @@ export class GradidoNodeProcess { 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) { diff --git a/dlt-connector/src/client/backend/graphql.ts b/dlt-connector/src/client/backend/graphql.ts index 03a4a3544..fafd77fcd 100644 --- a/dlt-connector/src/client/backend/graphql.ts +++ b/dlt-connector/src/client/backend/graphql.ts @@ -27,10 +27,13 @@ export const homeCommunityGraphqlQuery = gql` export const setHomeCommunityTopicId = gql` mutation ($uuid: String!, $hieroTopicId: String){ updateHomeCommunity(uuid: $uuid, hieroTopicId: $hieroTopicId) { - ...Community_common + uuid + name + hieroTopicId + foreign + creationDate } } - ${communityFragment} ` export const getReachableCommunities = gql` diff --git a/dlt-connector/src/utils/filesystem.ts b/dlt-connector/src/utils/filesystem.ts index 639c1b8a4..29a3a7519 100644 --- a/dlt-connector/src/utils/filesystem.ts +++ b/dlt-connector/src/utils/filesystem.ts @@ -9,7 +9,7 @@ export function checkFileExist(filePath: string): boolean { fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK) return true } catch (err) { - logger.debug(`file ${filePath} does not exist: ${err}`) + // logger.debug(`file ${filePath} does not exist: ${err}`) return false } } From 39ae8966754cd3c716e21a9a54e13cdb7e7d1d82 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 25 Oct 2025 15:31:05 +0200 Subject: [PATCH 10/11] fix lint --- dlt-connector/src/bootstrap/initGradidoNode.ts | 2 +- dlt-connector/src/utils/filesystem.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dlt-connector/src/bootstrap/initGradidoNode.ts b/dlt-connector/src/bootstrap/initGradidoNode.ts index 263bed249..7c652a667 100644 --- a/dlt-connector/src/bootstrap/initGradidoNode.ts +++ b/dlt-connector/src/bootstrap/initGradidoNode.ts @@ -1,6 +1,7 @@ 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' @@ -14,7 +15,6 @@ import { import { checkFileExist, checkPathExist } from '../utils/filesystem' import { isPortOpen } from '../utils/network' import { AppContextClients } from './appContext' -import AdmZip from 'adm-zip' const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.bootstrap.initGradidoNode`) diff --git a/dlt-connector/src/utils/filesystem.ts b/dlt-connector/src/utils/filesystem.ts index 29a3a7519..ea2d64e39 100644 --- a/dlt-connector/src/utils/filesystem.ts +++ b/dlt-connector/src/utils/filesystem.ts @@ -8,8 +8,8 @@ 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}`) + } catch (_err) { + // logger.debug(`file ${filePath} does not exist: ${_err}`) return false } } From 49051d4bed523e6f2dbc7696eebca9f40fe7344d Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 25 Oct 2025 15:32:00 +0200 Subject: [PATCH 11/11] add missing type --- dlt-connector/bun.lock | 3 +++ dlt-connector/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/dlt-connector/bun.lock b/dlt-connector/bun.lock index b1929bed8..46e566b54 100644 --- a/dlt-connector/bun.lock +++ b/dlt-connector/bun.lock @@ -11,6 +11,7 @@ "@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", @@ -250,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=="], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 1271726ec..e1bef07c9 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -24,6 +24,7 @@ "@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",