fix test, refactor

This commit is contained in:
einhornimmond 2025-10-21 11:56:31 +02:00
parent 4658f33b88
commit ed94bb7ea0
36 changed files with 617 additions and 469 deletions

View File

@ -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 ', () => {

View File

@ -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) => {

View File

@ -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)

View File

@ -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<typeof configSchema>
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)
}

View File

@ -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, '')

View File

@ -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'
}
}

View File

@ -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)
})

View File

@ -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')
})
})

View File

@ -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<KeyPairEd25519> {
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")
})
}

View File

@ -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',
)
})
})

View File

@ -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<KeyPairEd25519> {
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")
},
)
}

View File

@ -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<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const communityKeyPair = await KeyPairCalculation(
const communityKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic({ communityTopicId: this.community.hieroTopicId }),
)
const gmwKeyPair = communityKeyPair.deriveChild(

View File

@ -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<GradidoTransactionBuilder> {
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,
}),

View File

@ -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<GradidoTransactionBuilder> {
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,

View File

@ -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<GradidoTransactionBuilder> {
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)

View File

@ -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',
)
})
})
})

View File

@ -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<GradidoTransactionBuilder> {
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)

View File

@ -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<HieroTransactionId> {
// 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<string> => {
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<HieroTransactionIdString> {
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<HieroTransactionIdString> {
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<typeof transactionSchema>(transactionParsingResult.issues),
communitySchema: v.flatten<typeof communitySchema>(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)
}
}

View File

@ -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<GradidoTransactionBuilder> {
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),
)

View File

@ -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<typeof identifierSeedSchema>
import { hieroIdSchema, identifierSeedSchema, uuidv4Schema } from './typeGuard.schema'
// identifier for gradido community accounts, inside a community
export const identifierCommunityAccountSchema = v.object({

View File

@ -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,

View File

@ -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

View File

@ -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<typeof DateSchema>
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<typeof DateSchema>
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"]')`),
)
})
})

View File

@ -39,6 +39,24 @@ export const uuidv4Schema = v.pipe(
export type Uuidv4Input = v.InferInput<typeof uuidv4Schema>
/**
* 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<string, IdentifierSeed>((input: string) => input as IdentifierSeed),
)
export type IdentifierSeedInput = v.InferInput<typeof identifierSeedSchema>
/**
* type guard for memory block size 32
* create with `v.parse(memoryBlock32Schema, MemoryBlock.fromHex('39568d7e148a0afee7f27a67dbf7d4e87d1fdec958e2680df98a469690ffc1a2'))`
@ -124,16 +142,20 @@ export type HieroIdInput = v.InferInput<typeof hieroIdSchema>
* 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<string, HieroTransactionId>((input: string) => input as HieroTransactionId),
v.transform<string, HieroTransactionIdString>(
(input: string) => input as HieroTransactionIdString,
),
)
export type HieroTransactionIdInput = v.InferInput<typeof hieroTransactionIdSchema>
export type HieroTransactionIdInput = v.InferInput<typeof hieroTransactionIdStringSchema>
/**
* type guard for memo
@ -176,14 +198,12 @@ export const timeoutDurationSchema = v.pipe(
),
v.instance(DurationSeconds, 'expect DurationSeconds type'),
]),
v.transform<number | DurationSeconds, TimeoutDuration>(
(input: number | DurationSeconds) => {
if (input instanceof DurationSeconds) {
return input as TimeoutDuration
}
return new DurationSeconds(input) as TimeoutDuration
},
),
v.transform<number | DurationSeconds, TimeoutDuration>((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<Amount | GradidoUnit, GradidoAmount>(
(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<Amount | GradidoUnit, GradidoAmount>((input: Amount | GradidoUnit) => {
if (input instanceof GradidoUnit) {
return input as GradidoAmount
}
return GradidoUnit.fromString(input) as GradidoAmount
}),
)

View File

@ -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',
})
})
})

View File

@ -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<boolean> {
// 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<boolean> {
// 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
export type DltRoutes = typeof appRoutes

View File

@ -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(),
})