fix error with loop, implement dlt support for linked transactions

This commit is contained in:
einhornimmond 2024-11-12 21:15:36 +01:00
parent 4ab8d6b83a
commit 4875621699
11 changed files with 194 additions and 77 deletions

View File

@ -1,4 +1,5 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
import { TransactionLink } from '@entity/TransactionLink'
import { User } from '@entity/User'
import { gql, GraphQLClient } from 'graphql-request'
// eslint-disable-next-line import/named, n/no-extraneous-import
@ -99,8 +100,10 @@ export class DltConnectorClient {
return DltConnectorClient.instance
}
private getTransactionParams(input: DbTransaction | User): TransactionDraft | UserAccountDraft {
if (input instanceof DbTransaction) {
private getTransactionParams(
input: DbTransaction | User | TransactionLink,
): TransactionDraft | UserAccountDraft {
if (input instanceof DbTransaction || input instanceof TransactionLink) {
return new TransactionDraft(input)
} else if (input instanceof User) {
return new UserAccountDraft(input)
@ -138,7 +141,7 @@ export class DltConnectorClient {
* and update dltTransactionId of transaction in db with iota message id
*/
public async transmitTransaction(
transaction: DbTransaction | User,
transaction: DbTransaction | User | TransactionLink,
): Promise<TransactionResult | undefined> {
// we don't need the receive transactions, there contain basically the same data as the send transactions
if (

View File

@ -0,0 +1,7 @@
export class IdentifierSeed {
seed: string
constructor(seed: string) {
this.seed = seed
}
}

View File

@ -1,38 +1,52 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { Transaction } from '@entity/Transaction'
import { TransactionLink } from '@entity/TransactionLink'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { IdentifierSeed } from './IdentifierSeed'
import { UserIdentifier } from './UserIdentifier'
export class TransactionDraft {
user: UserIdentifier
linkedUser: UserIdentifier
linkedUser: UserIdentifier | IdentifierSeed
amount: string
type: string
createdAt: string
// only for creation transactions
targetDate?: string
// only for transaction links
timeoutDate?: string
constructor(transaction: Transaction) {
if (
!transaction.linkedUserGradidoID ||
!transaction.linkedUserCommunityUuid ||
!transaction.userCommunityUuid
) {
throw new LogError(
`missing necessary field in transaction: ${transaction.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`,
)
}
this.user = new UserIdentifier(transaction.userGradidoID, transaction.userCommunityUuid)
this.linkedUser = new UserIdentifier(
transaction.linkedUserGradidoID,
transaction.linkedUserCommunityUuid,
)
constructor(transaction: Transaction | TransactionLink) {
this.amount = transaction.amount.abs().toString()
this.type = TransactionTypeId[transaction.typeId]
this.createdAt = transaction.balanceDate.toISOString()
this.targetDate = transaction.creationDate?.toISOString()
if (transaction instanceof Transaction) {
if (
!transaction.linkedUserGradidoID ||
!transaction.linkedUserCommunityUuid ||
!transaction.userCommunityUuid
) {
throw new LogError(
`missing necessary field in transaction: ${transaction.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`,
)
}
this.user = new UserIdentifier(transaction.userGradidoID, transaction.userCommunityUuid)
this.linkedUser = new UserIdentifier(
transaction.linkedUserGradidoID,
transaction.linkedUserCommunityUuid,
)
this.createdAt = transaction.balanceDate.toISOString()
this.targetDate = transaction.creationDate?.toISOString()
this.type = TransactionTypeId[transaction.typeId]
} else if (transaction instanceof TransactionLink) {
const user = transaction.user
this.user = new UserIdentifier(user.gradidoID, user.communityUuid)
this.linkedUser = new IdentifierSeed(transaction.code)
this.createdAt = transaction.createdAt.toISOString()
this.type = TransactionTypeId[TransactionTypeId.LINK_SUMMARY]
this.timeoutDate = transaction.validUntil.toISOString()
}
}
}

View File

@ -1,6 +1,9 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { backendLogger as logger } from '@/server/logger'
import { BaseEntity, EntityPropertyNotFoundError, EntityTarget, OrderByCondition, SelectQueryBuilder } from '@dbTools/typeorm'
import { DltTransaction } from '@entity/DltTransaction'
import { DltUser } from '@entity/DltUser'
import { Transaction } from '@entity/Transaction'
import { TransactionLink } from '@entity/TransactionLink'
import { User } from '@entity/User'
// eslint-disable-next-line import/named, n/no-extraneous-import
import { FetchError } from 'node-fetch'
@ -8,11 +11,11 @@ import { FetchError } from 'node-fetch'
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult'
import { backendLogger as logger } from '@/server/logger'
import {
InterruptiveSleepManager,
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
} from '@/util/InterruptiveSleepManager'
import { LogError } from '@/server/LogError'
let isLoopRunning = true
@ -20,70 +23,105 @@ export const stopSendTransactionsToDltConnector = (): void => {
isLoopRunning = false
}
function logTransactionResult(
type: 'dltUser' | 'dltTransaction',
data: { id: number; messageId: string; error: string | null },
): void {
interface NextPendingTransactionQueries {
lastTransactionQuery: SelectQueryBuilder<Transaction>
lastUserQuery: SelectQueryBuilder<User>
lastTransactionLinkQuery: SelectQueryBuilder<TransactionLink>
}
function logTransactionResult(data: { id: number; messageId: string; error: string | null }): void {
if (data.error) {
logger.error(`Store ${type} with error: id=${data.id}, error=${data.error}`)
logger.error(`Store dltTransaction with error: id=${data.id}, error=${data.error}`)
} else {
logger.info(`Store ${type}: messageId=${data.messageId}, id=${data.id}`)
logger.info(`Store dltTransaction: messageId=${data.messageId}, id=${data.id}`)
}
}
async function saveTransactionResult(
pendingTransaction: User | Transaction,
pendingTransaction: User | Transaction | TransactionLink,
messageId: string,
error: string | null,
): Promise<void> {
const dltTransaction = DltTransaction.create()
dltTransaction.messageId = messageId
dltTransaction.error = error
if (pendingTransaction instanceof User) {
const dltUser = DltUser.create()
dltUser.userId = pendingTransaction.id
dltUser.messageId = messageId
dltUser.error = error
await DltUser.save(dltUser)
logTransactionResult('dltUser', dltUser)
dltTransaction.userId = pendingTransaction.id
} else if (pendingTransaction instanceof Transaction) {
const dltTransaction = DltTransaction.create()
dltTransaction.transactionId = pendingTransaction.id
dltTransaction.messageId = messageId
dltTransaction.error = error
await DltTransaction.save(dltTransaction)
logTransactionResult('dltTransaction', dltTransaction)
} else if (pendingTransaction instanceof TransactionLink) {
dltTransaction.transactionLinkId = pendingTransaction.id
}
await DltTransaction.save(dltTransaction)
logTransactionResult(dltTransaction)
}
async function findNextPendingTransaction(): Promise<Transaction | User | null> {
const lastTransactionPromise: Promise<Transaction | null> = Transaction.createQueryBuilder()
.leftJoin(DltTransaction, 'dltTransaction', 'Transaction.id = dltTransaction.transactionId')
.where('dltTransaction.transaction_id IS NULL')
// eslint-disable-next-line camelcase
.orderBy({ balance_date: 'ASC', Transaction_id: 'ASC' })
.limit(1)
.getOne()
const lastUserPromise: Promise<User | null> = User.createQueryBuilder()
.leftJoin(DltUser, 'dltUser', 'User.id = dltUser.userId')
.where('dltUser.user_id IS NULL')
// eslint-disable-next-line camelcase
.orderBy({ User_created_at: 'ASC', User_id: 'ASC' })
.limit(1)
.getOne()
const results = await Promise.all([lastTransactionPromise, lastUserPromise])
if (results[0] && results[1]) {
return results[0].balanceDate < results[1].createdAt ? results[0] : results[1]
} else if (results[0]) {
return results[0]
} else if (results[1]) {
return results[1]
async function findNextPendingTransaction(): Promise<Transaction | User | TransactionLink | null> {
// Helper function to avoid code repetition
const createQueryForPendingItems = (
qb: SelectQueryBuilder<Transaction | User | TransactionLink>,
joinCondition: string,
orderBy: OrderByCondition,
): Promise<Transaction | User | TransactionLink | null> => {
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)
.limit(1)
.getOne()
}
return null
const lastTransactionPromise = createQueryForPendingItems(
Transaction.createQueryBuilder(),
'Transaction.id = dltTransaction.transactionId',
// eslint-disable-next-line camelcase
{ balance_date: 'ASC', Transaction_id: 'ASC' },
)
const lastUserPromise = createQueryForPendingItems(
User.createQueryBuilder(),
'User.id = dltTransaction.userId',
// eslint-disable-next-line camelcase
{ User_created_at: 'ASC', User_id: 'ASC' },
)
const lastTransactionLinkPromise = createQueryForPendingItems(
TransactionLink.createQueryBuilder().leftJoinAndSelect('transactionLink.user', 'user'),
'TransactionLink.id = dltTransaction.transactionLinkId',
// eslint-disable-next-line camelcase
{ TransactionLinkId_created_at: 'ASC', User_id: 'ASC' },
)
const results = await Promise.all([
lastTransactionPromise,
lastUserPromise,
lastTransactionLinkPromise,
])
results.sort((a, b) => {
const getTime = (input: Transaction | User | TransactionLink | null) => {
if (!input) return Infinity
if (input instanceof Transaction) {
return input.balanceDate.getTime()
} else if (input instanceof User || input instanceof TransactionLink) {
return input.createdAt.getTime()
}
return Infinity
}
return getTime(a) - getTime(b)
})
return results[0] ?? null
}
async function processPendingTransactions(dltConnector: DltConnectorClient): Promise<void> {
let pendingTransaction: Transaction | User | null = null
while ((pendingTransaction = await findNextPendingTransaction())) {
let pendingTransaction: Transaction | User | TransactionLink | null = null
do {
pendingTransaction = await findNextPendingTransaction()
if (!pendingTransaction) {
return
}
let result: TransactionResult | undefined
let messageId = ''
let error: string | null = null
@ -103,7 +141,7 @@ async function processPendingTransactions(dltConnector: DltConnectorClient): Pro
}
await saveTransactionResult(pendingTransaction, messageId, error)
}
} while (pendingTransaction)
}
export async function sendTransactionsToDltConnector(): Promise<void> {
@ -114,9 +152,11 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
isLoopRunning = false
return
}
logger.info('Starting sendTransactionsToDltConnector task')
// define outside of loop for reuse and reducing gb collection
// const queries = getFindNextPendingTransactionQueries()
// eslint-disable-next-line no-unmodified-loop-condition
while (isLoopRunning) {
try {
@ -127,6 +167,9 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
1000,
)
} catch (e) {
if (e instanceof EntityPropertyNotFoundError) {
throw new LogError(e.message, e.stack)
}
// 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)}`)

View File

@ -38,8 +38,6 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate'
import { sendTransactionsToDltConnector } from '../../apis/dltConnector/sendTransactionsToDltConnector'
import { executeTransaction } from './TransactionResolver'
import { getUserCreation, validateContribution } from './util/creations'
import { getLastTransaction } from './util/getLastTransaction'
@ -311,8 +309,6 @@ export class TransactionLinkResolver {
} finally {
releaseLock()
}
// trigger to send transaction via dlt-connector
void sendTransactionsToDltConnector()
return true
} else {
const now = new Date()

View File

@ -1,7 +1,7 @@
import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransactionsToDltConnector'
import { CONFIG } from './config'
import { startValidateCommunities } from './federation/validateCommunities'
import { createServer } from './server/createServer'
import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransactionsToDltConnector'
async function main() {
const { app } = await createServer()

View File

@ -10,6 +10,7 @@ import {
} from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { DltTransaction } from '../DltTransaction'
import { User } from '../User'
@Entity('transaction_links')
export class TransactionLink extends BaseEntity {
@ -71,4 +72,8 @@ export class TransactionLink extends BaseEntity {
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionLinkId)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionLinkId' })
dltTransaction?: DltTransaction | null
@OneToOne(() => User, (user) => user.transactionLink)
@JoinColumn({ name: 'userId' })
user: User
}

View File

@ -17,6 +17,7 @@ import { UserRole } from '../UserRole'
import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer'
import { Community } from '../Community'
import { DltTransaction } from '../DltTransaction'
import { TransactionLink } from './TransactionLink'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@ -178,4 +179,8 @@ export class User extends BaseEntity {
@OneToOne(() => DltTransaction, (dlt) => dlt.userId)
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
dltTransaction?: DltTransaction | null
@OneToOne(() => TransactionLink, (transactionLink) => transactionLink.userId)
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
transactionLink?: TransactionLink | null
}

View File

@ -35,4 +35,9 @@ export class TransactionDraft {
@Field(() => String, { nullable: true })
@isValidDateString()
targetDate?: string
// only for transaction links
@Field(() => String, { nullable: true })
@isValidDateString()
timeoutDate?: string
}

View File

@ -0,0 +1,39 @@
import { GradidoTransactionBuilder, GradidoTransfer, TransferAmount } from 'gradido-blockchain-js'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
import { TransferTransactionRole } from './TransferTransaction.role'
export class DeferredTransferTransactionRole extends TransferTransactionRole {
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const senderKeyPair = await KeyPairCalculation(this.self.user)
const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser)
if (!this.self.timeoutDate) {
throw new LogError('timeoutDate date missing for deferred transfer transaction')
}
builder
.setCreatedAt(new Date(this.self.createdAt))
.setMemo('dummy memo for transfer')
.setDeferredTransfer(
new GradidoTransfer(
new TransferAmount(senderKeyPair.getPublicKey(), this.self.amount.toString()),
recipientKeyPair.getPublicKey(),
),
new Date(this.self.timeoutDate),
)
const senderCommunity = this.self.user.communityUuid
const recipientCommunity = this.self.linkedUser.communityUuid
if (senderCommunity !== recipientCommunity) {
// we have a cross group transaction
builder
.setSenderCommunity(uuid4ToHash(senderCommunity).convertToHex())
.setRecipientCommunity(uuid4ToHash(recipientCommunity).convertToHex())
}
builder.sign(senderKeyPair)
return builder
}
}

View File

@ -8,7 +8,7 @@ import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.con
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class TransferTransactionRole extends AbstractTransactionRole {
constructor(private self: TransactionDraft) {
constructor(protected self: TransactionDraft) {
super()
}