revert settlement in federation modul

This commit is contained in:
Claus-Peter Huebner 2023-08-30 15:42:18 +02:00
parent 99017802cd
commit d403087ed1
7 changed files with 248 additions and 116 deletions

View File

@ -17,6 +17,7 @@
},
"dependencies": {
"apollo-server-express": "^2.25.2",
"await-semaphore": "0.1.3",
"class-validator": "^0.13.2",
"cors": "2.8.5",
"cross-env": "^7.0.3",

View File

@ -2,10 +2,9 @@ import { registerEnumType } from 'type-graphql'
export enum PendingTransactionState {
NEW = 1,
WAIT_ON_PENDING = 2,
PENDING = 3,
WAIT_ON_CONFIRM = 4,
CONFIRMED = 5,
PENDING = 2,
SETTLED = 3,
REVERTED = 4,
}
registerEnumType(PendingTransactionState, {

View File

@ -14,6 +14,7 @@ import { fullName } from '@/graphql/util/fullName'
import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '../const/const'
import { checkTradingLevel } from '@/graphql/util/checkTradingLevel'
import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -146,7 +147,7 @@ export class SendCoinsResolver {
linkedUserGradidoID: userSenderIdentifier,
})
logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx)
if (pendingTx && pendingTx.amount === amount) {
if (pendingTx && pendingTx.amount === amount && pendingTx.memo === memo) {
logger.debug('XCom: revertSendCoins matching pendingTX for remove...')
try {
await pendingTx.remove()
@ -193,28 +194,6 @@ export class SendCoinsResolver {
): Promise<boolean> {
logger.debug(`settleSendCoins() via apiVersion=1_0 ...`)
try {
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneByOrFail({
communityUuid: communityReceiverIdentifier,
})
/*
if (!homeCom) {
throw new LogError(
`settleSendCoins with wrong communityReceiverIdentifier`,
communityReceiverIdentifier,
)
}
*/
// second check if receiver user exists in this community
const receiverUser = await DbUser.findOneByOrFail({ gradidoID: userReceiverIdentifier })
/*
if (!receiverUser) {
throw new LogError(
`settleSendCoins with unknown userReceiverIdentifier in the community=`,
homeCom.name,
)
}
*/
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: communityReceiverIdentifier,
userGradidoID: userReceiverIdentifier,
@ -227,12 +206,15 @@ export class SendCoinsResolver {
logger.debug('XCom: settleSendCoins found pendingTX=', pendingTx)
if (pendingTx && pendingTx.amount === amount && pendingTx.memo === memo) {
logger.debug('XCom: settleSendCoins matching pendingTX for settlement...')
try {
await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx)
logger.debug('XCom: settlePendingReceiveTransaction successfully...')
} catch (err) {
throw new LogError('Error in settlePendingReceiveTransaction: ', err)
}
const homeCom = await DbCommunity.findOneByOrFail({
communityUuid: communityReceiverIdentifier,
})
const receiverUser = await DbUser.findOneByOrFail({ gradidoID: userReceiverIdentifier })
await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx)
logger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successfull`)
return true
} else {
logger.debug(
'XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...',
@ -251,10 +233,83 @@ export class SendCoinsResolver {
userSenderName,
)
}
logger.debug(`settlePendingReceiveTransaction()-1_0... successfull`)
return true
} catch (err) {
throw new LogError(`Error in settlePendingReceiveTransaction: `, err)
}
}
@Mutation(() => Boolean)
async revertSettledSendCoins(
@Args()
{
communityReceiverIdentifier,
userReceiverIdentifier,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
}: SendCoinsArgs,
): Promise<boolean> {
try {
logger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`)
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: communityReceiverIdentifier,
})
if (!homeCom) {
throw new LogError(
`revertSettledSendCoins with wrong communityReceiverIdentifier`,
communityReceiverIdentifier,
)
}
// second check if receiver user exists in this community
const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier })
if (!receiverUser) {
throw new LogError(
`revertSettledSendCoins with unknown userReceiverIdentifier in the community=`,
homeCom.name,
)
}
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: communityReceiverIdentifier,
userGradidoID: userReceiverIdentifier,
state: PendingTransactionState.SETTLED,
typeId: TransactionTypeId.RECEIVE,
balanceDate: creationDate,
linkedUserCommunityUuid: communitySenderIdentifier,
linkedUserGradidoID: userSenderIdentifier,
})
logger.debug('XCom: revertSettledSendCoins found pendingTX=', pendingTx)
if (pendingTx && pendingTx.amount === amount && pendingTx.memo === memo) {
logger.debug('XCom: revertSettledSendCoins matching pendingTX for remove...')
try {
await revertSettledReceiveTransaction(homeCom, receiverUser, pendingTx)
logger.debug('XCom: revertSettledSendCoins pendingTX successfully')
} catch (err) {
throw new LogError('Error in revertSettledSendCoins of receiver: ', err)
}
} else {
logger.debug('XCom: revertSettledSendCoins NOT matching pendingTX...')
throw new LogError(
`Can't find in revertSettledSendCoins the pending receiver TX for args=`,
communityReceiverIdentifier,
userReceiverIdentifier,
PendingTransactionState.SETTLED,
TransactionTypeId.RECEIVE,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
)
}
logger.debug(`revertSendCoins()-1_0... successfull`)
return true
} catch (err) {
throw new LogError(`Error in revertSendCoins: `, err)
}
}
}

View File

@ -0,0 +1,139 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { User as DbUser } from '@entity/User'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { LogError } from '@/server/LogError'
import { federationLogger as logger } from '@/server/logger'
import { getLastTransaction } from '@/graphql/util/getLastTransaction'
import { TRANSACTIONS_LOCK } from '@/graphql/util/TRANSACTIONS_LOCK'
export async function revertSettledReceiveTransaction(
homeCom: DbCommunity,
receiverUser: DbUser,
pendingTx: DbPendingTransaction,
): Promise<boolean> {
// TODO: synchronisation with TRANSACTION_LOCK of backend-modul necessary!!!
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`start Transaction for write-access...`)
try {
logger.info('X-Com: revertSettledReceiveTransaction:', homeCom, receiverUser, pendingTx)
// ensure that no other pendingTx with the same sender or recipient exists
const openSenderPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW },
{ userGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.SETTLED },
{
linkedUserGradidoID: pendingTx.linkedUserGradidoID!,
state: PendingTransactionState.NEW,
},
{
linkedUserGradidoID: pendingTx.linkedUserGradidoID!,
state: PendingTransactionState.SETTLED,
},
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW },
{ userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.SETTLED },
{ linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.SETTLED },
],
})
if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) {
throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient')
}
const lastTransaction = await getLastTransaction(receiverUser.id)
// now the last Tx must be the equivalant to the pendingTX
if (
lastTransaction &&
lastTransaction.balance === pendingTx.balance &&
lastTransaction.balanceDate === pendingTx.balanceDate &&
lastTransaction.userGradidoID === pendingTx.userGradidoID &&
lastTransaction.userName === pendingTx.userName &&
lastTransaction.amount === pendingTx.amount &&
lastTransaction.memo === pendingTx.memo &&
lastTransaction.linkedUserGradidoID === pendingTx.linkedUserGradidoID &&
lastTransaction.linkedUserName === pendingTx.linkedUserName
) {
await queryRunner.manager.remove(dbTransaction, lastTransaction)
logger.debug(`X-Com: revert settlement receive Transaction removed:`, lastTransaction)
// and mark the pendingTx in the pending_transactions table as reverted
pendingTx.state = PendingTransactionState.REVERTED
await queryRunner.manager.save(DbPendingTransaction, pendingTx)
await queryRunner.commitTransaction()
logger.info(`commit revert settlement recipient Transaction successful...`)
} else {
// TODO: if the last TX is not equivelant to pendingTX, the transactions must be corrected in EXPERT-MODE
throw new LogError(
`X-Com: missmatching transaction order for revert settlement! lastTransation=${lastTransaction} != pendingTx=${pendingTx}`,
)
}
/*
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
await EVENT_TRANSACTION_RECEIVE(
recipient,
sender,
transactionReceive,
transactionReceive.amount,
)
*/
// trigger to send transaction via dlt-connector
// void sendTransactionsToDltConnector()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('X-Com: revert settlement recipient Transaction was not successful', e)
} finally {
await queryRunner.release()
releaseLock()
}
/*
void sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
email: recipient.emailContact.email,
language: recipient.language,
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
senderEmail: sender.emailContact.email,
transactionAmount: amount,
})
if (transactionLink) {
void sendTransactionLinkRedeemedEmail({
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.emailContact.email,
language: sender.language,
senderFirstName: recipient.firstName,
senderLastName: recipient.lastName,
senderEmail: recipient.emailContact.email,
transactionAmount: amount,
transactionMemo: memo,
})
}
logger.info(`finished executeTransaction successfully`)
} finally {
releaseLock()
}
*/
return true
}

View File

@ -2,7 +2,7 @@
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection, In } from '@dbTools/typeorm'
import { getConnection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import { Transaction as dbTransaction } from '@entity/Transaction'
@ -22,12 +22,13 @@ export async function settlePendingReceiveTransaction(
receiverUser: DbUser,
pendingTx: DbPendingTransaction,
): Promise<boolean> {
// TODO: synchronisation with TRANSACTION_LOCK of backend-modul necessary!!!
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`open Transaction to write...`)
logger.debug(`start Transaction for write-access...`)
try {
logger.info('X-Com: settlePendingReceiveTransaction:', homeCom, receiverUser, pendingTx)
@ -57,6 +58,7 @@ export async function settlePendingReceiveTransaction(
)
}
// transfer the pendingTx to the transactions table
const transactionReceive = new dbTransaction()
transactionReceive.typeId = pendingTx.typeId
transactionReceive.memo = pendingTx.memo
@ -86,85 +88,12 @@ export async function settlePendingReceiveTransaction(
await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
/*
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(
sender.id,
amount.mul(-1),
receivedCallDate,
transactionLink,
)
logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) {
throw new LogError('User has not enough GDD or amount is < 0', sendBalance)
}
// and mark the pendingTx in the pending_transactions table as settled
pendingTx.state = PendingTransactionState.SETTLED
await queryRunner.manager.save(DbPendingTransaction, pendingTx)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ')
logger.debug(`open Transaction to write...`)
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
transactionSend.userGradidoID = sender.gradidoID
transactionSend.userName = fullName(sender.firstName, sender.lastName)
transactionSend.linkedUserId = recipient.id
transactionSend.linkedUserGradidoID = recipient.gradidoID
transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName)
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.userGradidoID = recipient.gradidoID
transactionReceive.userName = fullName(recipient.firstName, recipient.lastName)
transactionReceive.linkedUserId = sender.id
transactionReceive.linkedUserGradidoID = sender.gradidoID
transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName)
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug('send Transaction updated', transactionSend)
if (transactionLink) {
logger.info('transactionLink', transactionLink)
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
dbTransactionLink,
{ id: transactionLink.id },
transactionLink,
)
}
*/
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
logger.info(`commit recipient Transaction successful...`)
/*
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
@ -180,7 +109,7 @@ export async function settlePendingReceiveTransaction(
// void sendTransactionsToDltConnector()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Transaction was not successful', e)
throw new LogError('X-Com: recipient Transaction was not successful', e)
} finally {
await queryRunner.release()
releaseLock()

View File

@ -53,13 +53,17 @@
// "@model/*": ["src/graphql/model/*"],
"@repository/*": ["src/typeorm/repository/*"],
"@test/*": ["test/*"],
/* common */
"@common/*": ["../common/src/*"],
"@email/*": ["../common/scr/email/*"],
"@event/*": ["../common/src/event/*"],
/* external */
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
"typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

View File

@ -1529,6 +1529,11 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
await-semaphore@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
babel-jest@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444"