mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'dlt_gradido_node_as_subprocess' into dlt_inspector_as_submodule
This commit is contained in:
commit
1604344445
@ -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 = {
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -4,4 +4,5 @@ export interface PublicCommunityInfo {
|
||||
creationDate: Date
|
||||
publicKey: string
|
||||
publicJwtKey: string
|
||||
hieroTopicId: string | null
|
||||
}
|
||||
|
||||
@ -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<void> {
|
||||
logger.error(`Error:`, err)
|
||||
}
|
||||
}
|
||||
// export communities for gradido dlt node server
|
||||
await exportCommunitiesToDltNodeServer()
|
||||
}
|
||||
|
||||
export async function writeJwtKeyPairInHomeCommunity(): Promise<DbCommunity> {
|
||||
@ -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<void> {
|
||||
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<string>()
|
||||
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}`)
|
||||
}
|
||||
|
||||
@ -90,6 +90,7 @@ DLT_CONNECTOR=false
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
DLT_NODE_SERVER_PORT=8340
|
||||
DLT_NODE_SERVER_URL=$URL_PROTOCOL://$COMMUNITY_HOST/dlt
|
||||
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
|
||||
|
||||
@ -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
|
||||
1
dlt-connector/.gitignore
vendored
1
dlt-connector/.gitignore
vendored
@ -7,3 +7,4 @@ package-json.lock
|
||||
coverage
|
||||
# emacs
|
||||
*~
|
||||
gradido_node
|
||||
@ -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=="],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
@ -36,7 +37,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 +65,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)
|
||||
}
|
||||
|
||||
@ -74,6 +75,9 @@ export async function checkGradidoNode(
|
||||
logger: Logger,
|
||||
homeCommunity: Community,
|
||||
): Promise<void> {
|
||||
// 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 (
|
||||
|
||||
89
dlt-connector/src/bootstrap/initGradidoNode.ts
Normal file
89
dlt-connector/src/bootstrap/initGradidoNode.ts
Normal file
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}`
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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}/api`
|
||||
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<T>(method: string, parameter: any): Promise<JsonRpcEitherResponse<T>> {
|
||||
this.logger.debug('call %s with %s', method, parameter)
|
||||
await isPortOpenRetry(CONFIG.NODE_SERVER_URL)
|
||||
await isPortOpenRetry(this.url)
|
||||
return this.client.exec<T>(method, parameter)
|
||||
}
|
||||
|
||||
|
||||
121
dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts
Normal file
121
dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts
Normal file
@ -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<void> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
84
dlt-connector/src/client/GradidoNode/communities.ts
Normal file
84
dlt-connector/src/client/GradidoNode/communities.ts
Normal file
@ -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<void> {
|
||||
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<void> {
|
||||
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<string>()
|
||||
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
|
||||
}
|
||||
@ -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
|
||||
@ -18,6 +22,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 +30,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 +45,10 @@ export class BackendClient {
|
||||
})
|
||||
}
|
||||
|
||||
public get url(): string {
|
||||
return this.urlValue
|
||||
}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
@ -77,6 +88,19 @@ export class BackendClient {
|
||||
return v.parse(communitySchema, data.updateHomeCommunity)
|
||||
}
|
||||
|
||||
public async getReachableCommunities(): Promise<Community[]> {
|
||||
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
|
||||
}> {
|
||||
|
||||
@ -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}
|
||||
`
|
||||
|
||||
@ -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<NodeAddressBook> {
|
||||
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<void> {
|
||||
this.logger.addContext('topicId', topicId.toString())
|
||||
let transaction = new TopicUpdateTransaction()
|
||||
|
||||
@ -1,3 +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'
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import path from 'node:path'
|
||||
import { MemoryBlock } from 'gradido-blockchain-js'
|
||||
import * as v from 'valibot'
|
||||
|
||||
@ -73,12 +74,30 @@ 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<string, number>((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',
|
||||
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'),
|
||||
v.transform<string, number>((input: string) => Number(input)),
|
||||
v.minValue(1),
|
||||
v.maxValue(65535),
|
||||
),
|
||||
'4000',
|
||||
),
|
||||
})
|
||||
|
||||
@ -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'
|
||||
@ -21,6 +22,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'
|
||||
@ -39,13 +41,17 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.interactions.sendToHiero.SendT
|
||||
export async function SendToHieroContext(
|
||||
input: TransactionInput | CommunityInput,
|
||||
): Promise<HieroTransactionIdString> {
|
||||
const role = chooseCorrectRole(input)
|
||||
const role = await chooseCorrectRole(input)
|
||||
const builder = await role.getGradidoTransactionBuilder()
|
||||
if (builder.isCrossCommunityTransaction()) {
|
||||
// build cross group transaction
|
||||
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,
|
||||
@ -102,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<AbstractTransactionRole> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -116,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) {
|
||||
|
||||
@ -25,6 +25,12 @@ mock.module('../KeyPairCacheManager', () => {
|
||||
}
|
||||
})
|
||||
|
||||
mock.module('../client/GradidoNode/communities', () => ({
|
||||
ensureCommunitiesAvailable: () => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('../client/hiero/HieroClient', () => ({
|
||||
HieroClient: {
|
||||
getInstance: () => ({
|
||||
|
||||
@ -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) {
|
||||
|
||||
30
dlt-connector/src/utils/filesystem.ts
Normal file
30
dlt-connector/src/utils/filesystem.ts
Normal file
@ -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
|
||||
}
|
||||
19
dlt-connector/src/utils/hiero.ts
Normal file
19
dlt-connector/src/utils/hiero.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
* for sending messages for at least `MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE` milliseconds.
|
||||
*
|
||||
* @param {HieroId} hieroTopicId - The topic ID to check.
|
||||
* @returns {Promise<boolean>} `true` if the topic is still open long enough, otherwise `false`.
|
||||
*/
|
||||
export async function isTopicStillOpen(hieroTopicId: HieroId): Promise<boolean> {
|
||||
const hieroClient = HieroClient.getInstance()
|
||||
const topicInfo = await hieroClient.getTopicInfo(hieroTopicId)
|
||||
return (
|
||||
topicInfo.expirationTime.getTime() >
|
||||
new Date().getTime() + MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user