From ed94bb7ea0dd2ece9a6f4947fe3db5be25913308 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 21 Oct 2025 11:56:31 +0200 Subject: [PATCH] fix test, refactor --- .../client/GradidoNode/input.schema.test.ts | 8 +- .../src/client/GradidoNode/input.schema.ts | 4 +- dlt-connector/src/client/hiero/HieroClient.ts | 56 ++++--- dlt-connector/src/config/index.ts | 12 +- .../src/data/KeyPairIdentifier.logic.ts | 75 +++++---- dlt-connector/src/errors.ts | 7 + dlt-connector/src/index.ts | 18 +-- .../KeyPairCalculation.context.test.ts | 99 ------------ .../KeyPairCalculation.context.ts | 54 ------- .../AbstractKeyPair.role.ts | 0 .../AbstractRemoteKeyPair.role.ts | 0 .../AccountKeyPair.role.ts | 0 .../ForeignCommunityKeyPair.role.ts | 0 .../HomeCommunityKeyPair.role.ts | 0 .../LinkedTransactionKeyPair.role.ts | 0 .../RemoteAccountKeyPair.role.ts | 0 .../ResolveKeyPair.context.test.ts | 84 ++++++++++ .../resolveKeyPair/ResolveKeyPair.context.ts | 81 ++++++++++ .../UserKeyPair.role.ts | 0 .../UserKeyPairRole.test.ts | 0 .../CommunityRootTransaction.role.ts | 4 +- .../sendToHiero/CreationTransaction.role.ts | 15 +- .../DeferredTransferTransaction.role.ts | 9 +- .../RedeemDeferredTransferTransaction.role.ts | 6 +- .../RegisterAddressTransaction.role.test.ts | 38 +++-- .../RegisterAddressTransaction.role.ts | 10 +- .../sendToHiero/SendToHiero.context.ts | 144 ++++++++++------- .../sendToHiero/TransferTransaction.role.ts | 6 +- dlt-connector/src/schemas/account.schema.ts | 9 +- .../src/schemas/transaction.schema.test.ts | 25 +-- .../src/schemas/transaction.schema.ts | 9 +- .../src/schemas/typeConverter.schema.test.ts | 54 ++++--- dlt-connector/src/schemas/typeGuard.schema.ts | 67 ++++---- dlt-connector/src/server/index.test.ts | 39 +++-- dlt-connector/src/server/index.ts | 147 ++++++++++++------ dlt-connector/src/server/input.schema.ts | 6 +- 36 files changed, 617 insertions(+), 469 deletions(-) delete mode 100644 dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts delete mode 100644 dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/AbstractKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/AbstractRemoteKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/AccountKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/ForeignCommunityKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/HomeCommunityKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/LinkedTransactionKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/RemoteAccountKeyPair.role.ts (100%) create mode 100644 dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.test.ts create mode 100644 dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.ts rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/UserKeyPair.role.ts (100%) rename dlt-connector/src/interactions/{keyPairCalculation => resolveKeyPair}/UserKeyPairRole.test.ts (100%) diff --git a/dlt-connector/src/client/GradidoNode/input.schema.test.ts b/dlt-connector/src/client/GradidoNode/input.schema.test.ts index 1268290fc..b4c86326e 100644 --- a/dlt-connector/src/client/GradidoNode/input.schema.test.ts +++ b/dlt-connector/src/client/GradidoNode/input.schema.test.ts @@ -2,18 +2,18 @@ import { beforeAll, describe, expect, it } from 'bun:test' import { parse } from 'valibot' import { HieroId, - HieroTransactionId, + HieroTransactionIdString, hieroIdSchema, - hieroTransactionIdSchema, + hieroTransactionIdStringSchema, } from '../../schemas/typeGuard.schema' import { transactionIdentifierSchema } from './input.schema' let topic: HieroId const topicString = '0.0.261' -let hieroTransactionId: HieroTransactionId +let hieroTransactionId: HieroTransactionIdString beforeAll(() => { topic = parse(hieroIdSchema, topicString) - hieroTransactionId = parse(hieroTransactionIdSchema, '0.0.261-1755348116-1281621') + hieroTransactionId = parse(hieroTransactionIdStringSchema, '0.0.261-1755348116-1281621') }) describe('transactionIdentifierSchema ', () => { diff --git a/dlt-connector/src/client/GradidoNode/input.schema.ts b/dlt-connector/src/client/GradidoNode/input.schema.ts index baa075baf..16a8643f5 100644 --- a/dlt-connector/src/client/GradidoNode/input.schema.ts +++ b/dlt-connector/src/client/GradidoNode/input.schema.ts @@ -1,5 +1,5 @@ import * as v from 'valibot' -import { hieroIdSchema, hieroTransactionIdSchema } from '../../schemas/typeGuard.schema' +import { hieroIdSchema, hieroTransactionIdStringSchema } from '../../schemas/typeGuard.schema' export const transactionsRangeSchema = v.object({ // default value is 1, from first transactions @@ -18,7 +18,7 @@ export const transactionIdentifierSchema = v.pipe( v.pipe(v.number('expect number type'), v.minValue(1, 'expect number >= 1')), undefined, ), - hieroTransactionId: v.nullish(hieroTransactionIdSchema, undefined), + hieroTransactionId: v.nullish(hieroTransactionIdStringSchema, undefined), topic: hieroIdSchema, }), v.custom((value: any) => { diff --git a/dlt-connector/src/client/hiero/HieroClient.ts b/dlt-connector/src/client/hiero/HieroClient.ts index 92a994345..a22e8f27b 100644 --- a/dlt-connector/src/client/hiero/HieroClient.ts +++ b/dlt-connector/src/client/hiero/HieroClient.ts @@ -62,7 +62,9 @@ export class HieroClient { this.logger.info(`waiting for ${this.pendingPromises.length} pending promises`) await Promise.all(this.pendingPromises) const endTime = new Date() - this.logger.info(`all pending promises resolved, used time: ${endTime.getTime() - startTime.getTime()}ms`) + this.logger.info( + `all pending promises resolved, used time: ${endTime.getTime() - startTime.getTime()}ms`, + ) } public async sendMessage( @@ -85,28 +87,34 @@ export class HieroClient { }).freezeWithSigner(this.wallet) // sign and execute transaction needs some time, so let it run in background const pendingPromiseIndex = this.pendingPromises.push( - hieroTransaction.signWithSigner(this.wallet).then(async (signedHieroTransaction) => { - const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet) - logger.info(`message sent to topic ${topicId}, transaction id: ${sendResponse.transactionId.toString()}`) - if (logger.isInfoEnabled()) { - // only for logging - sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => { - logger.info( - `message send status: ${receipt.status.toString()}`, - ) - }) - // only for logging - sendResponse.getRecordWithSigner(this.wallet).then((record) => { - logger.info(`message sent, cost: ${record.transactionFee.toString()}`) - const localEndTime = new Date() - logger.info(`HieroClient.sendMessage used time (full process): ${localEndTime.getTime() - startTime.getTime()}ms`) - }) - } - }).catch((e) => { - logger.error(e) - }).finally(() => { - this.pendingPromises.splice(pendingPromiseIndex, 1) - }) + hieroTransaction + .signWithSigner(this.wallet) + .then(async (signedHieroTransaction) => { + const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet) + logger.info( + `message sent to topic ${topicId}, transaction id: ${sendResponse.transactionId.toString()}`, + ) + if (logger.isInfoEnabled()) { + // only for logging + sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => { + logger.info(`message send status: ${receipt.status.toString()}`) + }) + // only for logging + sendResponse.getRecordWithSigner(this.wallet).then((record) => { + logger.info(`message sent, cost: ${record.transactionFee.toString()}`) + const localEndTime = new Date() + logger.info( + `HieroClient.sendMessage used time (full process): ${localEndTime.getTime() - startTime.getTime()}ms`, + ) + }) + } + }) + .catch((e) => { + logger.error(e) + }) + .finally(() => { + this.pendingPromises.splice(pendingPromiseIndex, 1) + }), ) const endTime = new Date() logger.info(`HieroClient.sendMessage used time: ${endTime.getTime() - startTime.getTime()}ms`) @@ -148,7 +156,7 @@ export class HieroClient { autoRenewPeriod: undefined, autoRenewAccountId: undefined, }) - + transaction = await transaction.freezeWithSigner(this.wallet) transaction = await transaction.signWithSigner(this.wallet) const createResponse = await transaction.executeWithSigner(this.wallet) diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index 164b78601..10e3cea39 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -1,5 +1,5 @@ import dotenv from 'dotenv' -import { parse, InferOutput, ValiError } from 'valibot' +import { InferOutput, parse, ValiError } from 'valibot' import { configSchema } from './schema' dotenv.config() @@ -8,15 +8,17 @@ type ConfigOutput = InferOutput let config: ConfigOutput try { - console.info('Config loading...') config = parse(configSchema, process.env) -} catch (error: Error | unknown) { +} catch (error) { if (error instanceof ValiError) { - console.error(`${error.issues[0].path[0].key}: ${error.message} received: ${error.issues[0].received}`) + // biome-ignore lint/suspicious/noConsole: need to parse config before initializing logger + console.error( + `${error.issues[0].path[0].key}: ${error.message} received: ${error.issues[0].received}`, + ) } else { + // biome-ignore lint/suspicious/noConsole: need to parse config before initializing logger console.error(error) } - // console.error('Config error:', JSON.stringify(error, null, 2)) process.exit(1) } diff --git a/dlt-connector/src/data/KeyPairIdentifier.logic.ts b/dlt-connector/src/data/KeyPairIdentifier.logic.ts index c64dda299..7ae40ea99 100644 --- a/dlt-connector/src/data/KeyPairIdentifier.logic.ts +++ b/dlt-connector/src/data/KeyPairIdentifier.logic.ts @@ -1,8 +1,25 @@ import { MemoryBlock } from 'gradido-blockchain-js' -import { ParameterError } from '../errors' +import { InvalidCallError, ParameterError } from '../errors' import { IdentifierKeyPair } from '../schemas/account.schema' -import { HieroId } from '../schemas/typeGuard.schema' +import { HieroId, IdentifierSeed, Uuidv4 } from '../schemas/typeGuard.schema' +/** + * @DCI-Logic + * Domain logic for identifying and classifying key pairs used in the Gradido blockchain. + * + * This logic determines the type of key pair (community, user, account, or seed) + * and provides deterministic methods for deriving consistent cache keys and hashes. + * It is pure, stateless, and guaranteed to operate on validated input + * (checked beforehand by Valibot using {@link identifierKeyPairSchema}). + * + * Responsibilities: + * - Identify key pair type via `isCommunityKeyPair()`, `isUserKeyPair()`, `isAccountKeyPair()`, or `isSeedKeyPair()` + * - Provide derived deterministic keys for caching or retrieval + * (e.g. `getCommunityUserKey()`, `getCommunityUserAccountKey()`) + * - or simple: `getKey()` if you don't need to know the details + * - Ensure that invalid method calls throw precise domain-specific errors + * (`InvalidCallError` for misuse, `ParameterError` for unexpected input) + */ export class KeyPairIdentifierLogic { public constructor(public identifier: IdentifierKeyPair) {} @@ -30,33 +47,27 @@ export class KeyPairIdentifierLogic { ) } - getSeed(): string { + getSeed(): IdentifierSeed { if (!this.identifier.seed) { - throw new Error( - 'get seed called on non seed key pair identifier, please check first with isSeedKeyPair()', - ) + throw new InvalidCallError('Invalid call: getSeed() on non-seed identifier') } - return this.identifier.seed.seed + return this.identifier.seed } getCommunityTopicId(): HieroId { return this.identifier.communityTopicId } - getUserUuid(): string { + getUserUuid(): Uuidv4 { if (!this.identifier.account) { - throw new Error( - 'get user uuid called on non user key pair identifier, please check first with isUserKeyPair() or isAccountKeyPair()', - ) + throw new InvalidCallError('Invalid call: getUserUuid() on non-user identifier') } return this.identifier.account.userUuid } getAccountNr(): number { - if (!this.identifier.account?.accountNr) { - throw new Error( - 'get account nr called on non account key pair identifier, please check first with isAccountKeyPair()', - ) + if (!this.identifier.account) { + throw new InvalidCallError('Invalid call: getAccountNr() on non-account identifier') } return this.identifier.account.accountNr } @@ -64,32 +75,36 @@ export class KeyPairIdentifierLogic { getSeedKey(): string { return this.getSeed() } - getCommunityKey(): HieroId { + getCommunityKey(): string { return this.getCommunityTopicId() } getCommunityUserKey(): string { - return this.createCommunityUserHash() + return this.deriveCommunityUserHash() } getCommunityUserAccountKey(): string { - return this.createCommunityUserHash() + this.getAccountNr().toString() + return this.deriveCommunityUserHash() + this.getAccountNr().toString() } getKey(): string { - if (this.isSeedKeyPair()) { - return this.getSeedKey() - } else if (this.isCommunityKeyPair()) { - return this.getCommunityKey() - } else if (this.isUserKeyPair()) { - return this.getCommunityUserKey() - } else if (this.isAccountKeyPair()) { - return this.getCommunityUserAccountKey() + switch (true) { + case this.isSeedKeyPair(): + return this.getSeedKey() + case this.isCommunityKeyPair(): + return this.getCommunityKey() + case this.isUserKeyPair(): + return this.getCommunityUserKey() + case this.isAccountKeyPair(): + return this.getCommunityUserAccountKey() + default: + throw new ParameterError('KeyPairIdentifier: unhandled input constellation') } - throw new ParameterError('KeyPairIdentifier: unhandled input type') } - private createCommunityUserHash(): string { - if (!this.identifier.account?.userUuid || !this.identifier.communityTopicId) { - throw new ParameterError('userUuid and/or communityTopicId is undefined') + private deriveCommunityUserHash(): string { + if (!this.identifier.account) { + throw new InvalidCallError( + 'Invalid call: getCommunityUserKey or getCommunityUserAccountKey() on non-user/non-account identifier', + ) } const resultString = this.identifier.communityTopicId + this.identifier.account.userUuid.replace(/-/g, '') diff --git a/dlt-connector/src/errors.ts b/dlt-connector/src/errors.ts index 3bedb023a..907048f5a 100644 --- a/dlt-connector/src/errors.ts +++ b/dlt-connector/src/errors.ts @@ -55,3 +55,10 @@ export class ParameterError extends Error { this.name = 'ParameterError' } } + +export class InvalidCallError extends Error { + constructor(message: string) { + super(message) + this.name = 'InvalidCallError' + } +} diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index 95f0068ce..2a681d300 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -7,6 +7,7 @@ import { BackendClient } from './client/backend/BackendClient' import { GradidoNodeClient } from './client/GradidoNode/GradidoNodeClient' import { HieroClient } from './client/hiero/HieroClient' import { CONFIG } from './config' +import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE } from './config/const' import { SendToHieroContext } from './interactions/sendToHiero/SendToHiero.context' import { KeyPairCacheManager } from './KeyPairCacheManager' import { Community, communitySchema } from './schemas/transaction.schema' @@ -52,7 +53,7 @@ async function main() { function setupGracefulShutdown(logger: Logger) { const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'] - signals.forEach(sig => { + signals.forEach((sig) => { process.on(sig, async () => { logger.info(`[shutdown] Got ${sig}, cleaning up…`) await gracefulShutdown(logger) @@ -60,13 +61,13 @@ function setupGracefulShutdown(logger: Logger) { }) }) - if (process.platform === "win32") { - const rl = require("readline").createInterface({ + if (process.platform === 'win32') { + const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout, }) - rl.on("SIGINT", () => { - process.emit("SIGINT" as any) + rl.on('SIGINT', () => { + process.emit('SIGINT' as any) }) } } @@ -113,8 +114,8 @@ async function homeCommunitySetup({ backend, hiero }: Clients, logger: Logger): } else { // if topic exist, check if we need to update it let topicInfo = await hiero.getTopicInfo(homeCommunity.hieroTopicId) - console.log(`topicInfo: ${JSON.stringify(topicInfo, null, 2)}`) - /*if ( + // console.log(`topicInfo: ${JSON.stringify(topicInfo, null, 2)}`) + if ( topicInfo.expirationTime.getTime() - new Date().getTime() < MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE ) { @@ -123,7 +124,7 @@ async function homeCommunitySetup({ backend, hiero }: Clients, logger: Logger): logger.info( `updated topic info, new expiration time: ${topicInfo.expirationTime.toLocaleDateString()}`, ) - }*/ + } } if (!homeCommunity.hieroTopicId) { throw new Error('still no topic id, after creating topic and update community in backend.') @@ -140,4 +141,3 @@ main().catch((e) => { console.error(e) process.exit(1) }) - diff --git a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts b/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts deleted file mode 100644 index 7c7103d94..000000000 --- a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, it, expect, mock, beforeAll, afterAll } from 'bun:test' -import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic' -import { KeyPairCalculation } from './KeyPairCalculation.context' -import { parse } from 'valibot' -import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema' -import { KeyPairCacheManager } from '../../KeyPairCacheManager' -import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js' -import { identifierKeyPairSchema } from '../../schemas/account.schema' -/* -// Mock JsonRpcClient -const mockRpcCall = mock((params) => { - console.log('mockRpcCall', params) - return { - isSuccess: () => false, - isError: () => true, - error: { - code: GradidoNodeErrorCodes.TRANSACTION_NOT_FOUND - } - } -}) -const mockRpcCallResolved = mock() - -mock.module('../../utils/network', () => ({ - isPortOpenRetry: async () => true, -})) - -mock.module('jsonrpc-ts-client', () => { - return { - default: class MockJsonRpcClient { - constructor() {} - exec = mockRpcCall - }, - } -}) -*/ - -mock.module('../../KeyPairCacheManager', () => { - let homeCommunityTopicId: HieroId | undefined - return { - KeyPairCacheManager: { - getInstance: () => ({ - setHomeCommunityTopicId: (topicId: HieroId) => { - homeCommunityTopicId = topicId - }, - getHomeCommunityTopicId: () => homeCommunityTopicId, - getKeyPair: (key: string, create: () => KeyPairEd25519) => { - return create() - }, - }), - }, - } -}) - -mock.module('../../config', () => ({ - CONFIG: { - HOME_COMMUNITY_SEED: MemoryBlock.fromHex('0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7'), - }, -})) - -const topicId = '0.0.21732' -const userUuid = 'aa25cf6f-2879-4745-b2ea-6d3c37fb44b0' - -console.log('userUuid', userUuid) - -afterAll(() => { - mock.restore() -}) - -describe('KeyPairCalculation', () => { - beforeAll(() => { - KeyPairCacheManager.getInstance().setHomeCommunityTopicId(parse(hieroIdSchema, '0.0.21732')) - }) - it('community key pair', async () => { - const identifier = new KeyPairIdentifierLogic(parse(identifierKeyPairSchema, { communityTopicId: topicId })) - const keyPair = await KeyPairCalculation(identifier) - expect(keyPair.getPublicKey()?.convertToHex()).toBe('7bcb0d0ad26d3f7ba597716c38a570220cece49b959e57927ee0c39a5a9c3adf') - }) - it('user key pair', async () => { - const identifier = new KeyPairIdentifierLogic(parse(identifierKeyPairSchema, { - communityTopicId: topicId, - account: { userUuid } - })) - expect(identifier.isAccountKeyPair()).toBe(false) - expect(identifier.isUserKeyPair()).toBe(true) - const keyPair = await KeyPairCalculation(identifier) - expect(keyPair.getPublicKey()?.convertToHex()).toBe('d61ae86c262fc0b5d763a8f41a03098fae73a7649a62aac844378a0eb0055921') - }) - - it('account key pair', async () => { - const identifier = new KeyPairIdentifierLogic(parse(identifierKeyPairSchema, { - communityTopicId: topicId, - account: { userUuid, accountNr: 1 } - })) - expect(identifier.isAccountKeyPair()).toBe(true) - expect(identifier.isUserKeyPair()).toBe(false) - const keyPair = await KeyPairCalculation(identifier) - expect(keyPair.getPublicKey()?.convertToHex()).toBe('6cffb0ee0b20dae828e46f2e003f78ac57b85e7268e587703932f06e1b2daee4') - }) -}) diff --git a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts b/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts deleted file mode 100644 index 883b214e8..000000000 --- a/dlt-connector/src/interactions/keyPairCalculation/KeyPairCalculation.context.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { KeyPairEd25519 } from 'gradido-blockchain-js' - -import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic' -import { KeyPairCacheManager } from '../../KeyPairCacheManager' -import { AccountKeyPairRole } from './AccountKeyPair.role' -import { ForeignCommunityKeyPairRole } from './ForeignCommunityKeyPair.role' -import { HomeCommunityKeyPairRole } from './HomeCommunityKeyPair.role' -import { LinkedTransactionKeyPairRole } from './LinkedTransactionKeyPair.role' -import { RemoteAccountKeyPairRole } from './RemoteAccountKeyPair.role' -import { UserKeyPairRole } from './UserKeyPair.role' - -/** - * @DCI-Context - * Context for calculating key pair for signing transactions - */ -export async function KeyPairCalculation(input: KeyPairIdentifierLogic): Promise { - const cache = KeyPairCacheManager.getInstance() - return await cache.getKeyPair(input.getKey(), async () => { - if (input.isSeedKeyPair()) { - return new LinkedTransactionKeyPairRole(input.getSeed()).generateKeyPair() - } - // If input does not belong to the home community, handle as remote key pair - if (cache.getHomeCommunityTopicId() !== input.getCommunityTopicId()) { - const role = input.isAccountKeyPair() - ? new RemoteAccountKeyPairRole(input.identifier) - : new ForeignCommunityKeyPairRole(input.getCommunityTopicId()) - return await role.retrieveKeyPair() - } - const communityKeyPair = await cache.getKeyPair(input.getCommunityKey(), async () => { - return new HomeCommunityKeyPairRole().generateKeyPair() - }) - if (!communityKeyPair) { - throw new Error("couldn't generate community key pair") - } - if (input.isCommunityKeyPair()) { - return communityKeyPair - } - const userKeyPair = await cache.getKeyPair(input.getCommunityUserKey(), async () => { - return new UserKeyPairRole(input.getUserUuid(), communityKeyPair).generateKeyPair() - }) - if (!userKeyPair) { - throw new Error("couldn't generate user key pair") - } - if (input.isUserKeyPair()) { - return userKeyPair - } - const accountNr = input.getAccountNr() - const accountKeyPair = new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair() - if (input.isAccountKeyPair()) { - return accountKeyPair - } - throw new Error("couldn't generate account key pair, unexpected type") - }) -} diff --git a/dlt-connector/src/interactions/keyPairCalculation/AbstractKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/AbstractKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/AbstractKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/AbstractKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/AbstractRemoteKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/AbstractRemoteKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/AbstractRemoteKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/AbstractRemoteKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/AccountKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/AccountKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/AccountKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/AccountKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/ForeignCommunityKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/ForeignCommunityKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/ForeignCommunityKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/ForeignCommunityKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/HomeCommunityKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/HomeCommunityKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/HomeCommunityKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/HomeCommunityKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/LinkedTransactionKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/LinkedTransactionKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/LinkedTransactionKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/LinkedTransactionKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/RemoteAccountKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/RemoteAccountKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/RemoteAccountKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/RemoteAccountKeyPair.role.ts diff --git a/dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.test.ts b/dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.test.ts new file mode 100644 index 000000000..6d208eaff --- /dev/null +++ b/dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.test.ts @@ -0,0 +1,84 @@ +import { afterAll, beforeAll, describe, expect, it, mock } from 'bun:test' +import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js' +import { parse } from 'valibot' +import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic' +import { KeyPairCacheManager } from '../../KeyPairCacheManager' +import { identifierKeyPairSchema } from '../../schemas/account.schema' +import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema' +import { ResolveKeyPair } from './ResolveKeyPair.context' + +mock.module('../../KeyPairCacheManager', () => { + let homeCommunityTopicId: HieroId | undefined + return { + KeyPairCacheManager: { + getInstance: () => ({ + setHomeCommunityTopicId: (topicId: HieroId) => { + homeCommunityTopicId = topicId + }, + getHomeCommunityTopicId: () => homeCommunityTopicId, + getKeyPair: (key: string, create: () => KeyPairEd25519) => { + return create() + }, + }), + }, + } +}) + +mock.module('../../config', () => ({ + CONFIG: { + HOME_COMMUNITY_SEED: MemoryBlock.fromHex( + '0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7', + ), + }, +})) + +const topicId = '0.0.21732' +const userUuid = 'aa25cf6f-2879-4745-b2ea-6d3c37fb44b0' + +afterAll(() => { + mock.restore() +}) + +describe('KeyPairCalculation', () => { + beforeAll(() => { + KeyPairCacheManager.getInstance().setHomeCommunityTopicId(parse(hieroIdSchema, '0.0.21732')) + }) + it('community key pair', async () => { + const identifier = new KeyPairIdentifierLogic( + parse(identifierKeyPairSchema, { communityTopicId: topicId }), + ) + const keyPair = await ResolveKeyPair(identifier) + expect(keyPair.getPublicKey()?.convertToHex()).toBe( + '7bcb0d0ad26d3f7ba597716c38a570220cece49b959e57927ee0c39a5a9c3adf', + ) + }) + it('user key pair', async () => { + const identifier = new KeyPairIdentifierLogic( + parse(identifierKeyPairSchema, { + communityTopicId: topicId, + account: { userUuid }, + }), + ) + expect(identifier.isAccountKeyPair()).toBe(false) + expect(identifier.isUserKeyPair()).toBe(true) + const keyPair = await ResolveKeyPair(identifier) + expect(keyPair.getPublicKey()?.convertToHex()).toBe( + 'd61ae86c262fc0b5d763a8f41a03098fae73a7649a62aac844378a0eb0055921', + ) + }) + + it('account key pair', async () => { + const identifier = new KeyPairIdentifierLogic( + parse(identifierKeyPairSchema, { + communityTopicId: topicId, + account: { userUuid, accountNr: 1 }, + }), + ) + expect(identifier.isAccountKeyPair()).toBe(true) + expect(identifier.isUserKeyPair()).toBe(false) + const keyPair = await ResolveKeyPair(identifier) + expect(keyPair.getPublicKey()?.convertToHex()).toBe( + '6cffb0ee0b20dae828e46f2e003f78ac57b85e7268e587703932f06e1b2daee4', + ) + }) +}) diff --git a/dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.ts b/dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.ts new file mode 100644 index 000000000..2fbdd906c --- /dev/null +++ b/dlt-connector/src/interactions/resolveKeyPair/ResolveKeyPair.context.ts @@ -0,0 +1,81 @@ +import { KeyPairEd25519 } from 'gradido-blockchain-js' + +import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic' +import { KeyPairCacheManager } from '../../KeyPairCacheManager' +import { AccountKeyPairRole } from './AccountKeyPair.role' +import { ForeignCommunityKeyPairRole } from './ForeignCommunityKeyPair.role' +import { HomeCommunityKeyPairRole } from './HomeCommunityKeyPair.role' +import { LinkedTransactionKeyPairRole } from './LinkedTransactionKeyPair.role' +import { RemoteAccountKeyPairRole } from './RemoteAccountKeyPair.role' +import { UserKeyPairRole } from './UserKeyPair.role' + +/** + * @DCI-Context + * Context for resolving the correct KeyPair for signing Gradido transactions. + * + * The context determines — based on the given {@link KeyPairIdentifierLogic} — + * which kind of KeyPair is required (community, user, account, remote, etc.). + * + * It first attempts to retrieve the KeyPair from the global {@link KeyPairCacheManager}. + * If no cached KeyPair exists, it dynamically generates or fetches it using the appropriate Role: + * - {@link LinkedTransactionKeyPairRole} for seed-based keys + * - {@link RemoteAccountKeyPairRole} or {@link ForeignCommunityKeyPairRole} for remote communities + * - {@link HomeCommunityKeyPairRole} for local community keys + * - {@link UserKeyPairRole} and {@link AccountKeyPairRole} for user and account levels + * + * Once generated, the KeyPair is stored in the cache for future reuse. + * + * @param input - Key pair identification logic containing all attributes + * (communityTopicId, userUuid, accountNr, seed, etc.) + * @returns The resolved {@link KeyPairEd25519} for the given input. + * + * @throws Error if the required KeyPair cannot be generated or resolved. + */ +export async function ResolveKeyPair(input: KeyPairIdentifierLogic): Promise { + const cache = KeyPairCacheManager.getInstance() + + return await cache.getKeyPair( + input.getKey(), + // function is called from cache manager, if key isn't currently cached + async () => { + // Seed (from linked transactions) + if (input.isSeedKeyPair()) { + return new LinkedTransactionKeyPairRole(input.getSeed()).generateKeyPair() + } + // Remote community branch + if (cache.getHomeCommunityTopicId() !== input.getCommunityTopicId()) { + const role = input.isAccountKeyPair() + ? new RemoteAccountKeyPairRole(input.identifier) + : new ForeignCommunityKeyPairRole(input.getCommunityTopicId()) + return await role.retrieveKeyPair() + } + // Community + const communityKeyPair = await cache.getKeyPair(input.getCommunityKey(), async () => { + return new HomeCommunityKeyPairRole().generateKeyPair() + }) + if (!communityKeyPair) { + throw new Error("couldn't generate community key pair") + } + if (input.isCommunityKeyPair()) { + return communityKeyPair + } + // User + const userKeyPair = await cache.getKeyPair(input.getCommunityUserKey(), async () => { + return new UserKeyPairRole(input.getUserUuid(), communityKeyPair).generateKeyPair() + }) + if (!userKeyPair) { + throw new Error("couldn't generate user key pair") + } + if (input.isUserKeyPair()) { + return userKeyPair + } + // Account + const accountNr = input.getAccountNr() + const accountKeyPair = new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair() + if (input.isAccountKeyPair()) { + return accountKeyPair + } + throw new Error("couldn't generate account key pair, unexpected type") + }, + ) +} diff --git a/dlt-connector/src/interactions/keyPairCalculation/UserKeyPair.role.ts b/dlt-connector/src/interactions/resolveKeyPair/UserKeyPair.role.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/UserKeyPair.role.ts rename to dlt-connector/src/interactions/resolveKeyPair/UserKeyPair.role.ts diff --git a/dlt-connector/src/interactions/keyPairCalculation/UserKeyPairRole.test.ts b/dlt-connector/src/interactions/resolveKeyPair/UserKeyPairRole.test.ts similarity index 100% rename from dlt-connector/src/interactions/keyPairCalculation/UserKeyPairRole.test.ts rename to dlt-connector/src/interactions/resolveKeyPair/UserKeyPairRole.test.ts diff --git a/dlt-connector/src/interactions/sendToHiero/CommunityRootTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/CommunityRootTransaction.role.ts index 34299683a..6207e28ec 100644 --- a/dlt-connector/src/interactions/sendToHiero/CommunityRootTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/CommunityRootTransaction.role.ts @@ -8,7 +8,7 @@ import { GMW_ACCOUNT_DERIVATION_INDEX, hardenDerivationIndex, } from '../../utils/derivationHelper' -import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' +import { ResolveKeyPair } from '../resolveKeyPair/ResolveKeyPair.context' import { AbstractTransactionRole } from './AbstractTransaction.role' export class CommunityRootTransactionRole extends AbstractTransactionRole { @@ -26,7 +26,7 @@ export class CommunityRootTransactionRole extends AbstractTransactionRole { public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() - const communityKeyPair = await KeyPairCalculation( + const communityKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic({ communityTopicId: this.community.hieroTopicId }), ) const gmwKeyPair = communityKeyPair.deriveChild( diff --git a/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts index fd70682ea..d11b031e2 100644 --- a/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/CreationTransaction.role.ts @@ -13,7 +13,7 @@ import { Transaction, } from '../../schemas/transaction.schema' import { HieroId } from '../../schemas/typeGuard.schema' -import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' +import { ResolveKeyPair } from '../resolveKeyPair/ResolveKeyPair.context' import { AbstractTransactionRole } from './AbstractTransaction.role' export class CreationTransactionRole extends AbstractTransactionRole { @@ -21,12 +21,7 @@ export class CreationTransactionRole extends AbstractTransactionRole { private readonly creationTransaction: CreationTransaction constructor(transaction: Transaction) { super() - try { - this.creationTransaction = parse(creationTransactionSchema, transaction) - } catch (error) { - console.error('creation: invalid transaction', JSON.stringify(error, null, 2)) - throw new Error('creation: invalid transaction') - } + this.creationTransaction = parse(creationTransactionSchema, transaction) this.homeCommunityTopicId = KeyPairCacheManager.getInstance().getHomeCommunityTopicId() if ( this.homeCommunityTopicId !== this.creationTransaction.user.communityTopicId || @@ -47,14 +42,14 @@ export class CreationTransactionRole extends AbstractTransactionRole { public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() // Recipient: user (account owner) - const recipientKeyPair = await KeyPairCalculation( + const recipientKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic(this.creationTransaction.user), ) // Signer: linkedUser (admin/moderator) - const signerKeyPair = await KeyPairCalculation( + const signerKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic(this.creationTransaction.linkedUser), ) - const homeCommunityKeyPair = await KeyPairCalculation( + const homeCommunityKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic({ communityTopicId: this.homeCommunityTopicId, }), diff --git a/dlt-connector/src/interactions/sendToHiero/DeferredTransferTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/DeferredTransferTransaction.role.ts index 36569d708..ac0b924f6 100644 --- a/dlt-connector/src/interactions/sendToHiero/DeferredTransferTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/DeferredTransferTransaction.role.ts @@ -7,14 +7,13 @@ import { } from 'gradido-blockchain-js' import { parse } from 'valibot' import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic' -import { IdentifierSeed, identifierSeedSchema } from '../../schemas/account.schema' import { DeferredTransferTransaction, deferredTransferTransactionSchema, Transaction, } from '../../schemas/transaction.schema' -import { HieroId } from '../../schemas/typeGuard.schema' -import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' +import { HieroId, IdentifierSeed, identifierSeedSchema } from '../../schemas/typeGuard.schema' +import { ResolveKeyPair } from '../resolveKeyPair/ResolveKeyPair.context' import { AbstractTransactionRole } from './AbstractTransaction.role' export class DeferredTransferTransactionRole extends AbstractTransactionRole { @@ -36,10 +35,10 @@ export class DeferredTransferTransactionRole extends AbstractTransactionRole { public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() - const senderKeyPair = await KeyPairCalculation( + const senderKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic(this.deferredTransferTransaction.user), ) - const recipientKeyPair = await KeyPairCalculation( + const recipientKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic({ communityTopicId: this.deferredTransferTransaction.linkedUser.communityTopicId, seed: this.seed, diff --git a/dlt-connector/src/interactions/sendToHiero/RedeemDeferredTransferTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/RedeemDeferredTransferTransaction.role.ts index a91990360..76d621762 100644 --- a/dlt-connector/src/interactions/sendToHiero/RedeemDeferredTransferTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/RedeemDeferredTransferTransaction.role.ts @@ -9,7 +9,7 @@ import { UserAccount, } from '../../schemas/transaction.schema' import { HieroId } from '../../schemas/typeGuard.schema' -import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' +import { ResolveKeyPair } from '../resolveKeyPair/ResolveKeyPair.context' import { AbstractTransactionRole } from './AbstractTransaction.role' export class RedeemDeferredTransferTransactionRole extends AbstractTransactionRole { @@ -34,7 +34,7 @@ export class RedeemDeferredTransferTransactionRole extends AbstractTransactionRo public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() - const senderKeyPair = await KeyPairCalculation( + const senderKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic(this.redeemDeferredTransferTransaction.user), ) const senderPublicKey = senderKeyPair.getPublicKey() @@ -56,7 +56,7 @@ export class RedeemDeferredTransferTransactionRole extends AbstractTransactionRo "redeem deferred transfer: couldn't deserialize deferred transfer from Gradido Node", ) } - const recipientKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(this.linkedUser)) + const recipientKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic(this.linkedUser)) builder .setCreatedAt(this.redeemDeferredTransferTransaction.createdAt) diff --git a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts index 032ec5eec..968a3c992 100644 --- a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts +++ b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.test.ts @@ -1,11 +1,9 @@ -import { describe, it, expect } from 'bun:test' -import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role' -import { parse } from 'valibot' -import { - transactionSchema, -} from '../../schemas/transaction.schema' -import { hieroIdSchema } from '../../schemas/typeGuard.schema' +import { describe, expect, it } from 'bun:test' import { InteractionToJson, InteractionValidate, ValidateType_SINGLE } from 'gradido-blockchain-js' +import { parse } from 'valibot' +import { transactionSchema } from '../../schemas/transaction.schema' +import { hieroIdSchema } from '../../schemas/typeGuard.schema' +import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role' const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec' const transaction = { @@ -21,15 +19,21 @@ const transaction = { createdAt: '2022-01-01T00:00:00.000Z', } -describe('RegisterAddressTransaction.role', () => { +describe('RegisterAddressTransaction.role', () => { it('get correct prepared builder', async () => { - const registerAddressTransactionRole = new RegisterAddressTransactionRole(parse(transactionSchema, transaction)) - expect(registerAddressTransactionRole.getSenderCommunityTopicId()).toBe(parse(hieroIdSchema, '0.0.21732')) - expect(() => registerAddressTransactionRole.getRecipientCommunityTopicId()).toThrow() - const builder = await registerAddressTransactionRole.getGradidoTransactionBuilder() - const gradidoTransaction = builder.build() - expect(() => new InteractionValidate(gradidoTransaction).run(ValidateType_SINGLE)).not.toThrow() - const json = JSON.parse(new InteractionToJson(gradidoTransaction).run()) - expect(json.bodyBytes.json.registerAddress.nameHash).toBe('bac2c06682808947f140d6766d02943761d4129ec055bb1f84dc3a4201a94c08') + const registerAddressTransactionRole = new RegisterAddressTransactionRole( + parse(transactionSchema, transaction), + ) + expect(registerAddressTransactionRole.getSenderCommunityTopicId()).toBe( + parse(hieroIdSchema, '0.0.21732'), + ) + expect(() => registerAddressTransactionRole.getRecipientCommunityTopicId()).toThrow() + const builder = await registerAddressTransactionRole.getGradidoTransactionBuilder() + const gradidoTransaction = builder.build() + expect(() => new InteractionValidate(gradidoTransaction).run(ValidateType_SINGLE)).not.toThrow() + const json = JSON.parse(new InteractionToJson(gradidoTransaction).run()) + expect(json.bodyBytes.json.registerAddress.nameHash).toBe( + 'bac2c06682808947f140d6766d02943761d4129ec055bb1f84dc3a4201a94c08', + ) }) -}) \ No newline at end of file +}) diff --git a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts index 32f78bac1..8f0f50e6b 100644 --- a/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/RegisterAddressTransaction.role.ts @@ -12,7 +12,7 @@ import { Transaction, } from '../../schemas/transaction.schema' import { HieroId } from '../../schemas/typeGuard.schema' -import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' +import { ResolveKeyPair } from '../resolveKeyPair/ResolveKeyPair.context' import { AbstractTransactionRole } from './AbstractTransaction.role' export class RegisterAddressTransactionRole extends AbstractTransactionRole { @@ -35,15 +35,13 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRole { public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() const communityTopicId = this.registerAddressTransaction.user.communityTopicId - const communityKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic({ communityTopicId })) + const communityKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic({ communityTopicId })) const keyPairIdentifier = this.registerAddressTransaction.user // when accountNr is 0 it is the user account keyPairIdentifier.account.accountNr = 0 - const userKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(keyPairIdentifier)) + const userKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic(keyPairIdentifier)) keyPairIdentifier.account.accountNr = 1 - const accountKeyPair = await KeyPairCalculation( - new KeyPairIdentifierLogic(keyPairIdentifier), - ) + const accountKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic(keyPairIdentifier)) builder .setCreatedAt(this.registerAddressTransaction.createdAt) diff --git a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts index 5061233be..e3ab0b7ed 100644 --- a/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts +++ b/dlt-connector/src/interactions/sendToHiero/SendToHiero.context.ts @@ -1,24 +1,25 @@ import { GradidoTransaction, + HieroTransactionId, + InteractionSerialize, InteractionValidate, - MemoryBlock, ValidateType_SINGLE, } from 'gradido-blockchain-js' import { getLogger } from 'log4js' -import { parse, safeParse } from 'valibot' +import * as v from 'valibot' import { HieroClient } from '../../client/hiero/HieroClient' import { LOG4JS_BASE_CATEGORY } from '../../config/const' import { InputTransactionType } from '../../enum/InputTransactionType' import { - Community, + CommunityInput, communitySchema, - Transaction, + TransactionInput, transactionSchema, } from '../../schemas/transaction.schema' import { HieroId, - HieroTransactionId, - hieroTransactionIdSchema, + HieroTransactionIdString, + hieroTransactionIdStringSchema, } from '../../schemas/typeGuard.schema' import { AbstractTransactionRole } from './AbstractTransaction.role' import { CommunityRootTransactionRole } from './CommunityRootTransaction.role' @@ -36,70 +37,99 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.interactions.sendToHiero.SendT * send every transaction only once to hiero! */ export async function SendToHieroContext( - input: Transaction | Community, -): Promise { - // let gradido blockchain validator run, it will throw an exception when something is wrong - const validate = (transaction: GradidoTransaction): void => { - const validator = new InteractionValidate(transaction) - validator.run(ValidateType_SINGLE) - } - - // send transaction as hiero topic message - const sendViaHiero = async ( - gradidoTransaction: GradidoTransaction, - topic: HieroId, - ): Promise => { - const client = HieroClient.getInstance() - const transactionId = await client.sendMessage(topic, gradidoTransaction) - if (!transactionId) { - throw new Error('missing transaction id from hiero') - } - logger.info('transmitted Gradido Transaction to Hiero', { transactionId: transactionId.toString() }) - return transactionId.toString() - } - - // choose correct role based on transaction type and input type - const chooseCorrectRole = (input: Transaction | Community): AbstractTransactionRole => { - const communityParsingResult = safeParse(communitySchema, input) - if (communityParsingResult.success) { - return new CommunityRootTransactionRole(communityParsingResult.output) - } - - const transaction = input as Transaction - switch (transaction.type) { - case InputTransactionType.GRADIDO_CREATION: - return new CreationTransactionRole(transaction) - case InputTransactionType.GRADIDO_TRANSFER: - return new TransferTransactionRole(transaction) - case InputTransactionType.REGISTER_ADDRESS: - return new RegisterAddressTransactionRole(transaction) - case InputTransactionType.GRADIDO_DEFERRED_TRANSFER: - return new DeferredTransferTransactionRole(transaction) - case InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER: - return new RedeemDeferredTransferTransactionRole(transaction) - default: - throw new Error('not supported transaction type: ' + transaction.type) - } - } - + input: TransactionInput | CommunityInput, +): Promise { const role = chooseCorrectRole(input) const builder = await role.getGradidoTransactionBuilder() if (builder.isCrossCommunityTransaction()) { + // build cross group transaction const outboundTransaction = builder.buildOutbound() validate(outboundTransaction) - const outboundHieroTransactionId = await sendViaHiero( + + // send outbound transaction to hiero at first, because we need the transaction id for inbound transaction + const outboundHieroTransactionIdString = await sendViaHiero( outboundTransaction, role.getSenderCommunityTopicId(), ) - builder.setParentMessageId(MemoryBlock.createPtr(new MemoryBlock(outboundHieroTransactionId))) + + // serialize Hiero transaction ID and attach it to the builder for the inbound transaction + const transactionIdSerializer = new InteractionSerialize( + new HieroTransactionId(outboundHieroTransactionIdString), + ) + builder.setParentMessageId(transactionIdSerializer.run()) + + // build and validate inbound transaction const inboundTransaction = builder.buildInbound() validate(inboundTransaction) + + // send inbound transaction to hiero await sendViaHiero(inboundTransaction, role.getRecipientCommunityTopicId()) - return parse(hieroTransactionIdSchema, outboundHieroTransactionId) + return outboundHieroTransactionIdString } else { + // build and validate local transaction const transaction = builder.build() validate(transaction) - const hieroTransactionId = await sendViaHiero(transaction, role.getSenderCommunityTopicId()) - return parse(hieroTransactionIdSchema, hieroTransactionId) + + // send transaction to hiero + const hieroTransactionIdString = await sendViaHiero( + transaction, + role.getSenderCommunityTopicId(), + ) + return hieroTransactionIdString + } +} + +// let gradido blockchain validator run, it will throw an exception when something is wrong +function validate(transaction: GradidoTransaction): void { + const validator = new InteractionValidate(transaction) + validator.run(ValidateType_SINGLE) +} + +// send transaction as hiero topic message +async function sendViaHiero( + gradidoTransaction: GradidoTransaction, + topic: HieroId, +): Promise { + const client = HieroClient.getInstance() + const transactionId = await client.sendMessage(topic, gradidoTransaction) + if (!transactionId) { + throw new Error('missing transaction id from hiero') + } + logger.info('transmitted Gradido Transaction to Hiero', { + transactionId: transactionId.toString(), + }) + return v.parse(hieroTransactionIdStringSchema, transactionId.toString()) +} + +// choose correct role based on transaction type and input type +function chooseCorrectRole(input: TransactionInput | CommunityInput): AbstractTransactionRole { + const communityParsingResult = v.safeParse(communitySchema, input) + if (communityParsingResult.success) { + return new CommunityRootTransactionRole(communityParsingResult.output) + } + + const transactionParsingResult = v.safeParse(transactionSchema, input) + if (!transactionParsingResult.success) { + logger.error("error validating transaction, doesn't match any schema", { + transactionSchema: v.flatten(transactionParsingResult.issues), + communitySchema: v.flatten(communityParsingResult.issues), + }) + throw new Error('invalid input') + } + + const transaction = transactionParsingResult.output + switch (transaction.type) { + case InputTransactionType.GRADIDO_CREATION: + return new CreationTransactionRole(transaction) + case InputTransactionType.GRADIDO_TRANSFER: + return new TransferTransactionRole(transaction) + case InputTransactionType.REGISTER_ADDRESS: + return new RegisterAddressTransactionRole(transaction) + case InputTransactionType.GRADIDO_DEFERRED_TRANSFER: + return new DeferredTransferTransactionRole(transaction) + case InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER: + return new RedeemDeferredTransferTransactionRole(transaction) + default: + throw new Error('not supported transaction type: ' + transaction.type) } } diff --git a/dlt-connector/src/interactions/sendToHiero/TransferTransaction.role.ts b/dlt-connector/src/interactions/sendToHiero/TransferTransaction.role.ts index 473bfbc1e..03d1480b9 100644 --- a/dlt-connector/src/interactions/sendToHiero/TransferTransaction.role.ts +++ b/dlt-connector/src/interactions/sendToHiero/TransferTransaction.role.ts @@ -12,7 +12,7 @@ import { transferTransactionSchema, } from '../../schemas/transaction.schema' import { HieroId } from '../../schemas/typeGuard.schema' -import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context' +import { ResolveKeyPair } from '../resolveKeyPair/ResolveKeyPair.context' import { AbstractTransactionRole } from './AbstractTransaction.role' export class TransferTransactionRole extends AbstractTransactionRole { @@ -33,11 +33,11 @@ export class TransferTransactionRole extends AbstractTransactionRole { public async getGradidoTransactionBuilder(): Promise { const builder = new GradidoTransactionBuilder() // sender + signer - const senderKeyPair = await KeyPairCalculation( + const senderKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic(this.transferTransaction.user), ) // recipient - const recipientKeyPair = await KeyPairCalculation( + const recipientKeyPair = await ResolveKeyPair( new KeyPairIdentifierLogic(this.transferTransaction.linkedUser), ) diff --git a/dlt-connector/src/schemas/account.schema.ts b/dlt-connector/src/schemas/account.schema.ts index d1dbb4802..e328a2f90 100644 --- a/dlt-connector/src/schemas/account.schema.ts +++ b/dlt-connector/src/schemas/account.schema.ts @@ -1,12 +1,5 @@ import * as v from 'valibot' -import { hieroIdSchema, uuidv4Schema } from './typeGuard.schema' - -// use code from transaction links -export const identifierSeedSchema = v.object({ - seed: v.pipe(v.string('expect string type'), v.length(24, 'expect seed length 24')), -}) - -export type IdentifierSeed = v.InferOutput +import { hieroIdSchema, identifierSeedSchema, uuidv4Schema } from './typeGuard.schema' // identifier for gradido community accounts, inside a community export const identifierCommunityAccountSchema = v.object({ diff --git a/dlt-connector/src/schemas/transaction.schema.test.ts b/dlt-connector/src/schemas/transaction.schema.test.ts index 7d204dba5..de87cacfd 100644 --- a/dlt-connector/src/schemas/transaction.schema.test.ts +++ b/dlt-connector/src/schemas/transaction.schema.test.ts @@ -1,23 +1,28 @@ import { beforeAll, describe, expect, it } from 'bun:test' -import { TypeBoxFromValibot } from '@sinclair/typemap' import { TypeCompiler } from '@sinclair/typebox/compiler' +import { TypeBoxFromValibot } from '@sinclair/typemap' import { randomBytes } from 'crypto' +import { AddressType_COMMUNITY_HUMAN } from 'gradido-blockchain-js' import { v4 as uuidv4 } from 'uuid' import { parse } from 'valibot' +import { AccountType } from '../enum/AccountType' import { InputTransactionType } from '../enum/InputTransactionType' import { gradidoAmountSchema, HieroId, hieroIdSchema, + identifierSeedSchema, Memo, memoSchema, timeoutDurationSchema, Uuidv4, uuidv4Schema, } from '../schemas/typeGuard.schema' -import { registerAddressTransactionSchema, TransactionInput, transactionSchema } from './transaction.schema' -import { AccountType } from '../enum/AccountType' -import { AddressType_COMMUNITY_HUMAN } from 'gradido-blockchain-js' +import { + registerAddressTransactionSchema, + TransactionInput, + transactionSchema, +} from './transaction.schema' const transactionLinkCode = (date: Date): string => { const time = date.getTime().toString(16) @@ -91,7 +96,7 @@ describe('transaction schemas', () => { expect(check.Check(registerAddress)).toBe(true) }) }) - + it('valid, gradido transfer', () => { const gradidoTransfer: TransactionInput = { user: { @@ -162,6 +167,8 @@ describe('transaction schemas', () => { }) }) it('valid, gradido transaction link / deferred transfer', () => { + const seed = transactionLinkCode(new Date()) + const seedParsed = parse(identifierSeedSchema, seed) const gradidoTransactionLink: TransactionInput = { user: { communityTopicId: topicString, @@ -171,9 +178,7 @@ describe('transaction schemas', () => { }, linkedUser: { communityTopicId: topicString, - seed: { - seed: transactionLinkCode(new Date()), - }, + seed, }, amount: '100', memo: memoString, @@ -191,9 +196,7 @@ describe('transaction schemas', () => { }, linkedUser: { communityTopicId: topic, - seed: { - seed: gradidoTransactionLink.linkedUser!.seed!.seed, - }, + seed: seedParsed, }, amount: parse(gradidoAmountSchema, gradidoTransactionLink.amount!), memo, diff --git a/dlt-connector/src/schemas/transaction.schema.ts b/dlt-connector/src/schemas/transaction.schema.ts index 7c1855ec8..dea8c48dc 100644 --- a/dlt-connector/src/schemas/transaction.schema.ts +++ b/dlt-connector/src/schemas/transaction.schema.ts @@ -1,19 +1,16 @@ import * as v from 'valibot' +import { AccountType } from '../enum/AccountType' import { InputTransactionType } from '../enum/InputTransactionType' -import { - identifierAccountSchema, - identifierCommunityAccountSchema, - identifierSeedSchema, -} from './account.schema' +import { identifierAccountSchema, identifierCommunityAccountSchema } from './account.schema' import { addressTypeSchema, dateSchema } from './typeConverter.schema' import { gradidoAmountSchema, hieroIdSchema, + identifierSeedSchema, memoSchema, timeoutDurationSchema, uuidv4Schema, } from './typeGuard.schema' -import { AccountType } from '../enum/AccountType' /** * Schema for community, for creating new CommunityRoot Transaction on gradido blockchain diff --git a/dlt-connector/src/schemas/typeConverter.schema.test.ts b/dlt-connector/src/schemas/typeConverter.schema.test.ts index dd8944247..2caa499b5 100644 --- a/dlt-connector/src/schemas/typeConverter.schema.test.ts +++ b/dlt-connector/src/schemas/typeConverter.schema.test.ts @@ -1,7 +1,7 @@ -import { Static, TypeBoxFromValibot } from '@sinclair/typemap' -import { TypeCompiler } from '@sinclair/typebox/compiler' // only for IDE, bun don't need this to work import { describe, expect, it } from 'bun:test' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { Static, TypeBoxFromValibot } from '@sinclair/typemap' import { AddressType_COMMUNITY_AUF } from 'gradido-blockchain-js' import * as v from 'valibot' import { AccountType } from '../enum/AccountType' @@ -26,25 +26,25 @@ describe('basic.schema', () => { expect(() => v.parse(dateSchema, 'invalid date')).toThrow(new Error('invalid date')) }) it('with type box', () => { - // Derive TypeBox Schema from the Valibot Schema - const DateSchema = TypeBoxFromValibot(dateSchema) + // Derive TypeBox Schema from the Valibot Schema + const DateSchema = TypeBoxFromValibot(dateSchema) - // Build the compiler - const check = TypeCompiler.Compile(DateSchema) - - // Valid value (String) - expect(check.Check('2021-01-01T10:10:00.000Z')).toBe(true) - - // typebox cannot use valibot custom validation and transformations, it will check only the input types - expect(check.Check('invalid date')).toBe(true) - - // Type inference (TypeScript) - type DateType = Static - const validDate: DateType = '2021-01-01T10:10:00.000Z' - const validDate2: DateType = new Date('2021-01-01') + // Build the compiler + const check = TypeCompiler.Compile(DateSchema) - // @ts-expect-error - const invalidDate: DateType = 123 // should fail in TS + // Valid value (String) + expect(check.Check('2021-01-01T10:10:00.000Z')).toBe(true) + + // typebox cannot use valibot custom validation and transformations, it will check only the input types + expect(check.Check('invalid date')).toBe(true) + + // Type inference (TypeScript) + type DateType = Static + const _validDate: DateType = '2021-01-01T10:10:00.000Z' + const _validDate2: DateType = new Date('2021-01-01') + + // @ts-expect-error + const _invalidDate: DateType = 123 // should fail in TS }) }) @@ -74,16 +74,24 @@ describe('basic.schema', () => { const check = TypeCompiler.Compile(AddressTypeSchema) expect(check.Check(AccountType.COMMUNITY_AUF)).toBe(true) // type box will throw an error, because it cannot handle valibots custom validation - expect(() => check.Check(AddressType_COMMUNITY_AUF)).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) - expect(() => check.Check('invalid')).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) + expect(() => check.Check(AddressType_COMMUNITY_AUF)).toThrow( + new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`), + ) + expect(() => check.Check('invalid')).toThrow( + new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`), + ) }) it('accountType with type box', () => { const AccountTypeSchema = TypeBoxFromValibot(accountTypeSchema) const check = TypeCompiler.Compile(AccountTypeSchema) expect(check.Check(AccountType.COMMUNITY_AUF)).toBe(true) // type box will throw an error, because it cannot handle valibots custom validation - expect(() => check.Check(AddressType_COMMUNITY_AUF)).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) - expect(() => check.Check('invalid')).toThrow(new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`)) + expect(() => check.Check(AddressType_COMMUNITY_AUF)).toThrow( + new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`), + ) + expect(() => check.Check('invalid')).toThrow( + new TypeError(`undefined is not an object (evaluating 'schema["~run"]')`), + ) }) }) diff --git a/dlt-connector/src/schemas/typeGuard.schema.ts b/dlt-connector/src/schemas/typeGuard.schema.ts index d580abe3a..3a93ac62f 100644 --- a/dlt-connector/src/schemas/typeGuard.schema.ts +++ b/dlt-connector/src/schemas/typeGuard.schema.ts @@ -39,6 +39,24 @@ export const uuidv4Schema = v.pipe( export type Uuidv4Input = v.InferInput +/** + * type guard for seed string + * create with `v.parse(seedSchema, '0c4676adfd96519a0551596c')` + * seed is a string of length 24 + */ +declare const validIdentifierSeed: unique symbol +export type IdentifierSeed = string & { [validIdentifierSeed]: true } + +// use code from transaction links +export const identifierSeedSchema = v.pipe( + v.string('expect string type'), + v.hexadecimal('expect hexadecimal string'), + v.length(24, 'expect seed length 24'), + v.transform((input: string) => input as IdentifierSeed), +) + +export type IdentifierSeedInput = v.InferInput + /** * type guard for memory block size 32 * create with `v.parse(memoryBlock32Schema, MemoryBlock.fromHex('39568d7e148a0afee7f27a67dbf7d4e87d1fdec958e2680df98a469690ffc1a2'))` @@ -124,16 +142,20 @@ export type HieroIdInput = v.InferInput * basically it is a Hiero id with a timestamp seconds-nanoseconds since 1970-01-01T00:00:00Z * seconds is int64, nanoseconds int32 */ -declare const validHieroTransactionId: unique symbol -export type HieroTransactionId = string & { [validHieroTransactionId]: true } +declare const validHieroTransactionIdString: unique symbol +export type HieroTransactionIdString = string & { [validHieroTransactionIdString]: true } -export const hieroTransactionIdSchema = v.pipe( - v.string('expect hiero transaction id type, for example 0.0.141760-1755138896-607329203 or 0.0.141760@1755138896.607329203'), +export const hieroTransactionIdStringSchema = v.pipe( + v.string( + 'expect hiero transaction id type, for example 0.0.141760-1755138896-607329203 or 0.0.141760@1755138896.607329203', + ), v.regex(/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+-[0-9]+|@[0-9]+\.[0-9]+)$/), - v.transform((input: string) => input as HieroTransactionId), + v.transform( + (input: string) => input as HieroTransactionIdString, + ), ) -export type HieroTransactionIdInput = v.InferInput +export type HieroTransactionIdInput = v.InferInput /** * type guard for memo @@ -176,14 +198,12 @@ export const timeoutDurationSchema = v.pipe( ), v.instance(DurationSeconds, 'expect DurationSeconds type'), ]), - v.transform( - (input: number | DurationSeconds) => { - if (input instanceof DurationSeconds) { - return input as TimeoutDuration - } - return new DurationSeconds(input) as TimeoutDuration - }, - ), + v.transform((input: number | DurationSeconds) => { + if (input instanceof DurationSeconds) { + return input as TimeoutDuration + } + return new DurationSeconds(input) as TimeoutDuration + }), ) /** @@ -210,16 +230,11 @@ declare const validGradidoAmount: unique symbol export type GradidoAmount = GradidoUnit & { [validGradidoAmount]: true } export const gradidoAmountSchema = v.pipe( - v.union([ - amountSchema, - v.instance(GradidoUnit, 'expect GradidoUnit type'), - ]), - v.transform( - (input: Amount | GradidoUnit) => { - if (input instanceof GradidoUnit) { - return input as GradidoAmount - } - return GradidoUnit.fromString(input) as GradidoAmount - }, - ), + v.union([amountSchema, v.instance(GradidoUnit, 'expect GradidoUnit type')]), + v.transform((input: Amount | GradidoUnit) => { + if (input instanceof GradidoUnit) { + return input as GradidoAmount + } + return GradidoUnit.fromString(input) as GradidoAmount + }), ) diff --git a/dlt-connector/src/server/index.test.ts b/dlt-connector/src/server/index.test.ts index f3a9aed7d..3fa0ce07b 100644 --- a/dlt-connector/src/server/index.test.ts +++ b/dlt-connector/src/server/index.test.ts @@ -1,10 +1,10 @@ -import { appRoutes } from '.' -import { describe, it, expect, beforeAll, mock } from 'bun:test' -import { KeyPairCacheManager } from '../KeyPairCacheManager' -import { hieroIdSchema } from '../schemas/typeGuard.schema' -import { parse } from 'valibot' -import { HieroId } from '../schemas/typeGuard.schema' +import { beforeAll, describe, expect, it, mock } from 'bun:test' +import { AccountId, Timestamp, TransactionId } from '@hashgraph/sdk' import { GradidoTransaction, KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js' +import { parse } from 'valibot' +import { KeyPairCacheManager } from '../KeyPairCacheManager' +import { HieroId, hieroIdSchema } from '../schemas/typeGuard.schema' +import { appRoutes } from '.' const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec' @@ -29,7 +29,7 @@ mock.module('../client/hiero/HieroClient', () => ({ HieroClient: { getInstance: () => ({ sendMessage: (topicId: HieroId, transaction: GradidoTransaction) => { - return { receipt: { status: '0.0.21732' }, response: { transactionId: '0.0.6566984@1758029639.561157605' } } + return new TransactionId(new AccountId(0, 0, 6566984), new Timestamp(1758029639, 561157605)) }, }), }, @@ -37,7 +37,9 @@ mock.module('../client/hiero/HieroClient', () => ({ mock.module('../config', () => ({ CONFIG: { - HOME_COMMUNITY_SEED: MemoryBlock.fromHex('0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7'), + HOME_COMMUNITY_SEED: MemoryBlock.fromHex( + '0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7', + ), }, })) @@ -59,17 +61,22 @@ describe('Server', () => { accountType: 'COMMUNITY_HUMAN', createdAt: '2022-01-01T00:00:00.000Z', } - const response = await appRoutes.handle(new Request('http://localhost/sendTransaction', { - method: 'POST', - body: JSON.stringify(transaction), - headers: { - 'Content-Type': 'application/json', - }, - })) + const response = await appRoutes.handle( + new Request('http://localhost/sendTransaction', { + method: 'POST', + body: JSON.stringify(transaction), + headers: { + 'Content-Type': 'application/json', + }, + }), + ) if (response.status !== 200) { + // biome-ignore lint/suspicious/noConsole: helper for debugging if test fails console.log(await response.text()) } expect(response.status).toBe(200) - expect(await response.text()).toBe('0.0.6566984@1758029639.561157605') + expect(await response.json()).toMatchObject({ + transactionId: '0.0.6566984@1758029639.561157605', + }) }) }) diff --git a/dlt-connector/src/server/index.ts b/dlt-connector/src/server/index.ts index b8aa44fd3..7168a76dc 100644 --- a/dlt-connector/src/server/index.ts +++ b/dlt-connector/src/server/index.ts @@ -1,92 +1,147 @@ import { TypeBoxFromValibot } from '@sinclair/typemap' -import { Type } from '@sinclair/typebox' import { Elysia, status, t } from 'elysia' import { AddressType_NONE } from 'gradido-blockchain-js' import { getLogger } from 'log4js' -import { parse } from 'valibot' +import * as v from 'valibot' import { GradidoNodeClient } from '../client/GradidoNode/GradidoNodeClient' import { LOG4JS_BASE_CATEGORY } from '../config/const' import { KeyPairIdentifierLogic } from '../data/KeyPairIdentifier.logic' -import { KeyPairCalculation } from '../interactions/keyPairCalculation/KeyPairCalculation.context' +import { ResolveKeyPair } from '../interactions/resolveKeyPair/ResolveKeyPair.context' import { SendToHieroContext } from '../interactions/sendToHiero/SendToHiero.context' -import { IdentifierAccount, identifierAccountSchema } from '../schemas/account.schema' +import { IdentifierAccountInput, identifierAccountSchema } from '../schemas/account.schema' import { transactionSchema } from '../schemas/transaction.schema' -import { hieroIdSchema, hieroTransactionIdSchema } from '../schemas/typeGuard.schema' +import { hieroTransactionIdStringSchema } from '../schemas/typeGuard.schema' import { - accountIdentifierSeedSchema, - accountIdentifierUserSchema, - existSchema, + accountIdentifierSeedTypeBoxSchema, + accountIdentifierUserTypeBoxSchema, + existTypeBoxSchema, } from './input.schema' const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.server`) +/** + * To define a route in Elysia: + * + * 1. Choose the HTTP method: get, post, patch, put, or delete. + * + * 2. Define the route path: + * - **Params**: values inside the path. + * Example: path: `/isCommunityExist/:communityTopicId` + * → called with: GET `/isCommunityExist/0.0.21732` + * + * - **Query**: values in the query string. + * Example: path: `/isCommunityExist` + * → called with: GET `/isCommunityExist?communityTopicId=0.0.21732` + * + * 3. Write the route handler: + * Return a JSON object — often by calling your business logic. + * + * 4. Define validation schemas using TypeBoxFromValibot: + * - `params` (for path parameters) + * - `query` (for query strings) + * - `body` (for POST/PUT/PATCH requests) + * - `response` (for output) + * + * Example: + * .get( + * '/isCommunityExist/:communityTopicId', + * async ({ params: { communityTopicId } }) => ({ + * exists: await isCommunityExist({ communityTopicId }) + * }), + * { + * params: t.Object({ communityTopicId: TypeBoxFromValibot(hieroIdSchema) }), + * response: t.Object({ exists: t.Boolean() }), + * }, + * ) + * + * 🔗 More info: https://elysiajs.com/at-glance.html + */ export const appRoutes = new Elysia() + // check if account exists by user, call example: + // GET /isAccountExist/by-user/0.0.21732/408780b2-59b3-402a-94be-56a4f4f4e8ec/0 .get( '/isAccountExist/by-user/:communityTopicId/:userUuid/:accountNr', - async ({ params: { communityTopicId, userUuid, accountNr } }) => { - const accountIdentifier = parse(identifierAccountSchema, { + async ({ params: { communityTopicId, userUuid, accountNr } }) => ({ + exists: await isAccountExist({ communityTopicId, account: { userUuid, accountNr }, - }) - return { exists: await isAccountExist(accountIdentifier) } + }), + }), + { + params: accountIdentifierUserTypeBoxSchema, + response: existTypeBoxSchema, }, - // validation schemas - { params: accountIdentifierUserSchema, response: existSchema }, ) + // check if account exists by seed, call example: + // GET /isAccountExist/by-seed/0.0.21732/0c4676adfd96519a0551596c .get( '/isAccountExist/by-seed/:communityTopicId/:seed', - async ({ params: { communityTopicId, seed } }) => { - const accountIdentifier = parse(identifierAccountSchema, { + async ({ params: { communityTopicId, seed } }) => ({ + exists: await isAccountExist({ communityTopicId, - seed: { seed }, - }) - return { exists: await isAccountExist(accountIdentifier) } + seed, + }), + }), + { + params: accountIdentifierSeedTypeBoxSchema, + response: existTypeBoxSchema, }, - // validation schemas - { params: accountIdentifierSeedSchema, response: existSchema }, ) + // send transaction to hiero, call example for send transaction: + // POST /sendTransaction + // body: { + // user: { + // communityTopicId: '0.0.21732', + // account: { + // userUuid: '408780b2-59b3-402a-94be-56a4f4f4e8ec', + // accountNr: 0, + // }, + // }, + // linkedUser: { + // communityTopicId: '0.0.21732', + // account: { + // userUuid: '10689787-00fe-4295-a996-05c0952558d9', + // accountNr: 0, + // }, + // }, + // amount: 10, + // memo: 'test', + // type: 'TRANSFER', + // createdAt: '2022-01-01T00:00:00.000Z', + // } .post( '/sendTransaction', - async ({ body }) => { - try { - const hieroTransactionId = await SendToHieroContext(parse(transactionSchema, body)) - console.log('server will return:', hieroTransactionId) - return { transactionId: hieroTransactionId } - } catch (e) { - if (e instanceof TypeError) { - console.log(`message: ${e.message}, stack: ${e.stack}`) - } - console.log(e) - throw status(500, e) - } - }, - // validation schemas + async ({ body }) => ({ + transactionId: await SendToHieroContext(body), + }), { body: TypeBoxFromValibot(transactionSchema), - response: t.Object({ transactionId: TypeBoxFromValibot(hieroTransactionIdSchema) }), + response: t.Object({ transactionId: TypeBoxFromValibot(hieroTransactionIdStringSchema) }), }, ) -async function isAccountExist(identifierAccount: IdentifierAccount): Promise { +// function stay here for now because it is small and simple, but maybe later if more functions are added, move it to a separate file +async function isAccountExist(identifierAccount: IdentifierAccountInput): Promise { + // check and prepare input const startTime = Date.now() - const accountKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(identifierAccount)) + const identifierAccountParsed = v.parse(identifierAccountSchema, identifierAccount) + const accountKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic(identifierAccountParsed)) const publicKey = accountKeyPair.getPublicKey() if (!publicKey) { - throw status(404, "couldn't calculate account key pair") + throw status(404, { message: "couldn't calculate account key pair" }) } // ask gradido node server for account type, if type !== NONE account exist const addressType = await GradidoNodeClient.getInstance().getAddressType( publicKey.convertToHex(), - identifierAccount.communityTopicId, + identifierAccountParsed.communityTopicId, ) + const exists = addressType !== AddressType_NONE const endTime = Date.now() - logger.info( - `isAccountExist: ${addressType !== AddressType_NONE}, time used: ${endTime - startTime}ms`, - ) + logger.info(`isAccountExist: ${exists}, time used: ${endTime - startTime}ms`) if (logger.isDebugEnabled()) { - logger.debug('params', identifierAccount) + logger.debug('params', identifierAccountParsed) } - return addressType !== AddressType_NONE + return exists } -export type DltRoutes = typeof appRoutes \ No newline at end of file +export type DltRoutes = typeof appRoutes diff --git a/dlt-connector/src/server/input.schema.ts b/dlt-connector/src/server/input.schema.ts index 336ade786..515e5b35f 100644 --- a/dlt-connector/src/server/input.schema.ts +++ b/dlt-connector/src/server/input.schema.ts @@ -2,18 +2,18 @@ import { TypeBoxFromValibot } from '@sinclair/typemap' import { t } from 'elysia' import { hieroIdSchema, uuidv4Schema } from '../schemas/typeGuard.schema' -export const accountIdentifierUserSchema = t.Object({ +export const accountIdentifierUserTypeBoxSchema = t.Object({ communityTopicId: TypeBoxFromValibot(hieroIdSchema), userUuid: TypeBoxFromValibot(uuidv4Schema), accountNr: t.Number({ min: 0 }), }) // identifier for a gradido account created by transaction link / deferred transfer -export const accountIdentifierSeedSchema = t.Object({ +export const accountIdentifierSeedTypeBoxSchema = t.Object({ communityTopicId: TypeBoxFromValibot(hieroIdSchema), seed: TypeBoxFromValibot(uuidv4Schema), }) -export const existSchema = t.Object({ +export const existTypeBoxSchema = t.Object({ exists: t.Boolean(), })