add backendTransaction table, update dltConnectorClient code and test

This commit is contained in:
einhorn_b 2023-12-22 15:19:21 +01:00
parent bcaae7002c
commit ba13144ad3
16 changed files with 2717 additions and 103 deletions

2533
backend/log.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -25,8 +25,6 @@ let testEnv: {
jest.mock('graphql-request', () => {
const originalModule = jest.requireActual('graphql-request')
let testCursor = 0
return {
__esModule: true,
...originalModule,
@ -38,30 +36,11 @@ jest.mock('graphql-request', () => {
// why not using mockResolvedValueOnce or mockReturnValueOnce?
// I have tried, but it didn't work and return every time the first value
request: jest.fn().mockImplementation(() => {
testCursor++
if (testCursor === 4) {
return Promise.resolve(
// invalid, is 33 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A',
},
},
)
} else if (testCursor === 5) {
throw Error('Connection error')
} else {
return Promise.resolve(
// valid, is 32 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
},
)
}
return Promise.resolve({
transmitTransaction: {
succeed: true,
},
})
}),
}
}),
@ -134,7 +113,10 @@ describe('transmitTransaction', () => {
const localTransaction = new DbTransaction()
localTransaction.typeId = 12
try {
await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction)
await DltConnectorClient.getInstance()?.transmitTransaction(
localTransaction,
'senderCommunityUUID',
)
} catch (e) {
expect(e).toMatchObject(
new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()),

View File

@ -6,6 +6,7 @@ import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { Contribution } from '@entity/Contribution'
const sendTransaction = gql`
mutation ($input: TransactionInput!) {
@ -125,15 +126,14 @@ export class DltConnectorClient {
transaction: DbTransaction,
type: 'sender' | 'recipient',
): Promise<string> {
const confirmingUserId = transaction.contribution?.confirmedBy
let confirmingUser: DbUser | undefined
let confirmingUserId: number | undefined
logger.info('confirming user id', confirmingUserId)
if (confirmingUserId) {
confirmingUser = await DbUser.findOneOrFail({ where: { id: confirmingUserId } })
}
switch (transaction.typeId) {
case TransactionTypeId.CREATION:
if (!confirmingUserId || !confirmingUser) {
confirmingUserId = (
await Contribution.findOneOrFail({ where: { transactionId: transaction.id } })
).confirmedBy
if (!confirmingUserId) {
throw new LogError(
"couldn't find id of confirming moderator for contribution transaction!",
)
@ -170,8 +170,8 @@ export class DltConnectorClient {
protected async getCorrectUserIdentifier(
transaction: DbTransaction,
senderCommunityUuid: string,
recipientCommunityUuid: string,
type: 'sender' | 'recipient',
recipientCommunityUuid?: string,
): Promise<UserIdentifier> {
// sender and receiver user on creation transaction
// sender user on send transaction (SEND and RECEIVE)
@ -195,7 +195,7 @@ export class DltConnectorClient {
public async transmitTransaction(
transaction: DbTransaction,
senderCommunityUuid: string,
recipientCommunityUuid: string,
recipientCommunityUuid?: string,
): Promise<boolean> {
const typeString = getTransactionTypeString(transaction.typeId)
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
@ -205,14 +205,14 @@ export class DltConnectorClient {
senderUser: await this.getCorrectUserIdentifier(
transaction,
senderCommunityUuid,
recipientCommunityUuid,
'sender',
recipientCommunityUuid,
),
recipientUser: await this.getCorrectUserIdentifier(
transaction,
senderCommunityUuid,
recipientCommunityUuid,
'recipient',
recipientCommunityUuid,
),
amount: amountString,
type: typeString,

View File

@ -24,6 +24,15 @@ import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector'
import { Contribution } from '@entity/Contribution'
import { User } from '@entity/User'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { creations } from '@/seeds/creation'
import { creationFactory } from '@/seeds/factory/creation'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
/*
// Mock the GraphQLClient
@ -423,9 +432,17 @@ describe('create and send Transactions to DltConnector', () => {
describe('with 3 creations and active dlt-connector', () => {
it('found 3 dlt-transactions', async () => {
txCREATION1 = await createTxCREATION1(false)
txCREATION2 = await createTxCREATION2(false)
txCREATION3 = await createTxCREATION3(false)
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
@ -435,10 +452,7 @@ describe('create and send Transactions to DltConnector', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
sendTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
sendTransaction: { succeed: true },
},
} as Response<unknown>
})
@ -464,7 +478,7 @@ describe('create and send Transactions to DltConnector', () => {
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[0].id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
@ -472,7 +486,7 @@ describe('create and send Transactions to DltConnector', () => {
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[1].id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
@ -480,7 +494,7 @@ describe('create and send Transactions to DltConnector', () => {
expect.objectContaining({
id: expect.any(Number),
transactionId: transactions[2].id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
@ -514,10 +528,7 @@ describe('create and send Transactions to DltConnector', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
sendTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
sendTransaction: { succeed: true },
},
} as Response<unknown>
})
@ -569,7 +580,7 @@ describe('create and send Transactions to DltConnector', () => {
expect.objectContaining({
id: expect.any(Number),
transactionId: txSEND1to2.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,
@ -577,7 +588,7 @@ describe('create and send Transactions to DltConnector', () => {
expect.objectContaining({
id: expect.any(Number),
transactionId: txRECEIVE2From1.id,
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
messageId: 'sended',
verified: false,
createdAt: expect.any(Date),
verifiedAt: null,

View File

@ -23,7 +23,6 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
if (!senderCommunityUuid) {
throw new Error('Cannot find community uuid of home community')
}
const recipientCommunityUuid = ''
if (dltConnector) {
logger.debug('with sending to DltConnector...')
const dltTransactions = await DltTransaction.find({
@ -37,27 +36,23 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
continue
}
try {
const messageId = await dltConnector.transmitTransaction(
const result = await dltConnector.transmitTransaction(
dltTx.transaction,
senderCommunityUuid,
recipientCommunityUuid,
)
const dltMessageId = Buffer.from(messageId, 'hex')
if (dltMessageId.length !== 32) {
logger.error(
'Error dlt message id is invalid: %s, should by 32 Bytes long in binary after converting from hex',
dltMessageId,
)
return
// message id isn't known at this point of time, because transaction will not direct sended to iota,
// it will first go to db and then sended, if no transaction is in db before
if (result) {
dltTx.messageId = 'sended'
await DltTransaction.save(dltTx)
logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id)
}
dltTx.messageId = dltMessageId.toString('hex')
await DltTransaction.save(dltTx)
logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id)
} catch (e) {
logger.error(
`error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`,
e,
)
console.log('error', e)
}
}
} else {

View File

@ -0,0 +1,13 @@
import { BackendTransaction } from '@entity/BackendTransaction'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
export class BackendTransactionFactory {
public static createFromTransactionDraft(transactionDraft: TransactionDraft): BackendTransaction {
const backendTransaction = BackendTransaction.create()
backendTransaction.backendTransactionId = transactionDraft.backendTransactionId
backendTransaction.typeId = transactionDraft.type
backendTransaction.createdAt = new Date(transactionDraft.createdAt)
return backendTransaction
}
}

View File

@ -0,0 +1,7 @@
import { BackendTransaction } from '@entity/BackendTransaction'
import { getDataSource } from '@/typeorm/DataSource'
export const BackendTransactionRepository = getDataSource()
.getRepository(BackendTransaction)
.extend({})

View File

@ -4,11 +4,13 @@ import { Transaction } from '@entity/Transaction'
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { LogError } from '@/server/LogError'
import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter'
import { AccountRepository } from './Account.repository'
import { BackendTransactionFactory } from './BackendTransaction.factory'
import { CommunityRepository } from './Community.repository'
import { TransactionBodyBuilder } from './proto/TransactionBody.builder'
@ -91,8 +93,13 @@ export class TransactionBuilder {
return this
}
public setBackendTransactionId(backendTransactionId: number): TransactionBuilder {
this.transaction.backendTransactionId = backendTransactionId
public addBackendTransaction(transactionDraft: TransactionDraft): TransactionBuilder {
if (!this.transaction.backendTransactions) {
this.transaction.backendTransactions = []
}
this.transaction.backendTransactions.push(
BackendTransactionFactory.createFromTransactionDraft(transactionDraft),
)
return this
}

View File

@ -20,30 +20,21 @@ export const TransactionRepository = getDataSource()
relations: { signingAccount: true },
})
},
async findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<{
existingTransactions: Transaction[]
missingMessageIdsHex: string[]
}> {
const existingTransactions = await this.createQueryBuilder('Transaction')
findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<Transaction[]> {
return this.createQueryBuilder('Transaction')
.where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', {
messageIDs: messageIDsHex,
})
.leftJoinAndSelect('Transaction.community', 'Community')
.leftJoinAndSelect('Transaction.otherCommunity', 'OtherCommunity')
.leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount')
.leftJoinAndSelect('Transaction.backendTransactions', 'BackendTransactions')
.leftJoinAndSelect('RecipientAccount.user', 'RecipientUser')
.leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount')
.leftJoinAndSelect('SigningAccount.user', 'SigningUser')
.getMany()
const foundMessageIds = existingTransactions
.map((recipe) => recipe.iotaMessageId?.toString('hex'))
.filter((messageId) => !!messageId)
// find message ids for which we don't already have a transaction recipe
const missingMessageIdsHex = messageIDsHex.filter(
(id: string) => !foundMessageIds.includes(id),
)
return { existingTransactions, missingMessageIdsHex }
},
async removeConfirmedTransaction(transactions: Transaction[]): Promise<Transaction[]> {
removeConfirmedTransaction(transactions: Transaction[]): Transaction[] {
return transactions.filter(
(transaction: Transaction) =>
transaction.runningHash === undefined || transaction.runningHash.length === 0,

View File

@ -4,8 +4,8 @@ import { TransactionDraft } from '@input/TransactionDraft'
import { TransactionRepository } from '@/data/Transaction.repository'
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
import { LogError } from '@/server/LogError'
import { TransactionErrorType } from '../enum/TransactionErrorType'
import { TransactionError } from '../model/TransactionError'
import { TransactionRecipe } from '../model/TransactionRecipe'
import { TransactionResult } from '../model/TransactionResult'
@ -21,22 +21,28 @@ export class TransactionResolver {
try {
await createTransactionRecipeContext.run()
const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe()
await transactionRecipe.save()
// check if a transaction with this signature already exist
const existingRecipe = await TransactionRepository.findBySignature(
transactionRecipe.signature,
)
if (existingRecipe) {
// transaction recipe with this signature already exist, we need only to store the backendTransaction
if (transactionRecipe.backendTransactions.length !== 1) {
throw new LogError('unexpected backend transaction count', {
count: transactionRecipe.backendTransactions.length,
transactionId: transactionRecipe.id,
})
}
const backendTransaction = transactionRecipe.backendTransactions[0]
backendTransaction.transactionId = transactionRecipe.id
await backendTransaction.save()
} else {
// we can store the transaction and with that automatic the backend transaction
await transactionRecipe.save()
}
return new TransactionResult(new TransactionRecipe(transactionRecipe))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error.code === 'ER_DUP_ENTRY') {
const existingRecipe = await TransactionRepository.findBySignature(
createTransactionRecipeContext.getTransactionRecipe().signature,
)
if (!existingRecipe) {
throw new TransactionError(
TransactionErrorType.LOGIC_ERROR,
"recipe cannot be added because signature exist but couldn't load this existing receipt",
)
}
return new TransactionResult(new TransactionRecipe(existingRecipe))
}
if (error instanceof TransactionError) {
return new TransactionResult(error)
} else {

View File

@ -40,11 +40,11 @@ export class TransactionRecipeRole {
.setSigningAccount(signingAccount)
.setRecipientAccount(recipientAccount)
.fromTransactionDraft(transactionDraft)
// build transaction entity
// build transaction entity
this.transactionBuilder
.fromTransactionBodyBuilder(transactionBodyBuilder)
.setBackendTransactionId(transactionDraft.backendTransactionId)
.addBackendTransaction(transactionDraft)
await this.transactionBuilder.setSenderCommunityFromSenderUser(senderUser)
if (recipientUser.communityUuid !== senderUser.communityUuid) {
await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser)

View File

@ -0,0 +1,45 @@
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, JoinColumn } from 'typeorm'
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Transaction } from '../Transaction'
@Entity('backend_transactions')
export class BackendTransaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
id: number
@Column({ name: 'backend_transaction_id', type: 'bigint', unsigned: true, nullable: true })
backendTransactionId?: number
@ManyToOne(() => Transaction, (transaction) => transaction.backendTransactions)
@JoinColumn({ name: 'transaction_id' })
transaction?: Transaction
@Column({ name: 'transaction_id', type: 'bigint', unsigned: true, nullable: true })
transactionId?: number
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
// account balance based on creation date
@Column({
name: 'balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
balance?: Decimal
@Column({ name: 'created_at', type: 'datetime', precision: 3 })
createdAt: Date
// use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
@Column({ name: 'verifiedOnBackend', type: 'tinyint', default: false })
verifiedOnBackend: boolean
}

View File

@ -6,12 +6,14 @@ import {
OneToOne,
JoinColumn,
BaseEntity,
OneToMany,
} from 'typeorm'
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Community } from '../Community'
import { BackendTransaction } from '../BackendTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {
@ -21,9 +23,6 @@ export class Transaction extends BaseEntity {
@Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true })
iotaMessageId?: Buffer
@Column({ name: 'backend_transaction_id', type: 'bigint', unsigned: true, nullable: true })
backendTransactionId?: number
@OneToOne(() => Transaction)
// eslint-disable-next-line no-use-before-define
paringTransaction?: Transaction
@ -120,4 +119,10 @@ export class Transaction extends BaseEntity {
// use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
@OneToMany(() => BackendTransaction, (backendTransaction) => backendTransaction.transaction, {
cascade: ['insert', 'update'],
})
@JoinColumn({ name: 'transaction_id' })
backendTransactions: BackendTransaction[]
}

View File

@ -0,0 +1 @@
export { BackendTransaction } from './0003-refactor_transaction_recipe/BackendTransaction'

View File

@ -1,5 +1,6 @@
import { Account } from './Account'
import { AccountCommunity } from './AccountCommunity'
import { BackendTransaction } from './BackendTransaction'
import { Community } from './Community'
import { InvalidTransaction } from './InvalidTransaction'
import { Migration } from './Migration'
@ -9,6 +10,7 @@ import { User } from './User'
export const entities = [
AccountCommunity,
Account,
BackendTransaction,
Community,
InvalidTransaction,
Migration,

View File

@ -34,7 +34,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
`CREATE TABLE \`transactions\` (
\`id\` bigint unsigned NOT NULL AUTO_INCREMENT,
\`iota_message_id\` varbinary(32) NULL DEFAULT NULL,
\`backend_transaction_id\` bigint unsigned NULL DEFAULT NULL,
\`paring_transaction_id\` bigint unsigned NULL DEFAULT NULL,
\`signing_account_id\` int unsigned NULL DEFAULT NULL,
\`recipient_account_id\` int unsigned NULL DEFAULT NULL,
@ -62,6 +61,22 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
`,
)
await queryFn(
`CREATE TABLE \`backend_transactions\` (
\`id\` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
\`backend_transaction_id\` BIGINT UNSIGNED NOT NULL,
\`transaction_id\` BIGINT UNSIGNED NULL DEFAULT NULL,
\`type_id\` INT UNSIGNED NOT NULL,
\`balance\` DECIMAL(40, 20) NULL DEFAULT NULL,
\`created_at\` DATETIME(3) NOT NULL,
\`confirmed_at\` DATETIME NULL DEFAULT NULL,
\`verifiedOnBackend\` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (\`id\`),
FOREIGN KEY (\`transaction_id\`) REFERENCES transactions(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`,
)
await queryFn(`ALTER TABLE \`communities\` ADD UNIQUE(\`iota_topic\`);`)
await queryFn(`ALTER TABLE \`users\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL;`)
@ -126,6 +141,7 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
await queryFn(`ALTER TABLE \`invalid_transactions\` DROP INDEX \`iota_message_id\`;`)
await queryFn(`ALTER TABLE \`invalid_transactions\` ADD INDEX(\`iota_message_id\`); `)
await queryFn(`DROP TABLE \`transactions\`;`)
await queryFn(`DROP TABLE \`backend_transactions\`;`)
await queryFn(
`ALTER TABLE \`users\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);`,