mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
simplify backend code for dlt connector calling
This commit is contained in:
parent
eb3bf5e904
commit
5ea4aec922
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
}
|
||||
107
backend/src/apis/dltConnector/index.ts
Normal file
107
backend/src/apis/dltConnector/index.ts
Normal 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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
"log4js": "^6.9.1",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^8.3.2",
|
||||
"valibot": "^1.1.0",
|
||||
"valibot": "1.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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'),
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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 ||
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)),
|
||||
)
|
||||
|
||||
@ -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
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
75
dlt-connector/src/server/index.test.ts
Normal file
75
dlt-connector/src/server/index.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@ -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) }),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user