first draft for settlePendingReceiverTransaction

This commit is contained in:
Claus-Peter Huebner 2023-08-29 00:49:51 +02:00
parent 1016dd071a
commit 6acf2f3c5b
9 changed files with 427 additions and 13 deletions

View File

@ -7,6 +7,7 @@ import { backendLogger as logger } from '@/server/logger'
import { SendCoinsArgs } from './model/SendCoinsArgs'
import { revertSendCoins } from './query/revertSendCoins'
import { voteForSendCoins } from './query/voteForSendCoins'
import { settleSendCoins } from './query/settleSendCoins'
// eslint-disable-next-line camelcase
export class SendCoinsClient {
@ -72,22 +73,20 @@ export class SendCoinsClient {
}
}
/*
commitSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
logger.debug(`X-Com: commitSendCoins against endpoint='${this.endpoint}'...`)
settleSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
logger.debug(`X-Com: settleSendCoins against endpoint='${this.endpoint}'...`)
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(commitSendCoins, { args })
const { data } = await this.client.rawRequest(settleSendCoins, { args })
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!data?.commitSendCoins?.acknowledged) {
logger.warn('X-Com: commitSendCoins without response data from endpoint', this.endpoint)
logger.warn('X-Com: settleSendCoins without response data from endpoint', this.endpoint)
return false
}
logger.debug(`X-Com: commitSendCoins successful from endpoint=${this.endpoint}`)
logger.debug(`X-Com: settleSendCoins successful from endpoint=${this.endpoint}`)
return true
} catch (err) {
throw new LogError(`X-Com: commitSendCoins failed for endpoint=${this.endpoint}`, err)
throw new LogError(`X-Com: settleSendCoins failed for endpoint=${this.endpoint}`, err)
}
}
*/
}

View File

@ -0,0 +1,25 @@
import { gql } from 'graphql-request'
export const settleSendCoins = gql`
mutation (
$communityReceiverIdentifier: String!
$userReceiverIdentifier: String!
$creationDate: Date!
$amount: Decimal!
$memo: String!
$communitySenderIdentifier: String!
$userSenderIdentifier: String!
$userSenderName: String!
) {
settleSendCoins(
communityReceiverIdentifier: $communityReceiverIdentifier
userReceiverIdentifier: $userReceiverIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
communitySenderIdentifier: $communitySenderIdentifier
userSenderIdentifier: $userSenderIdentifier
userSenderName: $userSenderName
)
}
`

View File

@ -3,6 +3,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection, In } from '@dbTools/typeorm'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User as dbUser } from '@entity/User'
@ -12,6 +13,7 @@ import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
import { Paginated } from '@arg/Paginated'
import { TransactionSendArgs } from '@arg/TransactionSendArgs'
import { Order } from '@enum/Order'
import { PendingTransactionState } from '@enum/PendingTransactionState'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList'
@ -52,6 +54,22 @@ export const executeTransaction = async (
try {
logger.info('executeTransaction', amount, memo, sender, recipient)
const openSenderPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: sender.gradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: sender.gradidoID, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
throw new LogError('There are still pending Transactions for Sender and/or Recipient')
}
if (sender.id === recipient.id) {
throw new LogError('Sender and Recipient are the same', sender.id)
}

View File

@ -114,3 +114,70 @@ export async function processXComPendingSendCoins(
}
return true
}
export async function processXComCommittingSendCoins(
receiverFCom: DbFederatedCommunity,
receiverCom: DbCommunity,
senderCom: DbCommunity,
creationDate: Date,
amount: Decimal,
memo: string,
sender: dbUser,
recipient: dbUser,
): Promise<boolean> {
try {
logger.debug(
`XCom: processXComCommittingSendCoins...`,
receiverFCom,
receiverCom,
senderCom,
creationDate,
amount,
memo,
sender,
recipient,
)
// first find pending Tx with given parameters
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: senderCom.communityUuid ? senderCom.communityUuid : 'homeCom-UUID',
userGradidoID: sender.gradidoID,
userName: fullName(sender.firstName, sender.lastName),
linkedUserCommunityUuid: receiverCom.communityUuid
? receiverCom.communityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
linkedUserGradidoID: recipient.gradidoID,
typeId: TransactionTypeId.SEND,
state: PendingTransactionState.NEW,
balanceDate: creationDate,
memo,
})
if (pendingTx) {
logger.debug(`X-Com: find pending Tx for settlement:`, pendingTx)
const client = SendCoinsClientFactory.getInstance(receiverFCom)
// eslint-disable-next-line camelcase
if (client instanceof V1_0_SendCoinsClient) {
const args = new SendCoinsArgs()
args.communityReceiverIdentifier = pendingTx.linkedUserCommunityUuid
? pendingTx.linkedUserCommunityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
if (pendingTx.linkedUserGradidoID) {
args.userReceiverIdentifier = pendingTx.linkedUserGradidoID
}
args.creationDate = pendingTx.balanceDate
args.amount = pendingTx.amount
args.memo = pendingTx.memo
args.communitySenderIdentifier = pendingTx.userCommunityUuid
args.userSenderIdentifier = pendingTx.userGradidoID
if (pendingTx.userName) {
args.userSenderName = pendingTx.userName
}
logger.debug(`X-Com: ready for settleSendCoins with args=`, args)
const acknoleged = await client.settleSendCoins(args)
logger.debug(`X-Com: returnd from settleSendCoins:`, acknoleged)
}
}
} catch (err) {
logger.error(`Error:`, err)
}
return true
}

View File

@ -0,0 +1,12 @@
import { Decimal } from 'decimal.js-light'
export const MAX_CREATION_AMOUNT = new Decimal(1000)
export const FULL_CREATION_AVAILABLE = [
MAX_CREATION_AMOUNT,
MAX_CREATION_AMOUNT,
MAX_CREATION_AMOUNT,
]
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
export const MEMO_MAX_CHARS = 255
export const MEMO_MIN_CHARS = 5

View File

@ -8,9 +8,11 @@ import { User as DbUser } from '@entity/User'
import { LogError } from '@/server/LogError'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance'
import { calculateRecepientBalance } from '../util/calculateRecepientBalance'
import Decimal from 'decimal.js-light'
import { fullName } from '@/graphql/util/fullName'
import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '../const/const'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -50,6 +52,14 @@ export class SendCoinsResolver {
homeCom.name,
)
}
if (memo.length < MEMO_MIN_CHARS) {
throw new LogError('Memo text is too short', memo.length)
}
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
}
const receiveBalance = await calculateRecepientBalance(receiverUser.id, amount, creationDate)
const pendingTx = DbPendingTransaction.create()
pendingTx.amount = amount
@ -151,4 +161,85 @@ export class SendCoinsResolver {
throw new LogError(`Error in revertSendCoins: `, err)
}
}
@Mutation(() => Boolean)
async settleSendCoins(
@Args()
{
communityReceiverIdentifier,
userReceiverIdentifier,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
}: SendCoinsArgs,
): 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,
state: PendingTransactionState.NEW,
typeId: TransactionTypeId.RECEIVE,
balanceDate: creationDate,
linkedUserCommunityUuid: communitySenderIdentifier,
linkedUserGradidoID: userSenderIdentifier,
})
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)
}
} else {
logger.debug(
'XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...',
)
throw new LogError(
`Can't find in settlePendingReceiveTransaction the pending receiver TX for args=`,
communityReceiverIdentifier,
userReceiverIdentifier,
PendingTransactionState.NEW,
TransactionTypeId.RECEIVE,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
)
}
logger.debug(`settlePendingReceiveTransaction()-1_0... successfull`)
return true
} catch (err) {
throw new LogError(`Error in settlePendingReceiveTransaction: `, err)
}
}
}

View File

@ -1,8 +1,7 @@
import { calculateDecay } from '@/graphql/util/decay'
import { getLastTransaction } from '@/graphql/util/getLastTransaction'
import { Decimal } from 'decimal.js-light'
import { getLastTransaction } from './getLastTransaction'
import { calculateDecay } from './decay'
import { Decay } from '../api/1_0/model/Decay'
import { Decay } from '../model/Decay'
export async function calculateRecepientBalance(
userId: number,

View File

@ -0,0 +1,199 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection, In } 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 { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User as DbUser } from '@entity/User'
import { Decimal } from 'decimal.js-light'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { TransactionTypeId } from '../enum/TransactionTypeId'
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 settlePendingReceiveTransaction(
homeCom: DbCommunity,
receiverUser: DbUser,
pendingTx: DbPendingTransaction,
): Promise<boolean> {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
logger.info('X-Com: settlePendingReceiveTransaction:', homeCom, receiverUser, pendingTx)
const openSenderPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: pendingTx?.userGradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: pendingTx?.linkedUserGradidoID, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: userReceiverIdentifier, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: userReceiverIdentifier, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) {
throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient')
}
if (
communityReceiverIdentifier === communitySenderIdentifier &&
communitySenderIdentifier === userSenderIdentifier
) {
throw new LogError('Sender and Recipient are the same user: ', userSenderName)
}
if (memo.length < MEMO_MIN_CHARS) {
throw new LogError('Memo text is too short', memo.length)
}
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
}
const recipientUser = await DbUser.findOneByOrFail({ gradidoID: userReceiverIdentifier })
const lastTransaction = await getLastTransaction(recipientUser.id)
const pendingTx = await DbPendingTransaction.findOneByOrFail({
userId: recipientUser.id,
userGradidoID: recipientUser.gradidoID,
})
if (lastTransaction?.id !== pendingTx.previous) {
throw new LogError(
`X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}`,
)
}
// 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)
}
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...`)
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('Transaction was not successful', e)
} finally {
await queryRunner.release()
}
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

@ -0,0 +1,4 @@
import { Semaphore } from 'await-semaphore'
const CONCURRENT_TRANSACTIONS = 1
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)