mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
fix error with loop, implement dlt support for linked transactions
This commit is contained in:
parent
4ab8d6b83a
commit
4875621699
@ -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 (
|
||||
|
||||
7
backend/src/apis/dltConnector/model/IdentifierSeed.ts
Normal file
7
backend/src/apis/dltConnector/model/IdentifierSeed.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class IdentifierSeed {
|
||||
seed: string
|
||||
|
||||
constructor(seed: string) {
|
||||
this.seed = seed
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)}`)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user