mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
fix test, refactor
This commit is contained in:
parent
4658f33b88
commit
ed94bb7ea0
@ -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 ', () => {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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, '')
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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")
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
)
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]')`),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user