simplify backend code for dlt connector calling

This commit is contained in:
einhornimmond 2025-09-20 15:21:55 +02:00
parent eb3bf5e904
commit 5ea4aec922
35 changed files with 639 additions and 1405 deletions

View File

@ -58,8 +58,11 @@ export class DltConnectorClient {
* transmit transaction via dlt-connector to hiero
* and update dltTransactionId of transaction in db with hiero transaction id
*/
public async sendTransaction(input: TransactionDraft): Promise<IRestResponse<string>> {
public async sendTransaction(input: TransactionDraft): Promise<IRestResponse<{ transactionId: string }>> {
logger.debug('transmit transaction or user to dlt connector', input)
return await this.client.create<string>('/sendTransaction', input)
return await this.client.create<{ transactionId: string }>(
'/sendTransaction',
input
)
}
}

View File

@ -1,14 +0,0 @@
/**
* Error Types for dlt-connector graphql responses
*/
export enum TransactionErrorType {
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
MISSING_PARAMETER = 'Missing parameter',
ALREADY_EXIST = 'Already exist',
DB_ERROR = 'DB Error',
PROTO_DECODE_ERROR = 'Proto Decode Error',
PROTO_ENCODE_ERROR = 'Proto Encode Error',
INVALID_SIGNATURE = 'Invalid Signature',
LOGIC_ERROR = 'Logic Error',
NOT_FOUND = 'Not found',
}

View File

@ -0,0 +1,107 @@
import { IRestResponse } from 'typed-rest-client'
import { DltTransactionType } from './enum/DltTransactionType'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { DltConnectorClient } from './DltConnectorClient'
import {
Community as DbCommunity,
Contribution as DbContribution,
DltTransaction as DbDltTransaction,
User as DbUser,
getHomeCommunity,
} from 'database'
import { TransactionDraft } from './model/TransactionDraft'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector`)
// will be undefined if dlt connect is disabled
const dltConnectorClient = DltConnectorClient.getInstance()
async function checkDltConnectorResult(dltTransaction: DbDltTransaction, clientResponse: Promise<IRestResponse<{ transactionId: string }>>)
: Promise<DbDltTransaction> {
// check result from dlt connector
try {
const response = await clientResponse
if (response.statusCode === 200 && response.result) {
dltTransaction.messageId = response.result.transactionId
} else {
dltTransaction.error = `empty result with status code ${response.statusCode}`
logger.error('error from dlt-connector', response)
}
} catch (e) {
logger.debug(e)
if (e instanceof Error) {
dltTransaction.error = e.message
} else if (typeof e === 'string') {
dltTransaction.error = e
} else {
throw e
}
}
return dltTransaction
}
/**
* send register address transaction via dlt-connector to hiero
* and update dltTransactionId of transaction in db with hiero transaction id
*/
export async function registerAddressTransaction(user: DbUser, community: DbCommunity): Promise<DbDltTransaction | null> {
if (!user.id) {
logger.error(`missing id for user: ${user.gradidoID}, please call registerAddressTransaction after user.save()`)
return null
}
// return null if some data where missing and log error
const draft = TransactionDraft.createRegisterAddress(user, community)
if (draft && dltConnectorClient) {
const clientResponse = dltConnectorClient.sendTransaction(draft)
let dltTransaction = new DbDltTransaction()
dltTransaction.typeId = DltTransactionType.REGISTER_ADDRESS
if (user.id) {
dltTransaction.userId = user.id
}
dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse)
return await dltTransaction.save()
}
return null
}
export async function contributionTransaction(
contribution: DbContribution,
signingUser: DbUser,
createdAt: Date,
): Promise<DbDltTransaction | null> {
const homeCommunity = await getHomeCommunity()
if (!homeCommunity) {
logger.error('home community not found')
return null
}
const draft = TransactionDraft.createContribution(contribution, createdAt, signingUser, homeCommunity)
if (draft && dltConnectorClient) {
const clientResponse = dltConnectorClient.sendTransaction(draft)
let dltTransaction = new DbDltTransaction()
dltTransaction.typeId = DltTransactionType.CREATION
dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse)
return await dltTransaction.save()
}
return null
}
export async function transferTransaction(
senderUser: DbUser,
recipientUser: DbUser,
amount: string,
memo: string,
createdAt: Date
): Promise<DbDltTransaction | null> {
const draft = TransactionDraft.createTransfer(senderUser, recipientUser, amount, memo, createdAt)
if (draft && dltConnectorClient) {
const clientResponse = dltConnectorClient.sendTransaction(draft)
let dltTransaction = new DbDltTransaction()
dltTransaction.typeId = DltTransactionType.TRANSFER
dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse)
return await dltTransaction.save()
}
return null
}

View File

@ -1,63 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from 'typeorm'
import { DltTransaction } from 'database'
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
import { Logger } from 'log4js'
export abstract class AbstractTransactionToDltRole<T extends ObjectLiteral> {
protected self: T | null
// public interface
public abstract initWithLast(): Promise<this>
public abstract getTimestamp(): number
public abstract convertToGraphqlInput(): TransactionDraft
public constructor(protected logger: Logger) {}
public getEntity(): T | null {
return this.self
}
public async saveTransactionResult(messageId: string, error: string | null): Promise<void> {
const dltTransaction = DltTransaction.create()
dltTransaction.messageId = messageId
dltTransaction.error = error
this.setJoinIdAndType(dltTransaction)
await DltTransaction.save(dltTransaction)
if (dltTransaction.error) {
this.logger.error(
`Store dltTransaction with error: id=${dltTransaction.id}, error=${dltTransaction.error}`,
)
} else {
this.logger.info(
`Store dltTransaction: messageId=${dltTransaction.messageId}, id=${dltTransaction.id}`,
)
}
}
// intern
protected abstract setJoinIdAndType(dltTransaction: DltTransaction): void
// helper
protected createQueryForPendingItems(
qb: SelectQueryBuilder<T>,
joinCondition: string,
orderBy: OrderByCondition,
): SelectQueryBuilder<T> {
return qb
.leftJoin(DltTransaction, 'dltTransaction', joinCondition)
.where('dltTransaction.user_id IS NULL')
.andWhere('dltTransaction.transaction_id IS NULL')
.andWhere('dltTransaction.transaction_link_Id IS NULL')
.orderBy(orderBy)
}
protected createDltTransactionEntry(messageId: string, error: string | null): DltTransaction {
const dltTransaction = DltTransaction.create()
dltTransaction.messageId = messageId
dltTransaction.error = error
return dltTransaction
}
}

View File

@ -1,94 +0,0 @@
import { DltTransaction, TransactionLink } from 'database'
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
import { TransactionType } from '@dltConnector/enum/TransactionType'
import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed'
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
import { AccountIdentifier } from '@dltConnector/model/AccountIdentifier'
import { LogError } from '@/server/LogError'
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
import { CommunityAccountIdentifier } from '@dltConnector/model/CommunityAccountIdentifier'
/**
* redeem deferred transfer transaction by creator, so "deleting" it
*/
export class TransactionLinkDeleteToDltRole extends AbstractTransactionToDltRole<TransactionLink> {
async initWithLast(): Promise<this> {
const queryBuilder = this.createQueryForPendingItems(
TransactionLink
.createQueryBuilder()
.leftJoinAndSelect('TransactionLink.user', 'user')
.leftJoinAndSelect('user.community', 'community'),
'TransactionLink.id = dltTransaction.transactionLinkId and dltTransaction.type_id <> 4',
// eslint-disable-next-line camelcase
{ TransactionLink_deletedAt: 'ASC', User_id: 'ASC' },
)
.andWhere('TransactionLink.deletedAt IS NOT NULL')
.withDeleted()
/*
const queryBuilder2 = TransactionLink.createQueryBuilder()
.leftJoinAndSelect('TransactionLink.user', 'user')
.where('TransactionLink.deletedAt IS NOT NULL')
.andWhere(() => {
const subQuery = DltTransaction.createQueryBuilder()
.select('1')
.where('DltTransaction.transaction_link_id = TransactionLink.id')
.andWhere('DltTransaction.type_id = :typeId', {
typeId: DltTransactionType.DELETE_DEFERRED_TRANSFER,
})
.getQuery()
return `NOT EXIST (${subQuery})`
})
.withDeleted()
// eslint-disable-next-line camelcase
.orderBy({ TransactionLink_deletedAt: 'ASC', User_id: 'ASC' })
*/
// console.log('query: ', queryBuilder.getSql())
this.self = await queryBuilder.getOne()
return this
}
public getTimestamp(): number {
if (!this.self) {
return Infinity
}
if (!this.self.deletedAt) {
throw new LogError('not deleted transaction link selected')
}
return this.self.deletedAt.getTime()
}
public convertToGraphqlInput(): TransactionDraft {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction link')
}
if (!this.self.deletedAt) {
throw new LogError('not deleted transaction link selected')
}
const draft = new TransactionDraft()
draft.amount = this.self.amount.abs().toString()
const user = this.self.user
if (!user.community) {
throw new LogError(`missing community for user ${user.id}`)
}
const topicId = user.community.hieroTopicId
if (!topicId) {
throw new LogError(`missing topicId for community ${user.community.id}`)
}
draft.user = new AccountIdentifier(topicId, new IdentifierSeed(this.self.code))
draft.linkedUser = new AccountIdentifier(topicId, new CommunityAccountIdentifier(user.gradidoID))
draft.createdAt = this.self.deletedAt.toISOString()
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
return draft
}
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction link')
}
dltTransaction.transactionLinkId = this.self.id
dltTransaction.typeId = DltTransactionType.DELETE_DEFERRED_TRANSFER
}
}

View File

@ -1,68 +0,0 @@
import { DltTransaction, TransactionLink } from 'database'
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
import { TransactionType } from '@dltConnector/enum/TransactionType'
import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed'
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
import { AccountIdentifier } from '@dltConnector/model/AccountIdentifier'
import { LogError } from '@/server/LogError'
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
import { CommunityAccountIdentifier } from '../../model/CommunityAccountIdentifier'
/**
* send transactionLink as Deferred Transfers
*/
export class TransactionLinkToDltRole extends AbstractTransactionToDltRole<TransactionLink> {
async initWithLast(): Promise<this> {
this.self = await this.createQueryForPendingItems(
TransactionLink
.createQueryBuilder()
.leftJoinAndSelect('TransactionLink.user', 'user')
.leftJoinAndSelect('user.community', 'community'),
'TransactionLink.id = dltTransaction.transactionLinkId',
// eslint-disable-next-line camelcase
{ TransactionLink_createdAt: 'ASC', User_id: 'ASC' },
).getOne()
return this
}
public getTimestamp(): number {
if (!this.self) {
return Infinity
}
return this.self.createdAt.getTime()
}
public convertToGraphqlInput(): TransactionDraft {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction link')
}
const draft = new TransactionDraft()
draft.amount = this.self.amount.abs().toString()
const user = this.self.user
if (!user.community) {
throw new LogError(`missing community for user ${user.id}`)
}
const topicId = user.community.hieroTopicId
if (!topicId) {
throw new LogError(`missing topicId for community ${user.community.id}`)
}
draft.user = new AccountIdentifier(topicId, new CommunityAccountIdentifier(user.gradidoID, 1))
draft.linkedUser = new AccountIdentifier(topicId, new IdentifierSeed(this.self.code))
draft.createdAt = this.self.createdAt.toISOString()
draft.timeoutDuration = (this.self.validUntil.getTime() - this.self.createdAt.getTime()) / 1000
draft.memo = this.self.memo
draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER
return draft
}
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction link')
}
dltTransaction.transactionLinkId = this.self.id
dltTransaction.typeId = DltTransactionType.DEFERRED_TRANSFER
}
}

View File

@ -1,105 +0,0 @@
import { DltTransaction, Transaction } from 'database'
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
import { TransactionType } from '@dltConnector/enum/TransactionType'
import { CommunityUser } from '@dltConnector/model/CommunityUser'
import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed'
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
import { UserIdentifier } from '@/apis/dltConnector/model/AccountIdentifier'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
/**
* send transfer and creations transactions to dlt connector as GradidoTransfer and GradidoCreation
*/
export class TransactionToDltRole extends AbstractTransactionToDltRole<Transaction> {
private type: DltTransactionType
async initWithLast(): Promise<this> {
this.self = await this.createQueryForPendingItems(
Transaction.createQueryBuilder().leftJoinAndSelect(
'Transaction.transactionLink',
'transactionLink',
),
'Transaction.id = dltTransaction.transactionId',
// eslint-disable-next-line camelcase
{ balance_date: 'ASC', Transaction_id: 'ASC' },
)
// we don't need the receive transactions, there contain basically the same data as the send transactions
.andWhere('Transaction.type_id <> :typeId', { typeId: TransactionTypeId.RECEIVE })
.getOne()
return this
}
public getTimestamp(): number {
if (!this.self) {
return Infinity
}
return this.self.balanceDate.getTime()
}
public convertToGraphqlInput(): TransactionDraft {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction')
}
const draft = new TransactionDraft()
draft.amount = this.self.amount.abs().toString()
switch (this.self.typeId as TransactionTypeId) {
case TransactionTypeId.CREATION:
draft.type = TransactionType.GRADIDO_CREATION
this.type = DltTransactionType.CREATION
break
case TransactionTypeId.SEND:
case TransactionTypeId.RECEIVE:
draft.type = TransactionType.GRADIDO_TRANSFER
this.type = DltTransactionType.TRANSFER
break
default:
this.type = DltTransactionType.UNKNOWN
throw new LogError('wrong role for type', this.self.typeId as TransactionTypeId)
}
if (
!this.self.linkedUserGradidoID ||
!this.self.linkedUserCommunityUuid ||
!this.self.userCommunityUuid
) {
throw new LogError(
`missing necessary field in transaction: ${this.self.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`,
)
}
// it is a redeem of a transaction link?
const transactionLink = this.self.transactionLink
if (transactionLink) {
draft.user = new UserIdentifier(
this.self.userCommunityUuid,
new IdentifierSeed(transactionLink.code),
)
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
this.type = DltTransactionType.REDEEM_DEFERRED_TRANSFER
} else {
draft.user = new UserIdentifier(
this.self.userCommunityUuid,
new CommunityUser(this.self.userGradidoID, 1),
)
}
draft.linkedUser = new UserIdentifier(
this.self.linkedUserCommunityUuid,
new CommunityUser(this.self.linkedUserGradidoID, 1),
)
draft.memo = this.self.memo
draft.createdAt = this.self.balanceDate.toISOString()
draft.targetDate = this.self.creationDate?.toISOString()
return draft
}
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction')
}
dltTransaction.transactionId = this.self.id
dltTransaction.typeId = this.type
}
}

View File

@ -1,61 +0,0 @@
import { DltTransaction, User } from 'database'
import { AccountType } from '@dltConnector/enum/AccountType'
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
import { TransactionType } from '@dltConnector/enum/TransactionType'
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
import { AccountIdentifier } from '@/apis/dltConnector/model/AccountIdentifier'
import { LogError } from '@/server/LogError'
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
import { CommunityAccountIdentifier } from '../../model/CommunityAccountIdentifier'
/**
* send new user to dlt connector, will be made to RegisterAddress Transaction
*/
export class UserToDltRole extends AbstractTransactionToDltRole<User> {
async initWithLast(): Promise<this> {
this.self = await this.createQueryForPendingItems(
User.createQueryBuilder().leftJoinAndSelect('User.community', 'community'),
'User.id = dltTransaction.userId',
// eslint-disable-next-line camelcase
{ User_created_at: 'ASC', User_id: 'ASC' },
).getOne()
return this
}
public getTimestamp(): number {
if (!this.self) {
return Infinity
}
return this.self.createdAt.getTime()
}
public convertToGraphqlInput(): TransactionDraft {
if (!this.self) {
throw new LogError('try to create dlt entry for empty transaction')
}
if (!this.self.community) {
throw new LogError(`missing community for user ${this.self.id}`)
}
const topicId = this.self.community.hieroTopicId
if (!topicId) {
throw new LogError(`missing topicId for community ${this.self.community.id}`)
}
const draft = new TransactionDraft()
draft.user = new AccountIdentifier(topicId, new CommunityAccountIdentifier(this.self.gradidoID))
draft.createdAt = this.self.createdAt.toISOString()
draft.accountType = AccountType.COMMUNITY_HUMAN
draft.type = TransactionType.REGISTER_ADDRESS
return draft
}
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
if (!this.self) {
throw new LogError('try to create dlt entry for empty user')
}
dltTransaction.userId = this.self.id
dltTransaction.typeId = DltTransactionType.REGISTER_ADDRESS
}
}

View File

@ -1,67 +0,0 @@
import { Transaction, TransactionLink, User } from 'database'
import { DltConnectorClient } from '@/apis/dltConnector/DltConnectorClient'
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
import { TransactionLinkDeleteToDltRole } from './TransactionLinkDeleteToDlt.role'
import { TransactionLinkToDltRole } from './TransactionLinkToDlt.role'
import { TransactionToDltRole } from './TransactionToDlt.role'
import { UserToDltRole } from './UserToDlt.role'
import { getLogger, Logger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
/**
* @DCI-Context
* Context for sending transactions to dlt connector, always the oldest not sended transaction first
*/
export async function transactionToDlt(dltConnector: DltConnectorClient): Promise<void> {
async function findNextPendingTransaction(logger: Logger): Promise<
AbstractTransactionToDltRole<Transaction | User | TransactionLink>
> {
// collect each oldest not sended entity from db and choose oldest
const results = await Promise.all([
new TransactionToDltRole(logger).initWithLast(),
new UserToDltRole(logger).initWithLast(),
new TransactionLinkToDltRole(logger).initWithLast(),
new TransactionLinkDeleteToDltRole(logger).initWithLast(),
])
// sort array to get oldest at first place
results.sort((a, b) => {
return a.getTimestamp() - b.getTimestamp()
})
return results[0]
}
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector.interaction.transactionToDlt`)
while (true) {
const pendingTransactionRole = await findNextPendingTransaction(logger)
const pendingTransaction = pendingTransactionRole.getEntity()
if (!pendingTransaction) {
break
}
let messageId = ''
let error: string | null = null
try {
const result = await dltConnector.sendTransaction(
pendingTransactionRole.convertToGraphqlInput()
)
if (result.statusCode === 200 && result.result) {
messageId = result.result
} else {
error = `empty result with status code ${result.statusCode}`
logger.error('error from dlt-connector', result)
}
} catch (e) {
logger.debug(e)
if (e instanceof Error) {
error = e.message
} else if (typeof e === 'string') {
error = e
} else {
throw e
}
}
await pendingTransactionRole.saveTransactionResult(messageId, error)
}
}

View File

@ -3,6 +3,12 @@ import { AccountType } from '@dltConnector/enum/AccountType'
import { TransactionType } from '@dltConnector/enum/TransactionType'
import { AccountIdentifier } from './AccountIdentifier'
import { Community as DbCommunity, Contribution as DbContribution, User as DbUser } from 'database'
import { CommunityAccountIdentifier } from './CommunityAccountIdentifier'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector.model.TransactionDraft`)
export class TransactionDraft {
user: AccountIdentifier
@ -19,4 +25,55 @@ export class TransactionDraft {
timeoutDuration?: number
// only for register address
accountType?: AccountType
}
static createRegisterAddress(user: DbUser, community: DbCommunity): TransactionDraft | null {
if (community.hieroTopicId) {
const draft = new TransactionDraft()
draft.user = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(user.gradidoID))
draft.type = TransactionType.REGISTER_ADDRESS
draft.createdAt = user.createdAt.toISOString()
draft.accountType = AccountType.COMMUNITY_HUMAN
return draft
} else {
logger.warn(`missing topicId for community ${community.id}`)
}
return null
}
static createContribution(contribution: DbContribution, createdAt: Date, signingUser: DbUser, community: DbCommunity): TransactionDraft | null {
if (community.hieroTopicId) {
const draft = new TransactionDraft()
draft.user = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(contribution.user.gradidoID))
draft.linkedUser = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(signingUser.gradidoID))
draft.type = TransactionType.GRADIDO_CREATION
draft.createdAt = createdAt.toISOString()
draft.amount = contribution.amount.toString()
draft.memo = contribution.memo
draft.targetDate = contribution.contributionDate.toISOString()
return draft
} else {
logger.warn(`missing topicId for community ${community.id}`)
}
return null
}
static createTransfer(sendingUser: DbUser, receivingUser: DbUser, amount: string, memo: string, createdAt: Date): TransactionDraft | null {
if (!sendingUser.community || !receivingUser.community) {
logger.warn(`missing community for user ${sendingUser.id} and/or ${receivingUser.id}`)
return null
}
if (sendingUser.community.hieroTopicId && receivingUser.community.hieroTopicId) {
const draft = new TransactionDraft()
draft.user = new AccountIdentifier(sendingUser.community.hieroTopicId, new CommunityAccountIdentifier(sendingUser.gradidoID))
draft.linkedUser = new AccountIdentifier(receivingUser.community.hieroTopicId, new CommunityAccountIdentifier(receivingUser.gradidoID))
draft.type = TransactionType.GRADIDO_TRANSFER
draft.createdAt = createdAt.toISOString()
draft.amount = amount
draft.memo = memo
return draft
} else {
logger.warn(`missing topicId for community ${community.id}`)
}
return null
}
}

View File

@ -1,728 +0,0 @@
import { Community, DltTransaction, Transaction } from 'database'
import { Decimal } from 'decimal.js-light'
import { Response } from 'graphql-request/dist/types'
import { DataSource } from 'typeorm'
import { v4 as uuidv4 } from 'uuid'
import { cleanDB, testEnvironment } from '@test/helpers'
import { i18n as localization } from '@test/testSetup'
import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { creations } from '@/seeds/creation'
import { creationFactory } from '@/seeds/factory/creation'
import { userFactory } from 'database/src/seeds/factory/user'
import { bibiBloxberg } from 'database/src/seeds/users/bibi-bloxberg'
import { bobBaumeister } from 'database/src/seeds/users/bob-baumeister'
import { peterLustig } from 'database/src/seeds/users/peter-lustig'
import { raeuberHotzenplotz } from 'database/src/seeds/users/raeuber-hotzenplotz'
import { getLogger } from 'config-schema/test/testSetup'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector'
jest.mock('@/password/EncryptorUtils')
const logger = getLogger(
`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`,
)
async function createHomeCommunity(): Promise<Community> {
const homeCommunity = Community.create()
homeCommunity.foreign = false
homeCommunity.communityUuid = uuidv4()
homeCommunity.url = 'localhost'
homeCommunity.publicKey = Buffer.from('0x6e6a6c6d6feffe', 'hex')
await Community.save(homeCommunity)
return homeCommunity
}
async function createTxCREATION1(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(1000)
tx.balance = new Decimal(100)
tx.balanceDate = new Date('01.01.2023 00:00:00')
tx.memo = 'txCREATION1'
tx.typeId = TransactionTypeId.CREATION
tx.userGradidoID = 'txCREATION1.userGradidoID'
tx.userId = 1
tx.userName = 'txCREATION 1'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('01.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('01.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxCREATION2(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(1000)
tx.balance = new Decimal(200)
tx.balanceDate = new Date('02.01.2023 00:00:00')
tx.memo = 'txCREATION2'
tx.typeId = TransactionTypeId.CREATION
tx.userGradidoID = 'txCREATION2.userGradidoID'
tx.userId = 2
tx.userName = 'txCREATION 2'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('02.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('02.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxCREATION3(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(1000)
tx.balance = new Decimal(300)
tx.balanceDate = new Date('03.01.2023 00:00:00')
tx.memo = 'txCREATION3'
tx.typeId = TransactionTypeId.CREATION
tx.userGradidoID = 'txCREATION3.userGradidoID'
tx.userId = 3
tx.userName = 'txCREATION 3'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('03.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('03.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxSend1ToReceive2(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(100)
tx.balance = new Decimal(1000)
tx.balanceDate = new Date('11.01.2023 00:00:00')
tx.memo = 'txSEND1 to txRECEIVE2'
tx.typeId = TransactionTypeId.SEND
tx.userGradidoID = 'txSEND1.userGradidoID'
tx.userId = 1
tx.userName = 'txSEND 1'
tx.linkedUserGradidoID = 'txRECEIVE2.linkedUserGradidoID'
tx.linkedUserId = 2
tx.linkedUserName = 'txRECEIVE 2'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('11.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a1'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('11.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxReceive2FromSend1(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(100)
tx.balance = new Decimal(1300)
tx.balanceDate = new Date('11.01.2023 00:00:00')
tx.memo = 'txSEND1 to txRECEIVE2'
tx.typeId = TransactionTypeId.RECEIVE
tx.userGradidoID = 'txRECEIVE2.linkedUserGradidoID'
tx.userId = 2
tx.userName = 'txRECEIVE 2'
tx.linkedUserGradidoID = 'txSEND1.userGradidoID'
tx.linkedUserId = 1
tx.linkedUserName = 'txSEND 1'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('11.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b2'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('11.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
/*
async function createTxSend2ToReceive3(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(200)
tx.balance = new Decimal(1100)
tx.balanceDate = new Date('23.01.2023 00:00:00')
tx.memo = 'txSEND2 to txRECEIVE3'
tx.typeId = TransactionTypeId.SEND
tx.userGradidoID = 'txSEND2.userGradidoID'
tx.userId = 2
tx.userName = 'txSEND 2'
tx.linkedUserGradidoID = 'txRECEIVE3.linkedUserGradidoID'
tx.linkedUserId = 3
tx.linkedUserName = 'txRECEIVE 3'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('23.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a2'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('23.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxReceive3FromSend2(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(200)
tx.balance = new Decimal(1500)
tx.balanceDate = new Date('23.01.2023 00:00:00')
tx.memo = 'txSEND2 to txRECEIVE3'
tx.typeId = TransactionTypeId.RECEIVE
tx.userGradidoID = 'txRECEIVE3.linkedUserGradidoID'
tx.userId = 3
tx.userName = 'txRECEIVE 3'
tx.linkedUserGradidoID = 'txSEND2.userGradidoID'
tx.linkedUserId = 2
tx.linkedUserName = 'txSEND 2'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('23.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b3'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('23.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxSend3ToReceive1(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(300)
tx.balance = new Decimal(1200)
tx.balanceDate = new Date('31.01.2023 00:00:00')
tx.memo = 'txSEND3 to txRECEIVE1'
tx.typeId = TransactionTypeId.SEND
tx.userGradidoID = 'txSEND3.userGradidoID'
tx.userId = 3
tx.userName = 'txSEND 3'
tx.linkedUserGradidoID = 'txRECEIVE1.linkedUserGradidoID'
tx.linkedUserId = 1
tx.linkedUserName = 'txRECEIVE 1'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('31.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a3'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('31.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
async function createTxReceive1FromSend3(verified: boolean): Promise<Transaction> {
let tx = Transaction.create()
tx.amount = new Decimal(300)
tx.balance = new Decimal(1300)
tx.balanceDate = new Date('31.01.2023 00:00:00')
tx.memo = 'txSEND3 to txRECEIVE1'
tx.typeId = TransactionTypeId.RECEIVE
tx.userGradidoID = 'txRECEIVE1.linkedUserGradidoID'
tx.userId = 1
tx.userName = 'txRECEIVE 1'
tx.linkedUserGradidoID = 'txSEND3.userGradidoID'
tx.linkedUserId = 3
tx.linkedUserName = 'txSEND 3'
tx = await Transaction.save(tx)
if (verified) {
const dlttx = DltTransaction.create()
dlttx.createdAt = new Date('31.01.2023 00:00:10')
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b1'
dlttx.transactionId = tx.id
dlttx.verified = true
dlttx.verifiedAt = new Date('31.01.2023 00:01:10')
await DltTransaction.save(dlttx)
}
return tx
}
*/
let con: DataSource
let testEnv: {
con: DataSource
}
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.destroy()
})
describe('create and send Transactions to DltConnector', () => {
let txCREATION1: Transaction
let txCREATION2: Transaction
let txCREATION3: Transaction
let txSEND1to2: Transaction
let txRECEIVE2From1: Transaction
// let txSEND2To3: Transaction
// let txRECEIVE3From2: Transaction
// let txSEND3To1: Transaction
// let txRECEIVE1From3: Transaction
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(async () => {
await cleanDB()
})
describe('with 3 creations but inactive dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
txCREATION1 = await createTxCREATION1(false)
txCREATION2 = await createTxCREATION2(false)
txCREATION3 = await createTxCREATION3(false)
await createHomeCommunity()
CONFIG.DLT_CONNECTOR = false
await sendTransactionsToDltConnector()
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
const transactions = await Transaction.find({
// where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
const dltTransactions = await DltTransaction.find({
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[0].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[1].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[2].id,
messageId: null,
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
expect(logger.info).nthCalledWith(2, 'sending to DltConnector currently not configured...')
})
})
describe('with 3 creations and active dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, raeuberHotzenplotz)
await userFactory(testEnv, bobBaumeister)
let count = 0
for (const creation of creations) {
await creationFactory(testEnv, creation)
count++
// we need only 3 for testing
if (count >= 3) {
break
}
}
await createHomeCommunity()
CONFIG.DLT_CONNECTOR = true
await sendTransactionsToDltConnector()
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
const transactions = await Transaction.find({
// where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
const dltTransactions = await DltTransaction.find({
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[0].id,
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[1].id,
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[2].id,
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
})
})
describe('with 3 verified creations, 1 sendCoins and active dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
txCREATION1 = await createTxCREATION1(true)
txCREATION2 = await createTxCREATION2(true)
txCREATION3 = await createTxCREATION3(true)
await createHomeCommunity()
txSEND1to2 = await createTxSend1ToReceive2(false)
txRECEIVE2From1 = await createTxReceive2FromSend1(false)
/*
txSEND2To3 = await createTxSend2ToReceive3()
txRECEIVE3From2 = await createTxReceive3FromSend2()
txSEND3To1 = await createTxSend3ToReceive1()
txRECEIVE1From3 = await createTxReceive1FromSend3()
*/
CONFIG.DLT_CONNECTOR = true
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
return {
data: {
sendTransaction: { succeed: true },
},
} as Response<unknown>
})
await sendTransactionsToDltConnector()
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
// Find the previous created transactions of sendCoin mutation
/*
const transactions = await Transaction.find({
// where: { memo: 'unrepeatable memo' },
order: { balanceDate: 'ASC', id: 'ASC' },
})
*/
const dltTransactions = await DltTransaction.find({
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
// relations: ['transaction'],
order: { createdAt: 'ASC', id: 'ASC' },
})
expect(dltTransactions).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
transactionId: txCREATION1.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1',
verified: true,
createdAt: new Date('01.01.2023 00:00:10'),
verifiedAt: new Date('01.01.2023 00:01:10'),
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txCREATION2.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2',
verified: true,
createdAt: new Date('02.01.2023 00:00:10'),
verifiedAt: new Date('02.01.2023 00:01:10'),
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txCREATION3.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3',
verified: true,
createdAt: new Date('03.01.2023 00:00:10'),
verifiedAt: new Date('03.01.2023 00:01:10'),
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txSEND1to2.id,
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
transactionId: txRECEIVE2From1.id,
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
}),
]),
)
})
/*
describe('with one Community of api 1_0 and not matching pubKey', () => {
beforeEach(async () => {
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
return {
data: {
getPublicKey: {
publicKey: 'somePubKey',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
// await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs not matching publicKeys', () => {
expect(logger.warn).toBeCalledWith(
'Federation: received not matching publicKey:',
'somePubKey',
expect.stringMatching('11111111111111111111111111111111'),
)
})
})
describe('with one Community of api 1_0 and matching pubKey', () => {
beforeEach(async () => {
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
return {
data: {
getPublicKey: {
publicKey: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
// await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs community pubKey verified', () => {
expect(logger.info).toHaveBeenNthCalledWith(
3,
'Federation: verified community with',
'http//localhost:5001/api/',
)
})
})
describe('with two Communities of api 1_0 and 1_1', () => {
beforeEach(async () => {
jest.clearAllMocks()
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
return {
data: {
getPublicKey: {
publicKey: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables2)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
// await validateCommunities()
})
it('logs two communities found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
)
})
})
describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
let dbCom: DbFederatedCommunity
beforeEach(async () => {
const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables3)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
dbCom = await DbFederatedCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
})
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
// await validateCommunities()
})
it('logs three community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
)
})
it('logs unsupported api for community with api 2_0 ', () => {
expect(logger.warn).toBeCalledWith(
'Federation: dbCom with unsupported apiVersion',
dbCom.endPoint,
'2_0',
)
})
})
*/
})
})

View File

@ -1,69 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { CONFIG } from '@/config'
import { TypeORMError } from 'typeorm'
// eslint-disable-next-line import/named, n/no-extraneous-import
import { FetchError } from 'node-fetch'
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
import { LogError } from '@/server/LogError'
import {
InterruptiveSleepManager,
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
} from '@/util/InterruptiveSleepManager'
import { transactionToDlt } from './interaction/transactionToDlt/transactionToDlt.context'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector.sendTransactionsToDltConnector`)
let isLoopRunning = true
export const stopSendTransactionsToDltConnector = (): void => {
isLoopRunning = false
}
export async function sendTransactionsToDltConnector(): Promise<void> {
const dltConnector = DltConnectorClient.getInstance()
if (!dltConnector) {
logger.info('currently not configured...')
isLoopRunning = false
return
}
logger.info('task started')
// define outside of loop for reuse and reducing gb collection
// const queries = getFindNextPendingTransactionQueries()
// eslint-disable-next-line no-unmodified-loop-condition
while (isLoopRunning) {
try {
// return after no pending transactions are left
await transactionToDlt(dltConnector)
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
// TODO: put sleep time into config, because it influence performance,
// transactionToDlt call 4 db queries to look for new transactions
CONFIG.PRODUCTION ? 100000 : 1000,
)
} catch (e) {
// couldn't connect to dlt-connector? We wait
if (e instanceof FetchError) {
logger.error(`error connecting dlt-connector, wait 5 seconds before retry: ${String(e)}`)
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
5000,
)
} else {
if (e instanceof TypeORMError) {
// seems to be a error in code, so let better stop here
throw new LogError(e.message, e.stack)
} else {
logger.error(`Error while sending to DLT-connector or writing messageId`, e)
}
}
}
}
}

View File

@ -2,6 +2,7 @@ import {
Contribution as DbContribution,
Transaction as DbTransaction,
User as DbUser,
DltTransaction as DbDltTransaction,
UserContact,
} from 'database'
import { Decimal } from 'decimal.js-light'
@ -60,6 +61,7 @@ import { extractGraphQLFields } from './util/extractGraphQLFields'
import { findContributions } from './util/findContributions'
import { getLastTransaction } from './util/getLastTransaction'
import { InterruptiveSleepManager, TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/util/InterruptiveSleepManager'
import { contributionTransaction } from '@/apis/dltConnector'
const db = AppDatabase.getInstance()
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`)
@ -436,11 +438,13 @@ export class ContributionResolver {
const logger = createLogger()
logger.addContext('contribution', id)
let transaction: DbTransaction
let dltTransactionPromise: Promise<DbDltTransaction | null>
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne({ where: { id } })
const contribution = await DbContribution.findOne({ where: { id }, relations: {user: {emailContact: true}} })
if (!contribution) {
throw new LogError('Contribution not found', id)
}
@ -450,18 +454,18 @@ export class ContributionResolver {
if (contribution.contributionStatus === 'DENIED') {
throw new LogError('Contribution already denied', id)
}
const moderatorUser = getUser(context)
if (moderatorUser.id === contribution.userId) {
throw new LogError('Moderator can not confirm own contribution')
}
const user = await DbUser.findOneOrFail({
where: { id: contribution.userId },
withDeleted: true,
relations: ['emailContact'],
})
const user = contribution.user
if (user.deletedAt) {
throw new LogError('Can not confirm contribution since the user was deleted')
}
const receivedCallDate = new Date()
dltTransactionPromise = contributionTransaction(contribution, moderatorUser, receivedCallDate)
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
validateContribution(
creations,
@ -469,8 +473,7 @@ export class ContributionResolver {
contribution.contributionDate,
clientTimezoneOffset,
)
const receivedCallDate = new Date()
const queryRunner = db.getDataSource().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
@ -491,7 +494,7 @@ export class ContributionResolver {
}
newBalance = newBalance.add(contribution.amount.toString())
const transaction = new DbTransaction()
transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
@ -509,7 +512,7 @@ export class ContributionResolver {
transaction.balanceDate = receivedCallDate
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
await queryRunner.manager.insert(DbTransaction, transaction)
transaction = await queryRunner.manager.save(DbTransaction, transaction)
contribution.confirmedAt = receivedCallDate
contribution.confirmedBy = moderatorUser.id
@ -519,9 +522,6 @@ export class ContributionResolver {
await queryRunner.commitTransaction()
// notify dlt-connector loop for new work
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
logger.info('creation commited successfuly.')
await sendContributionConfirmedEmail({
firstName: user.firstName,
@ -547,6 +547,16 @@ export class ContributionResolver {
} finally {
releaseLock()
}
// update transaction id in dlt transaction tables
// wait for finishing transaction by dlt-connector/hiero
const startTime = new Date()
const dltTransaction = await dltTransactionPromise
if(dltTransaction) {
dltTransaction.transactionId = transaction.id
await dltTransaction.save()
}
const endTime = new Date()
logger.debug(`dlt-connector contribution finished in ${endTime.getTime() - startTime.getTime()} ms`)
return true
}

View File

@ -2,6 +2,7 @@ import {
AppDatabase,
countOpenPendingTransactions,
Community as DbCommunity,
DltTransaction as DbDltTransaction,
PendingTransaction as DbPendingTransaction,
Transaction as dbTransaction,
TransactionLink as dbTransactionLink,
@ -54,6 +55,7 @@ import {
} from './util/processXComSendCoins'
import { storeForeignUser } from './util/storeForeignUser'
import { transactionLinkSummary } from './util/transactionLinkSummary'
import { transferTransaction } from '@/apis/dltConnector'
const db = AppDatabase.getInstance()
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`)
@ -66,6 +68,12 @@ export const executeTransaction = async (
logger: Logger,
transactionLink?: dbTransactionLink | null,
): Promise<boolean> => {
const receivedCallDate = new Date()
let dltTransactionPromise: Promise<DbDltTransaction | null> = Promise.resolve(null)
if (!transactionLink) {
dltTransactionPromise = transferTransaction(sender, recipient, amount.toString(), memo, receivedCallDate)
}
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
@ -82,8 +90,7 @@ export const executeTransaction = async (
throw new LogError('Sender and Recipient are the same', sender.id)
}
// validate amount
const receivedCallDate = new Date()
// validate amount
const sendBalance = await calculateBalance(
sender.id,
amount.mul(-1),
@ -163,7 +170,12 @@ export const executeTransaction = async (
}
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
// update dltTransaction with transactionId
const dltTransaction = await dltTransactionPromise
if (dltTransaction) {
dltTransaction.transactionId = transactionSend.id
await dltTransaction.save()
}
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
@ -179,8 +191,9 @@ export const executeTransaction = async (
} finally {
await queryRunner.release()
}
// notify dlt-connector loop for new work
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
// InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
await sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,

View File

@ -1,6 +1,7 @@
import {
AppDatabase,
ContributionLink as DbContributionLink,
DltTransaction as DbDltTransaction,
TransactionLink as DbTransactionLink,
User as DbUser,
UserContact as DbUserContact,
@ -85,10 +86,6 @@ import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
import { communityDbUser } from '@/util/communityUser'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { durationInMinutesFromDates, getTimeDurationObject, printTimeDuration } from '@/util/time'
import {
InterruptiveSleepManager,
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
} from '@/util/InterruptiveSleepManager'
import { delay } from '@/util/utilities'
import random from 'random-bigint'
@ -108,6 +105,7 @@ import { deleteUserRole, setUserRole } from './util/modifyUserRole'
import { sendUserToGms } from './util/sendUserToGms'
import { syncHumhub } from './util/syncHumhub'
import { validateAlias } from 'core'
import { registerAddressTransaction } from '@/apis/dltConnector'
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
const DEFAULT_LANGUAGE = 'de'
@ -391,6 +389,7 @@ export class UserResolver {
if (homeCom.communityUuid) {
dbUser.communityUuid = homeCom.communityUuid
}
dbUser.gradidoID = gradidoID
dbUser.firstName = firstName
dbUser.lastName = lastName
@ -401,8 +400,11 @@ export class UserResolver {
dbUser.alias = alias
}
dbUser.publisherId = publisherId ?? 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', new UserLoggingView(dbUser))
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
if(logger.isDebugEnabled()) {
logger.debug('new dbUser', new UserLoggingView(dbUser))
}
if (redeemCode) {
if (redeemCode.match(/^CL-/)) {
const contributionLink = await DbContributionLink.findOne({
@ -438,7 +440,7 @@ export class UserResolver {
dbUser.emailContact = emailContact
dbUser.emailId = emailContact.id
await queryRunner.manager.save(dbUser).catch((error) => {
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
throw new LogError('Error while updating dbUser', error)
})
@ -470,6 +472,8 @@ export class UserResolver {
} finally {
await queryRunner.release()
}
// register user into blockchain
const dltTransactionPromise = registerAddressTransaction(dbUser, homeCom)
logger.info('createUser() successful...')
if (CONFIG.HUMHUB_ACTIVE) {
let spaceId: number | null = null
@ -483,9 +487,6 @@ export class UserResolver {
}
}
// notify dlt-connector loop for new work
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
if (redeemCode) {
eventRegisterRedeem.affectedUser = dbUser
eventRegisterRedeem.actingUser = dbUser
@ -509,6 +510,11 @@ export class UserResolver {
}
}
}
// wait for finishing dlt transaction
const startTime = new Date()
await dltTransactionPromise
const endTime = new Date()
logger.info(`dlt-connector register address finished in ${endTime.getTime() - startTime.getTime()} ms`)
return new User(dbUser)
}

View File

@ -24,7 +24,7 @@ async function main() {
// task is running the whole time for transmitting transaction via dlt-connector to iota
// can be notified with InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
// that a new transaction or user was stored in db
void sendTransactionsToDltConnector()
// void sendTransactionsToDltConnector()
void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
}

View File

@ -20,7 +20,7 @@
"log4js": "^6.9.1",
"typescript": "^5.8.3",
"uuid": "^8.3.2",
"valibot": "^1.1.0",
"valibot": "1.1.0",
},
},
},

View File

@ -33,7 +33,7 @@
"log4js": "^6.9.1",
"typescript": "^5.8.3",
"uuid": "^8.3.2",
"valibot": "^1.1.0"
"valibot": "1.1.0"
},
"engines": {
"node": ">=18"

View File

@ -11,12 +11,14 @@ describe('community.schema', () => {
uuid: '4f28e081-5c39-4dde-b6a4-3bde71de8d65',
hieroTopicId: '0.0.4',
foreign: false,
name: 'Test',
creationDate: '2021-01-01',
}),
).toEqual({
hieroTopicId: v.parse(hieroIdSchema, '0.0.4'),
uuid: v.parse(uuidv4Schema, '4f28e081-5c39-4dde-b6a4-3bde71de8d65'),
foreign: false,
name: 'Test',
creationDate: new Date('2021-01-01'),
})
})

View File

@ -2,10 +2,8 @@ import {
AccountBalance,
AccountBalanceQuery,
Client,
Key,
LocalProvider,
PrivateKey,
Timestamp,
TopicCreateTransaction,
TopicId,
TopicInfoQuery,
@ -59,6 +57,7 @@ export class HieroClient {
topicId: HieroId,
transaction: GradidoTransaction,
): Promise<{ receipt: TransactionReceipt; response: TransactionResponse }> {
let startTime = new Date()
this.logger.addContext('topicId', topicId.toString())
const serializedTransaction = transaction.getSerializedTransaction()
if (!serializedTransaction) {
@ -69,13 +68,27 @@ export class HieroClient {
topicId,
message: serializedTransaction.data(),
}).freezeWithSigner(this.wallet)
let endTime = new Date()
this.logger.info(`prepare message, until freeze, cost: ${endTime.getTime() - startTime.getTime()}ms`)
startTime = new Date()
const signedHieroTransaction = await hieroTransaction.signWithSigner(this.wallet)
endTime = new Date()
this.logger.info(`sign message, cost: ${endTime.getTime() - startTime.getTime()}ms`)
startTime = new Date()
const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet)
endTime = new Date()
this.logger.info(`send message, cost: ${endTime.getTime() - startTime.getTime()}ms`)
startTime = new Date()
const sendReceipt = await sendResponse.getReceiptWithSigner(this.wallet)
endTime = new Date()
this.logger.info(`get receipt, cost: ${endTime.getTime() - startTime.getTime()}ms`)
this.logger.info(
`message sent to topic ${topicId}, status: ${sendReceipt.status.toString()}, transaction id: ${sendResponse.transactionId.toString()}`,
)
startTime = new Date()
const record = await sendResponse.getRecordWithSigner(this.wallet)
endTime = new Date()
this.logger.info(`get record, cost: ${endTime.getTime() - startTime.getTime()}ms`)
this.logger.info(`message sent, cost: ${record.transactionFee.toString()}`)
return { receipt: sendReceipt, response: sendResponse }
}

View File

@ -1,10 +1,10 @@
import { MemoryBlock } from 'gradido-blockchain-js'
import { ParameterError } from '../errors'
import { IdentifierAccount } from '../schemas/account.schema'
import { IdentifierKeyPair } from '../schemas/account.schema'
import { HieroId } from '../schemas/typeGuard.schema'
export class KeyPairIdentifierLogic {
public constructor(public identifier: IdentifierAccount) {}
public constructor(public identifier: IdentifierKeyPair) {}
isCommunityKeyPair(): boolean {
return !this.identifier.seed && !this.identifier.account
@ -91,8 +91,8 @@ export class KeyPairIdentifierLogic {
if (!this.identifier.account?.userUuid || !this.identifier.communityTopicId) {
throw new ParameterError('userUuid and/or communityTopicId is undefined')
}
const resultHexString =
const resultString =
this.identifier.communityTopicId + this.identifier.account.userUuid.replace(/-/g, '')
return MemoryBlock.fromHex(resultHexString).calculateHash().convertToHex()
return new MemoryBlock(resultString).calculateHash().convertToHex()
}
}

View File

@ -7,7 +7,6 @@ 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'

View File

@ -0,0 +1,99 @@
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

@ -21,7 +21,12 @@ export class CreationTransactionRole extends AbstractTransactionRole {
private readonly creationTransaction: CreationTransaction
constructor(transaction: Transaction) {
super()
this.creationTransaction = parse(creationTransactionSchema, transaction)
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.homeCommunityTopicId = KeyPairCacheManager.getInstance().getHomeCommunityTopicId()
if (
this.homeCommunityTopicId !== this.creationTransaction.user.communityTopicId ||

View File

@ -0,0 +1,35 @@
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 { InteractionToJson, InteractionValidate, ValidateType_SINGLE } from 'gradido-blockchain-js'
const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec'
const transaction = {
user: {
communityTopicId: '0.0.21732',
account: {
userUuid,
accountNr: 0,
},
},
type: 'REGISTER_ADDRESS',
accountType: 'COMMUNITY_HUMAN',
createdAt: '2022-01-01T00:00:00.000Z',
}
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')
})
})

View File

@ -34,19 +34,15 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRole {
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const communityKeyPair = await KeyPairCalculation(
new KeyPairIdentifierLogic({
communityTopicId: this.registerAddressTransaction.user.communityTopicId,
}),
)
const accountKeyPairIdentifier = this.registerAddressTransaction.user
const communityTopicId = this.registerAddressTransaction.user.communityTopicId
const communityKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic({ communityTopicId }))
const keyPairIdentifier = this.registerAddressTransaction.user
// when accountNr is 0 it is the user account
const userKeyPairIdentifier = accountKeyPairIdentifier
userKeyPairIdentifier.account.accountNr = 0
const userKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(userKeyPairIdentifier))
keyPairIdentifier.account.accountNr = 0
const userKeyPair = await KeyPairCalculation(new KeyPairIdentifierLogic(keyPairIdentifier))
keyPairIdentifier.account.accountNr = 1
const accountKeyPair = await KeyPairCalculation(
new KeyPairIdentifierLogic(accountKeyPairIdentifier),
new KeyPairIdentifierLogic(keyPairIdentifier),
)
builder

View File

@ -58,28 +58,25 @@ export async function SendToHieroContext(
// choose correct role based on transaction type and input type
const chooseCorrectRole = (input: Transaction | Community): AbstractTransactionRole => {
const transactionParsingResult = safeParse(transactionSchema, input)
const communityParsingResult = safeParse(communitySchema, input)
if (transactionParsingResult.success) {
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)
}
} else if (communityParsingResult.success) {
if (communityParsingResult.success) {
return new CommunityRootTransactionRole(communityParsingResult.output)
} else {
throw new Error('not expected input')
}
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)
}
}

View File

@ -11,18 +11,22 @@ export type IdentifierSeed = v.InferOutput<typeof identifierSeedSchema>
// identifier for gradido community accounts, inside a community
export const identifierCommunityAccountSchema = v.object({
userUuid: uuidv4Schema,
accountNr: v.nullish(v.number('expect number type'), 0),
accountNr: v.optional(v.number('expect number type'), 0),
})
export type IdentifierCommunityAccount = v.InferOutput<typeof identifierCommunityAccountSchema>
export const identifierKeyPairSchema = v.object({
communityTopicId: hieroIdSchema,
account: v.optional(identifierCommunityAccountSchema),
seed: v.optional(identifierSeedSchema),
})
export type IdentifierKeyPairInput = v.InferInput<typeof identifierKeyPairSchema>
export type IdentifierKeyPair = v.InferOutput<typeof identifierKeyPairSchema>
// identifier for gradido account, including the community uuid
export const identifierAccountSchema = v.pipe(
v.object({
communityTopicId: hieroIdSchema,
account: v.nullish(identifierCommunityAccountSchema, undefined),
seed: v.nullish(identifierSeedSchema, undefined),
}),
identifierKeyPairSchema,
v.custom((value: any) => {
const setFieldsCount = Number(value.seed !== undefined) + Number(value.account !== undefined)
if (setFieldsCount !== 1) {

View File

@ -1,4 +1,6 @@
import { beforeAll, describe, expect, it } from 'bun:test'
import { TypeBoxFromValibot } from '@sinclair/typemap'
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { randomBytes } from 'crypto'
import { v4 as uuidv4 } from 'uuid'
import { parse } from 'valibot'
@ -13,7 +15,9 @@ import {
Uuidv4,
uuidv4Schema,
} from '../schemas/typeGuard.schema'
import { TransactionInput, transactionSchema } from './transaction.schema'
import { registerAddressTransactionSchema, TransactionInput, transactionSchema } from './transaction.schema'
import { AccountType } from '../enum/AccountType'
import { AddressType_COMMUNITY_HUMAN } from 'gradido-blockchain-js'
const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16)
@ -40,27 +44,54 @@ describe('transaction schemas', () => {
memoString = 'TestMemo'
memo = parse(memoSchema, memoString)
})
it('valid, register new user address', () => {
const registerAddress: TransactionInput = {
user: {
communityTopicId: topicString,
account: { userUuid: userUuidString },
},
type: InputTransactionType.REGISTER_ADDRESS,
createdAt: '2022-01-01T00:00:00.000Z',
}
expect(parse(transactionSchema, registerAddress)).toEqual({
user: {
communityTopicId: topic,
account: {
userUuid,
accountNr: 0,
describe('register address', () => {
let registerAddress: TransactionInput
beforeAll(() => {
registerAddress = {
user: {
communityTopicId: topicString,
account: { userUuid: userUuidString },
},
},
type: registerAddress.type,
createdAt: new Date(registerAddress.createdAt),
type: InputTransactionType.REGISTER_ADDRESS,
accountType: AccountType.COMMUNITY_HUMAN,
createdAt: new Date().toISOString(),
}
})
it('valid transaction schema', () => {
expect(parse(transactionSchema, registerAddress)).toEqual({
user: {
communityTopicId: topic,
account: {
userUuid,
accountNr: 0,
},
},
type: registerAddress.type,
accountType: AccountType.COMMUNITY_HUMAN,
createdAt: new Date(registerAddress.createdAt),
})
})
it('valid register address schema', () => {
expect(parse(registerAddressTransactionSchema, registerAddress)).toEqual({
user: {
communityTopicId: topic,
account: {
userUuid,
accountNr: 0,
},
},
accountType: AddressType_COMMUNITY_HUMAN,
createdAt: new Date(registerAddress.createdAt),
})
})
it('valid, transaction schema with typebox', () => {
// console.log(JSON.stringify(TypeBoxFromValibot(transactionSchema), null, 2))
const TTransactionSchema = TypeBoxFromValibot(transactionSchema)
const check = TypeCompiler.Compile(TTransactionSchema)
expect(check.Check(registerAddress)).toBe(true)
})
})
it('valid, gradido transfer', () => {
const gradidoTransfer: TransactionInput = {
user: {

View File

@ -5,7 +5,7 @@ import {
identifierCommunityAccountSchema,
identifierSeedSchema,
} from './account.schema'
import { accountTypeSchema, addressTypeSchema, dateSchema } from './typeConverter.schema'
import { addressTypeSchema, dateSchema } from './typeConverter.schema'
import {
gradidoAmountSchema,
hieroIdSchema,
@ -13,6 +13,7 @@ import {
timeoutDurationSchema,
uuidv4Schema,
} from './typeGuard.schema'
import { AccountType } from '../enum/AccountType'
/**
* Schema for community, for creating new CommunityRoot Transaction on gradido blockchain
@ -29,14 +30,14 @@ export type Community = v.InferOutput<typeof communitySchema>
export const transactionSchema = v.object({
user: identifierAccountSchema,
linkedUser: v.nullish(identifierAccountSchema, undefined),
amount: v.nullish(gradidoAmountSchema, undefined),
memo: v.nullish(memoSchema, undefined),
linkedUser: v.optional(identifierAccountSchema),
amount: v.optional(gradidoAmountSchema),
memo: v.optional(memoSchema),
type: v.enum(InputTransactionType),
createdAt: dateSchema,
targetDate: v.nullish(dateSchema, undefined),
timeoutDuration: v.nullish(timeoutDurationSchema, undefined),
accountType: v.nullish(accountTypeSchema, undefined),
targetDate: v.optional(dateSchema),
timeoutDuration: v.optional(timeoutDurationSchema),
accountType: v.optional(v.enum(AccountType)),
})
export type TransactionInput = v.InferInput<typeof transactionSchema>

View File

@ -1,6 +1,8 @@
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 { AddressType_COMMUNITY_AUF, AddressType_COMMUNITY_PROJECT } from 'gradido-blockchain-js'
import { AddressType_COMMUNITY_AUF } from 'gradido-blockchain-js'
import * as v from 'valibot'
import { AccountType } from '../enum/AccountType'
import {
@ -23,6 +25,27 @@ describe('basic.schema', () => {
it('invalid date', () => {
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)
// 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')
// @ts-expect-error
const invalidDate: DateType = 123 // should fail in TS
})
})
describe('AddressType and AccountType', () => {
@ -46,6 +69,22 @@ describe('basic.schema', () => {
const accountType = v.parse(accountTypeSchema, AddressType_COMMUNITY_AUF)
expect(accountType).toBe(AccountType.COMMUNITY_AUF)
})
it('addressType with type box', () => {
const AddressTypeSchema = TypeBoxFromValibot(addressTypeSchema)
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"]')`))
})
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"]')`))
})
})
it('confirmedTransactionSchema', () => {

View File

@ -47,8 +47,8 @@ export const addressTypeSchema = v.pipe(
*/
export const accountTypeSchema = v.pipe(
v.union([
v.custom<AddressType>(isAddressType, 'expect AddressType'),
v.enum(AccountType, 'expect AccountType'),
v.custom<AddressType>(isAddressType, 'expect AddressType'),
]),
v.transform<AddressType | AccountType, AccountType>((value) => toAccountType(value)),
)

View File

@ -168,11 +168,21 @@ declare const validTimeoutDuration: unique symbol
export type TimeoutDuration = DurationSeconds & { [validTimeoutDuration]: true }
export const timeoutDurationSchema = v.pipe(
v.number('expect number type'),
v.minValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MIN, 'expect number >= 1 hour'),
v.maxValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MAX, 'expect number <= 3 months'),
v.transform<number, TimeoutDuration>(
(input: number) => new DurationSeconds(input) as TimeoutDuration,
v.union([
v.pipe(
v.number('expect number type'),
v.minValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MIN, 'expect number >= 1 hour'),
v.maxValue(LINKED_TRANSACTION_TIMEOUT_DURATION_MAX, 'expect number <= 3 months'),
),
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
},
),
)
@ -200,8 +210,16 @@ declare const validGradidoAmount: unique symbol
export type GradidoAmount = GradidoUnit & { [validGradidoAmount]: true }
export const gradidoAmountSchema = v.pipe(
amountSchema,
v.transform<Amount, GradidoAmount>(
(input: Amount) => 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

@ -0,0 +1,75 @@
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 { GradidoTransaction, KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js'
const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec'
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('../client/hiero/HieroClient', () => ({
HieroClient: {
getInstance: () => ({
sendMessage: (topicId: HieroId, transaction: GradidoTransaction) => {
return { receipt: { status: '0.0.21732' }, response: { transactionId: '0.0.6566984@1758029639.561157605' } }
},
}),
},
}))
mock.module('../config', () => ({
CONFIG: {
HOME_COMMUNITY_SEED: MemoryBlock.fromHex('0102030401060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe7'),
},
}))
beforeAll(() => {
KeyPairCacheManager.getInstance().setHomeCommunityTopicId(parse(hieroIdSchema, '0.0.21732'))
})
describe('Server', () => {
it('send register address transaction', async () => {
const transaction = {
user: {
communityTopicId: '0.0.21732',
account: {
userUuid,
accountNr: 0,
},
},
type: 'REGISTER_ADDRESS',
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',
},
}))
if (response.status !== 200) {
console.log(await response.text())
}
expect(response.status).toBe(200)
expect(await response.text()).toBe('0.0.6566984@1758029639.561157605')
})
})

View File

@ -1,6 +1,6 @@
import { TypeBoxFromValibot } from '@sinclair/typemap'
import { Type } from '@sinclair/typebox'
import { Elysia, status } from 'elysia'
import { Elysia, status, t } from 'elysia'
import { AddressType_NONE } from 'gradido-blockchain-js'
import { getLogger } from 'log4js'
import { parse } from 'valibot'
@ -11,7 +11,7 @@ import { KeyPairCalculation } from '../interactions/keyPairCalculation/KeyPairCa
import { SendToHieroContext } from '../interactions/sendToHiero/SendToHiero.context'
import { IdentifierAccount, identifierAccountSchema } from '../schemas/account.schema'
import { transactionSchema } from '../schemas/transaction.schema'
import { hieroTransactionIdSchema } from '../schemas/typeGuard.schema'
import { hieroIdSchema, hieroTransactionIdSchema } from '../schemas/typeGuard.schema'
import {
accountIdentifierSeedSchema,
accountIdentifierUserSchema,
@ -48,29 +48,22 @@ export const appRoutes = new Elysia()
.post(
'/sendTransaction',
async ({ body }) => {
console.log("sendTransaction was called")
return "0.0.123"
console.log(body)
console.log(parse(transactionSchema, body))
const transaction = parse(transactionSchema, body)
return await SendToHieroContext(transaction)
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
{
// body: TypeBoxFromValibot(transactionSchema),
body: Type.Object({
user: Type.Object({
communityUser: Type.Object({
uuid: Type.String({ format: 'uuid' }),
accountNr: Type.Optional(Type.String()), // optional/undefined
}),
communityUuid: Type.String({ format: 'uuid' }),
}),
createdAt: Type.String({ format: 'date-time' }),
accountType: Type.Literal('COMMUNITY_HUMAN'),
type: Type.Literal('REGISTER_ADDRESS'),
})
// response: TypeBoxFromValibot(hieroTransactionIdSchema),
body: TypeBoxFromValibot(transactionSchema),
response: t.Object({ transactionId: TypeBoxFromValibot(hieroTransactionIdSchema) }),
},
)