Merge remote-tracking branch

'origin/3187-feature-x-sendcoind-23-invoke-settlement-of-x-pending-tx'
into 2947-refactor-the-existing-sendcoins-resolver-methode-to-distingue-between-local-transaction-and-x-transaction
This commit is contained in:
Claus-Peter Huebner 2023-09-18 23:49:31 +02:00
commit a013fd241a
41 changed files with 1796 additions and 284 deletions

View File

@ -12,7 +12,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0071-add-pending_transactions-table',
DB_VERSION: '0072-add_communityuuid_to_transactions_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

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

View File

@ -4,10 +4,10 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export class SendCoinsArgs {
@Field(() => String)
communityReceiverIdentifier: string
recipientCommunityUuid: string
@Field(() => String)
userReceiverIdentifier: string
recipientUserIdentifier: string
@Field(() => String)
creationDate: string
@ -19,11 +19,11 @@ export class SendCoinsArgs {
memo: string
@Field(() => String)
communitySenderIdentifier: string
senderCommunityUuid: string
@Field(() => String)
userSenderIdentifier: string
senderUserUuid: string
@Field(() => String)
userSenderName: string
senderUserName: string
}

View File

@ -2,24 +2,24 @@ import { gql } from 'graphql-request'
export const revertSendCoins = gql`
mutation (
$communityReceiverIdentifier: String!
$userReceiverIdentifier: String!
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$communitySenderIdentifier: String!
$userSenderIdentifier: String!
$userSenderName: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
revertSendCoins(
communityReceiverIdentifier: $communityReceiverIdentifier
userReceiverIdentifier: $userReceiverIdentifier
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
communitySenderIdentifier: $communitySenderIdentifier
userSenderIdentifier: $userSenderIdentifier
userSenderName: $userSenderName
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}
`

View File

@ -0,0 +1,25 @@
import { gql } from 'graphql-request'
export const revertSettledSendCoins = gql`
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
revertSettledSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}
`

View File

@ -0,0 +1,25 @@
import { gql } from 'graphql-request'
export const settleSendCoins = gql`
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
settleSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}
`

View File

@ -2,24 +2,24 @@ import { gql } from 'graphql-request'
export const voteForSendCoins = gql`
mutation (
$communityReceiverIdentifier: String!
$userReceiverIdentifier: String!
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$communitySenderIdentifier: String!
$userSenderIdentifier: String!
$userSenderName: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
voteForSendCoins(
communityReceiverIdentifier: $communityReceiverIdentifier
userReceiverIdentifier: $userReceiverIdentifier
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
communitySenderIdentifier: $communitySenderIdentifier
userSenderIdentifier: $userSenderIdentifier
userSenderName: $userSenderName
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}
`

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

@ -1,4 +1,5 @@
import { IsNull, getConnection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage } from '@entity/ContributionMessage'
import { Transaction as DbTransaction } from '@entity/Transaction'
@ -447,6 +448,7 @@ export class ContributionResolver {
if (user.deletedAt) {
throw new LogError('Can not confirm contribution since the user was deleted')
}
const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } })
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
validateContribution(
creations,
@ -480,6 +482,9 @@ export class ContributionResolver {
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
if (homeCom.communityUuid) {
transaction.userCommunityUuid = homeCom.communityUuid
}
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null

View File

@ -1,6 +1,7 @@
import { randomBytes } from 'crypto'
import { getConnection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { Transaction as DbTransaction } from '@entity/Transaction'
@ -165,6 +166,7 @@ export class TransactionLinkResolver {
@Ctx() context: Context,
): Promise<boolean> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } })
const user = getUser(context)
if (code.match(/^CL-/)) {
@ -271,6 +273,9 @@ export class TransactionLinkResolver {
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo
transaction.userId = contribution.userId
if (homeCom.communityUuid) {
transaction.userCommunityUuid = homeCom.communityUuid
}
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null
@ -343,6 +348,7 @@ export class TransactionLinkResolver {
transactionLink.memo,
linkedUser,
user,
homeCom,
transactionLink,
)
await EVENT_TRANSACTION_LINK_REDEEM(

View File

@ -3,6 +3,8 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection, In, IsNull } 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'
@ -12,6 +14,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'
@ -46,12 +49,29 @@ export const executeTransaction = async (
memo: string,
sender: dbUser,
recipient: dbUser,
homeCom: dbCommunity,
transactionLink?: dbTransactionLink | null,
): Promise<boolean> => {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
logger.info('executeTransaction', amount, memo, sender, recipient)
logger.info('executeTransaction', amount, memo, homeCom, 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)
@ -80,9 +100,15 @@ export const executeTransaction = async (
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
if (homeCom.communityUuid) {
transactionSend.userCommunityUuid = homeCom.communityUuid
}
transactionSend.userGradidoID = sender.gradidoID
transactionSend.userName = fullName(sender.firstName, sender.lastName)
transactionSend.linkedUserId = recipient.id
if (homeCom.communityUuid) {
transactionSend.linkedUserCommunityUuid = homeCom.communityUuid
}
transactionSend.linkedUserGradidoID = recipient.gradidoID
transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName)
transactionSend.amount = amount.mul(-1)
@ -100,9 +126,15 @@ export const executeTransaction = async (
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
if (homeCom.communityUuid) {
transactionReceive.userCommunityUuid = homeCom.communityUuid
}
transactionReceive.userGradidoID = recipient.gradidoID
transactionReceive.userName = fullName(recipient.firstName, recipient.lastName)
transactionReceive.linkedUserId = sender.id
if (homeCom.communityUuid) {
transactionReceive.linkedUserCommunityUuid = homeCom.communityUuid
}
transactionReceive.linkedUserGradidoID = sender.gradidoID
transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName)
transactionReceive.amount = amount
@ -487,9 +519,10 @@ export class TransactionResolver {
{ recipientCommunityIdentifier, recipientIdentifier, amount, memo }: TransactionSendArgs,
@Ctx() context: Context,
): Promise<boolean> {
logger.debug(
`sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, identifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`,
logger.info(
`sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`,
)
const homeCom = await dbCommunity.findOneOrFail({ where: { foreign: false } })
const senderUser = getUser(context)
@ -501,7 +534,7 @@ export class TransactionResolver {
throw new LogError('The recipient user was not found', recipientUser)
}
await executeTransaction(amount, memo, senderUser, recipientUser)
await executeTransaction(amount, memo, senderUser, recipientUser, homeCom)
logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
} else {
// processing a x-community sendCoins

View File

@ -16,6 +16,8 @@ import { backendLogger as logger } from '@/server/logger'
import { calculateSenderBalance } from '@/util/calculateSenderBalance'
import { fullName } from '@/util/utilities'
import { settlePendingSenderTransaction } from './settlePendingSenderTransaction'
export async function processXComPendingSendCoins(
receiverFCom: DbFederatedCommunity,
receiverCom: DbCommunity,
@ -49,18 +51,16 @@ export async function processXComPendingSendCoins(
// eslint-disable-next-line camelcase
if (client instanceof V1_0_SendCoinsClient) {
const args = new SendCoinsArgs()
args.communityReceiverIdentifier = receiverCom.communityUuid
args.recipientCommunityUuid = receiverCom.communityUuid
? receiverCom.communityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
args.userReceiverIdentifier = recipient.gradidoID
args.recipientUserIdentifier = recipient.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = amount
args.memo = memo
args.communitySenderIdentifier = senderCom.communityUuid
? senderCom.communityUuid
: 'homeCom-UUID'
args.userSenderIdentifier = sender.gradidoID
args.userSenderName = fullName(sender.firstName, sender.lastName)
args.senderCommunityUuid = senderCom.communityUuid ? senderCom.communityUuid : 'homeCom-UUID'
args.senderUserUuid = sender.gradidoID
args.senderUserName = fullName(sender.firstName, sender.lastName)
logger.debug(`X-Com: ready for voteForSendCoins with args=`, args)
const recipientName = await client.voteForSendCoins(args)
logger.debug(`X-Com: returnd from voteForSendCoins:`, recipientName)
@ -114,3 +114,95 @@ 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.recipientCommunityUuid = pendingTx.linkedUserCommunityUuid
? pendingTx.linkedUserCommunityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
if (pendingTx.linkedUserGradidoID) {
args.recipientUserIdentifier = pendingTx.linkedUserGradidoID
}
args.creationDate = pendingTx.balanceDate.toISOString()
args.amount = pendingTx.amount
args.memo = pendingTx.memo
args.senderCommunityUuid = pendingTx.userCommunityUuid
args.senderUserUuid = pendingTx.userGradidoID
if (pendingTx.userName) {
args.senderUserName = 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)
if (acknoleged) {
// settle the pending transaction on receiver-side was successfull, so now settle the sender side
try {
await settlePendingSenderTransaction(senderCom, sender, pendingTx)
} catch (err) {
logger.error(`Error in writing sender pending transaction: `, err)
// revert the existing pending transaction on receiver side
let revertCount = 0
logger.debug(`X-Com: first try to revertSetteledSendCoins of receiver`)
do {
if (await client.revertSettledSendCoins(args)) {
logger.debug(
`revertSettledSendCoins()-1_0... successfull after revertCount=`,
revertCount,
)
// treat revertingSettledSendCoins as an error of the whole sendCoins-process
throw new LogError('Error in settle sender pending transaction: `, err')
}
} while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++)
throw new LogError(
`Error in reverting receiver pending transaction even after revertCount=`,
revertCount,
)
}
}
}
}
} catch (err) {
logger.error(`Error:`, err)
}
return true
}

View File

@ -0,0 +1,146 @@
/* 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 '@/graphql/enum/PendingTransactionState'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { calculateSenderBalance } from '@/util/calculateSenderBalance'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { getLastTransaction } from './getLastTransaction'
export async function settlePendingSenderTransaction(
homeCom: DbCommunity,
senderUser: DbUser,
pendingTx: DbPendingTransaction,
): Promise<boolean> {
// TODO: synchronisation with TRANSACTION_LOCK of federation-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: settlePendingSenderTransaction:', homeCom, senderUser, 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 },
{ linkedUserGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 1 || openReceiverPendingTx > 1) {
throw new LogError('There are more than 1 pending Transactions for Sender and/or Recipient')
}
const lastTransaction = await getLastTransaction(senderUser.id)
if (lastTransaction?.id !== pendingTx.previous) {
throw new LogError(
`X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}`,
)
}
// transfer the pendingTx to the transactions table
const transactionSend = new dbTransaction()
transactionSend.typeId = pendingTx.typeId
transactionSend.memo = pendingTx.memo
transactionSend.userId = pendingTx.userId
transactionSend.userGradidoID = pendingTx.userGradidoID
transactionSend.userName = pendingTx.userName
transactionSend.linkedUserId = pendingTx.linkedUserId
transactionSend.linkedUserGradidoID = pendingTx.linkedUserGradidoID
transactionSend.linkedUserName = pendingTx.linkedUserName
transactionSend.amount = pendingTx.amount
const sendBalance = await calculateSenderBalance(
senderUser.id,
pendingTx.amount,
pendingTx.balanceDate,
)
if (sendBalance?.balance !== pendingTx.balance) {
throw new LogError(
`X-Com: Calculation-Error on receiver balance: receiveBalance=${sendBalance?.balance}, pendingTx.balance=${pendingTx.balance}`,
)
}
transactionSend.balance = pendingTx.balance
transactionSend.balanceDate = pendingTx.balanceDate
transactionSend.decay = pendingTx.decay
transactionSend.decayStart = pendingTx.decayStart
transactionSend.previous = pendingTx.previous
transactionSend.linkedTransactionId = pendingTx.linkedTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`send Transaction inserted: ${dbTransaction}`)
// and mark the pendingTx in the pending_transactions table as settled
pendingTx.state = PendingTransactionState.SETTLED
await queryRunner.manager.save(DbPendingTransaction, pendingTx)
await queryRunner.commitTransaction()
logger.info(`commit send 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('X-Com: send 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

@ -0,0 +1,34 @@
import { Community as DbCommunity } from '@entity/Community'
import { v4 as uuidv4 } from 'uuid'
import { CONFIG } from '@/config'
export async function writeHomeCommunityEntry(): Promise<void> {
try {
// check for existing homeCommunity entry
let homeCom = await DbCommunity.findOne({ where: { foreign: false } })
if (homeCom) {
// simply update the existing entry, but it MUST keep the ID and UUID because of possible relations
homeCom.publicKey = Buffer.from('public-key-data-seeding') // keyPair.publicKey
// homeCom.privateKey = keyPair.secretKey
homeCom.url = 'http://localhost/api/'
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
await DbCommunity.save(homeCom)
} else {
// insert a new homecommunity entry including a new ID and a new but ensured unique UUID
homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.publicKey = Buffer.from('public-key-data-seeding') // keyPair.publicKey
// homeCom.privateKey = keyPair.secretKey
homeCom.communityUuid = uuidv4() // await newCommunityUuid()
homeCom.url = 'http://localhost/api/'
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
homeCom.creationDate = new Date()
await DbCommunity.insert(homeCom)
}
} catch (err) {
throw new Error(`Seeding: Error writing HomeCommunity-Entry`) // : ${err}`)
}
}

View File

@ -12,6 +12,7 @@ import { CONFIG } from '@/config'
import { createServer } from '@/server/createServer'
import { backendLogger as logger } from '@/server/logger'
import { writeHomeCommunityEntry } from './community'
import { contributionLinks } from './contributionLink/index'
import { creations } from './creation/index'
import { contributionLinkFactory } from './factory/contributionLink'
@ -57,6 +58,9 @@ const run = async () => {
await cleanDB()
logger.info('##seed## clean database successful...')
// seed home community
await writeHomeCommunityEntry()
// seed the standard users
for (const user of users) {
await userFactory(seedClient, user)

View File

@ -58,6 +58,8 @@ const virtualLinkTransaction = (
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
userCommunityUuid: '',
linkedUserCommunityUuid: null,
}
return new Transaction(linkDbTransaction, user)
}
@ -92,6 +94,8 @@ const virtualDecayTransaction = (
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
userCommunityUuid: '',
linkedUserCommunityUuid: null,
}
return new Transaction(decayDbTransaction, user)
}

View File

@ -0,0 +1,163 @@
/* eslint-disable no-use-before-define */
import { Decimal } from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Contribution } from '../Contribution'
import { DltTransaction } from '../DltTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
previous: number | null
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
name: 'transaction_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
transactionLinkId?: number | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date | null
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({
name: 'user_community_uuid',
type: 'varchar',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
userCommunityUuid: string
@Column({
name: 'user_gradido_id',
type: 'varchar',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
userGradidoID: string
@Column({
name: 'user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
userName: string | null
@Column({
name: 'linked_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedUserId?: number | null
@Column({
name: 'linked_user_community_uuid',
type: 'varchar',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserCommunityUuid: string | null
@Column({
name: 'linked_user_gradido_id',
type: 'varchar',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserGradidoID: string | null
@Column({
name: 'linked_user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserName: string | null
@Column({
name: 'linked_transaction_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedTransactionId?: number | null
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
contribution?: Contribution | null
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionId)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
dltTransaction?: DltTransaction | null
@OneToOne(() => Transaction)
@JoinColumn({ name: 'previous' })
previousTransaction?: Transaction | null
}

View File

@ -1 +1 @@
export { Transaction } from './0070-add_dlt_transactions_table/Transaction'
export { Transaction } from './0072-add_communityuuid_to_transactions_table/Transaction'

View File

@ -0,0 +1,32 @@
/* MIGRATION TO add users that have a transaction but do not exist */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `user_community_uuid` char(36) DEFAULT NULL NULL AFTER `user_id`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `linked_user_community_uuid` char(36) DEFAULT NULL NULL AFTER `linked_user_id`;',
)
// read the community uuid of the homeCommunity
const result = await queryFn(`SELECT c.community_uuid from communities as c WHERE c.foreign = 0`)
// and if uuid exists enter the home_community_uuid for sender and recipient of each still existing transaction
if (result[0]) {
await queryFn(
`UPDATE transactions as t SET t.user_community_uuid = "${result[0].community_uuid}" WHERE t.user_id IS NOT NULL AND t.user_community_uuid IS NULL`,
)
await queryFn(
`UPDATE transactions as t SET t.linked_user_community_uuid = "${result[0].community_uuid}" WHERE t.linked_user_id IS NOT NULL AND t.linked_user_community_uuid IS NULL`,
)
}
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `user_community_uuid` char(36) NOT NULL AFTER `user_id`;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_community_uuid`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_community_uuid`;')
}

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
DB_VERSION: '0071-add-pending_transactions-table',
DB_VERSION: '0072-add_communityuuid_to_transactions_table',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',

View File

@ -16,7 +16,9 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
"dependencies": {
"@types/uuid": "8.3.4",
"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",
@ -27,7 +29,8 @@
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1",
"reflect-metadata": "^0.1.13",
"type-graphql": "^1.1.1"
"type-graphql": "^1.1.1",
"uuid": "8.3.2"
},
"devDependencies": {
"@types/express": "4.17.12",

View File

@ -1,6 +1,7 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
import { Decimal } from 'decimal.js-light'
import dotenv from 'dotenv'
dotenv.config()
Decimal.set({
@ -9,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0071-add-pending_transactions-table',
DB_VERSION: '0072-add_communityuuid_to_transactions_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
@ -53,9 +54,14 @@ const federation = {
FEDERATION_API: process.env.FEDERATION_API || '1_0',
FEDERATION_PORT: process.env.FEDERATION_PORT || 5010,
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
FEDERATION_TRADING_LEVEL: {
RECEIVER_COMMUNITY_URL: 'https://stage3.gradido.net/api/',
SEND_COINS: true,
AMOUNT: 100,
},
}
const CONFIG = {
export const CONFIG = {
...constants,
...server,
...database,

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

@ -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

@ -4,10 +4,10 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export class SendCoinsArgs {
@Field(() => String)
communityReceiverIdentifier: string
recipientCommunityUuid: string
@Field(() => String)
userReceiverIdentifier: string
recipientUserIdentifier: string
@Field(() => String)
creationDate: string
@ -19,11 +19,11 @@ export class SendCoinsArgs {
memo: string
@Field(() => String)
communitySenderIdentifier: string
senderCommunityUuid: string
@Field(() => String)
userSenderIdentifier: string
senderUserUuid: string
@Field(() => String)
userSenderName: string
senderUserName: string
}

View File

@ -4,11 +4,12 @@ import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import { Community as DbCommunity } from '@entity/Community'
import CONFIG from '@/config'
import { Connection } from '@dbTools/typeorm'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
let con: Connection
CONFIG.FEDERATION_API = '1_0'

View File

@ -4,11 +4,17 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
import { Community as DbCommunity } from '@entity/Community'
import CONFIG from '@/config'
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { fullName } from '@/graphql/util/fullName'
import { GraphQLError } from 'graphql'
import { cleanDB, testEnvironment } from '@test/helpers'
import { logger } from '@test/testSetup'
import { Connection } from '@dbTools/typeorm'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import Decimal from 'decimal.js-light'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { Transaction as DbTransaction } from '@entity/Transaction'
let mutate: ApolloServerTestClient['mutate'], con: Connection
// let query: ApolloServerTestClient['query']
@ -21,18 +27,18 @@ let testEnv: {
CONFIG.FEDERATION_API = '1_0'
let homeCom: DbCommunity
let foreignCom: DbCommunity
let sendUser: DbUser
let sendContact: DbUserContact
let recipUser: DbUser
let recipContact: DbUserContact
beforeAll(async () => {
testEnv = await testEnvironment(logger)
mutate = testEnv.mutate
// query = testEnv.query
con = testEnv.con
// const server = await createServer()
// con = server.con
// query = createTestClient(server.apollo).query
// mutate = createTestClient(server.apollo).mutate
// DbCommunity.clear()
// DbUser.clear()
await cleanDB()
})
@ -43,95 +49,103 @@ afterAll(async () => {
describe('SendCoinsResolver', () => {
const voteForSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
voteForSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}`
const settleSendCoinsMutation = `
mutation (
$communityReceiverIdentifier: String!
$userReceiverIdentifier: String!
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$communitySenderIdentifier: String!
$userSenderIdentifier: String!
$userSenderName: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
voteForSendCoins(
communityReceiverIdentifier: $communityReceiverIdentifier
userReceiverIdentifier: $userReceiverIdentifier
settleSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
communitySenderIdentifier: $communitySenderIdentifier
userSenderIdentifier: $userSenderIdentifier
userSenderName: $userSenderName
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}
`
const revertSendCoinsMutation = `
mutation (
$communityReceiverIdentifier: String!
$userReceiverIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$communitySenderIdentifier: String!
$userSenderIdentifier: String!
$userSenderName: String!
) {
revertSendCoins(
communityReceiverIdentifier: $communityReceiverIdentifier
userReceiverIdentifier: $userReceiverIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
communitySenderIdentifier: $communitySenderIdentifier
userSenderIdentifier: $userSenderIdentifier
userSenderName: $userSenderName
)
}
`
}`
beforeEach(async () => {
await cleanDB()
homeCom = DbCommunity.create()
homeCom.foreign = false
homeCom.url = 'homeCom-url'
homeCom.name = 'homeCom-Name'
homeCom.description = 'homeCom-Description'
homeCom.creationDate = new Date()
homeCom.publicKey = Buffer.from('homeCom-publicKey')
homeCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba'
await DbCommunity.insert(homeCom)
foreignCom = DbCommunity.create()
foreignCom.foreign = true
foreignCom.url = 'foreignCom-url'
foreignCom.name = 'foreignCom-Name'
foreignCom.description = 'foreignCom-Description'
foreignCom.creationDate = new Date()
foreignCom.publicKey = Buffer.from('foreignCom-publicKey')
foreignCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb'
await DbCommunity.insert(foreignCom)
sendUser = DbUser.create()
sendUser.alias = 'sendUser-alias'
sendUser.firstName = 'sendUser-FirstName'
sendUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebc'
sendUser.lastName = 'sendUser-LastName'
await DbUser.insert(sendUser)
sendContact = await newEmailContact('send.user@email.de', sendUser.id)
sendContact = await DbUserContact.save(sendContact)
sendUser.emailContact = sendContact
sendUser.emailId = sendContact.id
await DbUser.save(sendUser)
recipUser = DbUser.create()
recipUser.alias = 'recipUser-alias'
recipUser.firstName = 'recipUser-FirstName'
recipUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebd'
recipUser.lastName = 'recipUser-LastName'
await DbUser.insert(recipUser)
recipContact = await newEmailContact('recip.user@email.de', recipUser.id)
recipContact = await DbUserContact.save(recipContact)
recipUser.emailContact = recipContact
recipUser.emailId = recipContact.id
await DbUser.save(recipUser)
})
describe('voteForSendCoins', () => {
let homeCom: DbCommunity
let foreignCom: DbCommunity
let sendUser: DbUser
let recipUser: DbUser
beforeEach(async () => {
await cleanDB()
homeCom = DbCommunity.create()
homeCom.foreign = false
homeCom.url = 'homeCom-url'
homeCom.name = 'homeCom-Name'
homeCom.description = 'homeCom-Description'
homeCom.creationDate = new Date()
homeCom.publicKey = Buffer.from('homeCom-publicKey')
homeCom.communityUuid = 'homeCom-UUID'
await DbCommunity.insert(homeCom)
foreignCom = DbCommunity.create()
foreignCom.foreign = true
foreignCom.url = 'foreignCom-url'
foreignCom.name = 'foreignCom-Name'
foreignCom.description = 'foreignCom-Description'
foreignCom.creationDate = new Date()
foreignCom.publicKey = Buffer.from('foreignCom-publicKey')
foreignCom.communityUuid = 'foreignCom-UUID'
await DbCommunity.insert(foreignCom)
sendUser = DbUser.create()
sendUser.alias = 'sendUser-alias'
sendUser.firstName = 'sendUser-FirstName'
sendUser.gradidoID = 'sendUser-GradidoID'
sendUser.lastName = 'sendUser-LastName'
await DbUser.insert(sendUser)
recipUser = DbUser.create()
recipUser.alias = 'recipUser-alias'
recipUser.firstName = 'recipUser-FirstName'
recipUser.gradidoID = 'recipUser-GradidoID'
recipUser.lastName = 'recipUser-LastName'
await DbUser.insert(recipUser)
})
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
@ -139,19 +153,19 @@ describe('SendCoinsResolver', () => {
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
communityReceiverIdentifier: 'invalid foreignCom',
userReceiverIdentifier: recipUser.gradidoID,
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: new Date().toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('voteForSendCoins with wrong communityReceiverIdentifier')],
errors: [new GraphQLError('voteForSendCoins with wrong recipientCommunityUuid')],
}),
)
})
@ -164,21 +178,21 @@ describe('SendCoinsResolver', () => {
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
communityReceiverIdentifier: foreignCom.communityUuid,
userReceiverIdentifier: 'invalid recipient',
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: new Date().toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'voteForSendCoins with unknown userReceiverIdentifier in the community=',
'voteForSendCoins with unknown recipientUserIdentifier in the community=',
),
],
}),
@ -193,14 +207,14 @@ describe('SendCoinsResolver', () => {
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
communityReceiverIdentifier: foreignCom.communityUuid,
userReceiverIdentifier: recipUser.gradidoID,
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: new Date().toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
@ -215,59 +229,43 @@ describe('SendCoinsResolver', () => {
})
describe('revertSendCoins', () => {
let homeCom: DbCommunity
let foreignCom: DbCommunity
let sendUser: DbUser
let recipUser: DbUser
const revertSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
revertSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}`
const creationDate = new Date()
beforeEach(async () => {
await cleanDB()
homeCom = DbCommunity.create()
homeCom.foreign = false
homeCom.url = 'homeCom-url'
homeCom.name = 'homeCom-Name'
homeCom.description = 'homeCom-Description'
homeCom.creationDate = new Date()
homeCom.publicKey = Buffer.from('homeCom-publicKey')
homeCom.communityUuid = 'homeCom-UUID'
await DbCommunity.insert(homeCom)
foreignCom = DbCommunity.create()
foreignCom.foreign = true
foreignCom.url = 'foreignCom-url'
foreignCom.name = 'foreignCom-Name'
foreignCom.description = 'foreignCom-Description'
foreignCom.creationDate = new Date()
foreignCom.publicKey = Buffer.from('foreignCom-publicKey')
foreignCom.communityUuid = 'foreignCom-UUID'
await DbCommunity.insert(foreignCom)
sendUser = DbUser.create()
sendUser.alias = 'sendUser-alias'
sendUser.firstName = 'sendUser-FirstName'
sendUser.gradidoID = 'sendUser-GradidoID'
sendUser.lastName = 'sendUser-LastName'
await DbUser.insert(sendUser)
recipUser = DbUser.create()
recipUser.alias = 'recipUser-alias'
recipUser.firstName = 'recipUser-FirstName'
recipUser.gradidoID = 'recipUser-GradidoID'
recipUser.lastName = 'recipUser-LastName'
await DbUser.insert(recipUser)
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
communityReceiverIdentifier: foreignCom.communityUuid,
userReceiverIdentifier: recipUser.gradidoID,
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
})
})
@ -279,19 +277,19 @@ describe('SendCoinsResolver', () => {
await mutate({
mutation: revertSendCoinsMutation,
variables: {
communityReceiverIdentifier: 'invalid foreignCom',
userReceiverIdentifier: recipUser.gradidoID,
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('revertSendCoins with wrong communityReceiverIdentifier')],
errors: [new GraphQLError('revertSendCoins with wrong recipientCommunityUuid')],
}),
)
})
@ -304,21 +302,21 @@ describe('SendCoinsResolver', () => {
await mutate({
mutation: revertSendCoinsMutation,
variables: {
communityReceiverIdentifier: foreignCom.communityUuid,
userReceiverIdentifier: 'invalid recipient',
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'revertSendCoins with unknown userReceiverIdentifier in the community=',
'revertSendCoins with unknown recipientUserIdentifier in the community=',
),
],
}),
@ -333,14 +331,14 @@ describe('SendCoinsResolver', () => {
await mutate({
mutation: revertSendCoinsMutation,
variables: {
communityReceiverIdentifier: foreignCom.communityUuid,
userReceiverIdentifier: recipUser.gradidoID,
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
communitySenderIdentifier: homeCom.communityUuid,
userSenderIdentifier: sendUser.gradidoID,
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
@ -353,4 +351,271 @@ describe('SendCoinsResolver', () => {
})
})
})
describe('settleSendCoins', () => {
let pendingTx: DbPendingTransaction
const creationDate = new Date()
beforeEach(async () => {
pendingTx = DbPendingTransaction.create()
pendingTx.amount = new Decimal(100)
pendingTx.balanceDate = creationDate
// pendingTx.balance = new Decimal(0)
pendingTx.linkedUserId = sendUser.id
if (foreignCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = foreignCom.communityUuid
}
pendingTx.linkedUserGradidoID = sendUser.gradidoID
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.RECEIVE
pendingTx.memo = 'X-Com-TX memo'
pendingTx.userId = recipUser.id
if (homeCom.communityUuid) {
pendingTx.userCommunityUuid = homeCom.communityUuid
}
pendingTx.userGradidoID = recipUser.gradidoID
await DbPendingTransaction.insert(pendingTx)
})
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: settleSendCoinsMutation,
variables: {
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('settleSendCoins with wrong recipientCommunityUuid')],
}),
)
})
})
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: settleSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'settleSendCoins with unknown recipientUserIdentifier in the community=',
),
],
}),
)
})
})
describe('valid X-Com-TX settled', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: settleSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
data: {
settleSendCoins: true,
},
}),
)
})
})
})
describe('revertSettledSendCoins', () => {
const revertSettledSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
revertSettledSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}`
let pendingTx: DbPendingTransaction
let settledTx: DbTransaction
const creationDate = new Date()
beforeEach(async () => {
pendingTx = DbPendingTransaction.create()
pendingTx.amount = new Decimal(100)
pendingTx.balanceDate = creationDate
// pendingTx.balance = new Decimal(0)
pendingTx.linkedUserId = sendUser.id
if (foreignCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = foreignCom.communityUuid
}
pendingTx.linkedUserGradidoID = sendUser.gradidoID
pendingTx.linkedUserName = fullName(sendUser.firstName, sendUser.lastName)
pendingTx.state = PendingTransactionState.SETTLED
pendingTx.typeId = TransactionTypeId.RECEIVE
pendingTx.memo = 'X-Com-TX memo'
pendingTx.userId = recipUser.id
if (homeCom.communityUuid) {
pendingTx.userCommunityUuid = homeCom.communityUuid
}
pendingTx.userGradidoID = recipUser.gradidoID
await DbPendingTransaction.insert(pendingTx)
settledTx = DbTransaction.create()
settledTx.amount = new Decimal(100)
settledTx.balanceDate = creationDate
// pendingTx.balance = new Decimal(0)
settledTx.linkedUserId = sendUser.id
settledTx.linkedUserCommunityUuid = foreignCom.communityUuid
settledTx.linkedUserGradidoID = sendUser.gradidoID
settledTx.linkedUserName = fullName(sendUser.firstName, sendUser.lastName)
settledTx.typeId = TransactionTypeId.RECEIVE
settledTx.memo = 'X-Com-TX memo'
settledTx.userId = recipUser.id
if (homeCom.communityUuid) {
settledTx.userCommunityUuid = homeCom.communityUuid
}
settledTx.userGradidoID = recipUser.gradidoID
await DbTransaction.insert(settledTx)
})
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
variables: {
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('revertSettledSendCoins with wrong recipientCommunityUuid')],
}),
)
})
})
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'revertSettledSendCoins with unknown recipientUserIdentifier in the community=',
),
],
}),
)
})
})
describe('valid X-Com-TX settled', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
}),
).toEqual(
expect.objectContaining({
data: {
revertSettledSendCoins: true,
},
}),
)
})
})
})
})
async function newEmailContact(email: string, userId: number): Promise<DbUserContact> {
const emailContact = new DbUserContact()
emailContact.email = email
emailContact.userId = userId
emailContact.type = 'EMAIL'
emailContact.emailChecked = false
emailContact.emailOptInTypeId = 1
emailContact.emailVerificationCode = '1' + userId
return emailContact
}

View File

@ -4,13 +4,16 @@ import { federationLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import { SendCoinsArgs } from '../model/SendCoinsArgs'
import { User as DbUser } from '@entity/User'
import { LogError } from '@/server/LogError'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { calculateRecipientBalance } from '@/graphql/util/calculateRecipientBalance'
import { calculateRecipientBalance } from '../util/calculateRecipientBalance'
import Decimal from 'decimal.js-light'
import { fullName } from '@/graphql/util/fullName'
import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction'
// import { checkTradingLevel } from '@/graphql/util/checkTradingLevel'
import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction'
import { findUserByIdentifier } from '@/graphql/util/findUserByIdentifier'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -19,33 +22,46 @@ export class SendCoinsResolver {
async voteForSendCoins(
@Args()
{
communityReceiverIdentifier,
userReceiverIdentifier,
recipientCommunityUuid,
recipientUserIdentifier,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
senderCommunityUuid,
senderUserUuid,
senderUserName,
}: SendCoinsArgs,
): Promise<string | null> {
logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`)
let result: string | null = null
): Promise<string> {
logger.debug(
`voteForSendCoins() via apiVersion=1_0 ...`,
recipientCommunityUuid,
recipientUserIdentifier,
creationDate,
amount.toString(),
memo,
senderCommunityUuid,
senderUserUuid,
senderUserName,
)
let result: string
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: communityReceiverIdentifier,
communityUuid: recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`voteForSendCoins with wrong communityReceiverIdentifier`,
communityReceiverIdentifier,
`voteForSendCoins with wrong recipientCommunityUuid`,
recipientCommunityUuid,
)
}
// second check if receiver user exists in this community
const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier })
if (!receiverUser) {
let receiverUser
try {
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(recipientUserIdentifier)
} catch (err) {
logger.error('Error in findUserByIdentifier:', err)
throw new LogError(
`voteForSendCoins with unknown userReceiverIdentifier in the community=`,
`voteForSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
}
@ -54,21 +70,21 @@ export class SendCoinsResolver {
const receiveBalance = await calculateRecipientBalance(receiverUser.id, amount, txDate)
const pendingTx = DbPendingTransaction.create()
pendingTx.amount = amount
pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0)
pendingTx.balance = receiveBalance ? receiveBalance.balance : amount
pendingTx.balanceDate = txDate
pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null
pendingTx.creationDate = new Date()
pendingTx.linkedUserCommunityUuid = communitySenderIdentifier
pendingTx.linkedUserGradidoID = userSenderIdentifier
pendingTx.linkedUserName = userSenderName
pendingTx.linkedUserCommunityUuid = senderCommunityUuid
pendingTx.linkedUserGradidoID = senderUserUuid
pendingTx.linkedUserName = senderUserName
pendingTx.memo = memo
pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.RECEIVE
pendingTx.userId = receiverUser.id
pendingTx.userCommunityUuid = communityReceiverIdentifier
pendingTx.userGradidoID = userReceiverIdentifier
pendingTx.userCommunityUuid = recipientCommunityUuid
pendingTx.userGradidoID = receiverUser.gradidoID
pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName)
await DbPendingTransaction.insert(pendingTx)
@ -84,44 +100,47 @@ export class SendCoinsResolver {
async revertSendCoins(
@Args()
{
communityReceiverIdentifier,
userReceiverIdentifier,
recipientCommunityUuid,
recipientUserIdentifier,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
senderCommunityUuid,
senderUserUuid,
senderUserName,
}: SendCoinsArgs,
): Promise<boolean> {
logger.debug(`revertSendCoins() via apiVersion=1_0 ...`)
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: communityReceiverIdentifier,
communityUuid: recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`revertSendCoins with wrong communityReceiverIdentifier`,
communityReceiverIdentifier,
`revertSendCoins with wrong recipientCommunityUuid`,
recipientCommunityUuid,
)
}
// second check if receiver user exists in this community
const receiverUser = await DbUser.findOneBy({ gradidoID: userReceiverIdentifier })
if (!receiverUser) {
let receiverUser
try {
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(recipientUserIdentifier)
} catch (err) {
logger.error('Error in findUserByIdentifier:', err)
throw new LogError(
`revertSendCoins with unknown userReceiverIdentifier in the community=`,
`revertSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
}
try {
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: communityReceiverIdentifier,
userGradidoID: userReceiverIdentifier,
userCommunityUuid: recipientCommunityUuid,
userGradidoID: receiverUser.gradidoID,
state: PendingTransactionState.NEW,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(creationDate),
linkedUserCommunityUuid: communitySenderIdentifier,
linkedUserGradidoID: userSenderIdentifier,
linkedUserCommunityUuid: senderCommunityUuid,
linkedUserGradidoID: senderUserUuid,
})
logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx)
if (pendingTx && pendingTx.amount.toString() === amount.toString()) {
@ -140,16 +159,16 @@ export class SendCoinsResolver {
)
throw new LogError(
`Can't find in revertSendCoins the pending receiver TX for args=`,
communityReceiverIdentifier,
userReceiverIdentifier,
recipientCommunityUuid,
recipientUserIdentifier,
PendingTransactionState.NEW,
TransactionTypeId.RECEIVE,
creationDate,
amount,
memo,
communitySenderIdentifier,
userSenderIdentifier,
userSenderName,
senderCommunityUuid,
senderUserUuid,
senderUserName,
)
}
logger.debug(`revertSendCoins()-1_0... successfull`)
@ -158,4 +177,150 @@ export class SendCoinsResolver {
throw new LogError(`Error in revertSendCoins: `, err)
}
}
@Mutation(() => Boolean)
async settleSendCoins(
@Args()
{
recipientCommunityUuid,
recipientUserIdentifier,
creationDate,
amount,
memo,
senderCommunityUuid,
senderUserUuid,
senderUserName,
}: SendCoinsArgs,
): Promise<boolean> {
logger.debug(
`settleSendCoins() via apiVersion=1_0 ...userCommunityUuid=${recipientCommunityUuid}, userGradidoID=${recipientUserIdentifier}, balanceDate=${creationDate},amount=${amount.valueOf()}, memo=${memo}, linkedUserCommunityUuid = ${senderCommunityUuid}, userSenderIdentifier=${senderUserUuid}, userSenderName=${senderUserName}`,
)
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`settleSendCoins with wrong recipientCommunityUuid`,
recipientCommunityUuid,
)
}
let receiverUser
try {
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(recipientUserIdentifier)
} catch (err) {
logger.error('Error in findUserByIdentifier:', err)
throw new LogError(
`settleSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
}
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: recipientCommunityUuid,
userGradidoID: receiverUser.gradidoID,
state: PendingTransactionState.NEW,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(creationDate),
linkedUserCommunityUuid: senderCommunityUuid,
linkedUserGradidoID: senderUserUuid,
})
logger.debug('XCom: settleSendCoins found pendingTX=', pendingTx?.toString())
if (pendingTx && pendingTx.amount.toString() === amount.toString() && pendingTx.memo === memo) {
logger.debug('XCom: settleSendCoins matching pendingTX for settlement...')
await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx)
logger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successfull`)
return true
} else {
logger.debug('XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...')
throw new LogError(
`Can't find in settlePendingReceiveTransaction the pending receiver TX for args=`,
recipientCommunityUuid,
recipientUserIdentifier,
PendingTransactionState.NEW,
TransactionTypeId.RECEIVE,
creationDate,
amount,
memo,
senderCommunityUuid,
senderUserUuid,
senderUserName,
)
}
}
@Mutation(() => Boolean)
async revertSettledSendCoins(
@Args()
{
recipientCommunityUuid,
recipientUserIdentifier,
creationDate,
amount,
memo,
senderCommunityUuid,
senderUserUuid,
senderUserName,
}: SendCoinsArgs,
): Promise<boolean> {
logger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`)
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`revertSettledSendCoins with wrong recipientCommunityUuid`,
recipientCommunityUuid,
)
}
let receiverUser
try {
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(recipientUserIdentifier)
} catch (err) {
logger.error('Error in findUserByIdentifier:', err)
throw new LogError(
`revertSettledSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
}
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: recipientCommunityUuid,
userGradidoID: receiverUser.gradidoID,
state: PendingTransactionState.SETTLED,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(creationDate),
linkedUserCommunityUuid: senderCommunityUuid,
linkedUserGradidoID: senderUserUuid,
})
logger.debug('XCom: revertSettledSendCoins found pendingTX=', pendingTx)
if (pendingTx && pendingTx.amount.toString() === amount.toString() && 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=`,
recipientCommunityUuid,
recipientUserIdentifier,
PendingTransactionState.SETTLED,
TransactionTypeId.RECEIVE,
creationDate,
amount,
memo,
senderCommunityUuid,
senderUserUuid,
senderUserName,
)
}
logger.debug(`revertSendCoins()-1_0... successfull`)
return true
}
}

View File

@ -0,0 +1,19 @@
import { calculateDecay } from '@/graphql/util/decay'
import { getLastTransaction } from '@/graphql/util/getLastTransaction'
import { Decimal } from 'decimal.js-light'
import { Decay } from '../model/Decay'
export async function calculateRecipientBalance(
userId: number,
amount: Decimal,
time: Date,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
const lastTransaction = await getLastTransaction(userId)
if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
const balance = decay.balance.add(amount.toString())
return { balance, lastTransactionId: lastTransaction.id, decay }
}

View File

@ -0,0 +1,141 @@
/* 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.toISOString() === pendingTx.balanceDate.toISOString() &&
lastTransaction.userGradidoID === pendingTx.userGradidoID &&
lastTransaction.userName === pendingTx.userName &&
lastTransaction.amount.toString() === pendingTx.amount.toString() &&
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!`,
lastTransaction,
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

@ -0,0 +1,153 @@
/* 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'
import { calculateRecipientBalance } from './calculateRecipientBalance'
import Decimal from 'decimal.js-light'
export async function settlePendingReceiveTransaction(
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: settlePendingReceiveTransaction:', 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 },
{ linkedUserGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: pendingTx.linkedUserGradidoID!, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: pendingTx.userGradidoID, state: PendingTransactionState.NEW },
],
})
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)
if (lastTransaction !== null && lastTransaction.id !== pendingTx.previous) {
throw new LogError(
`X-Com: missmatching transaction order! lastTransationId=${lastTransaction?.id} != pendingTx.previous=${pendingTx.previous}`,
)
}
// transfer the pendingTx to the transactions table
const transactionReceive = new dbTransaction()
transactionReceive.typeId = pendingTx.typeId
transactionReceive.memo = pendingTx.memo
transactionReceive.userId = pendingTx.userId
transactionReceive.userCommunityUuid = pendingTx.userCommunityUuid
transactionReceive.userGradidoID = pendingTx.userGradidoID
transactionReceive.userName = pendingTx.userName
transactionReceive.linkedUserId = pendingTx.linkedUserId
transactionReceive.linkedUserCommunityUuid = pendingTx.linkedUserCommunityUuid
transactionReceive.linkedUserGradidoID = pendingTx.linkedUserGradidoID
transactionReceive.linkedUserName = pendingTx.linkedUserName
transactionReceive.amount = pendingTx.amount
const receiveBalance = await calculateRecipientBalance(
receiverUser.id,
pendingTx.amount,
pendingTx.balanceDate,
)
if (
receiveBalance !== null &&
receiveBalance.balance.toString() !== pendingTx.balance.toString()
) {
throw new LogError(
`X-Com: Calculation-Error on receiver balance: receiveBalance=${receiveBalance.balance}, pendingTx.balance=${pendingTx.balance}`,
)
}
transactionReceive.balance = receiveBalance ? receiveBalance.balance : pendingTx.amount
transactionReceive.balanceDate = pendingTx.balanceDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = pendingTx.linkedTransactionId
await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// and mark the pendingTx in the pending_transactions table as settled
pendingTx.state = PendingTransactionState.SETTLED
await queryRunner.manager.save(DbPendingTransaction, pendingTx)
await queryRunner.commitTransaction()
logger.info(`commit recipient 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('X-Com: 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

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

View File

@ -0,0 +1,25 @@
import CONFIG from '@/config'
import { Community as DbCommunity } from '@entity/Community'
import { Decimal } from 'decimal.js-light'
import { federationLogger as logger } from '@/server/logger'
export async function checkTradingLevel(homeCom: DbCommunity, amount: Decimal): Promise<boolean> {
const tradingLevel = CONFIG.FEDERATION_TRADING_LEVEL
if (homeCom.url !== tradingLevel.RECEIVER_COMMUNITY_URL) {
logger.warn(
`X-Com: tradingLevel allows to receive coins only wiht url ${tradingLevel.RECEIVER_COMMUNITY_URL}`,
)
return false
}
if (!tradingLevel.SEND_COINS) {
logger.warn(`X-Com: tradingLevel disable general x-com sendcoin actions!`)
return false
}
if (new Decimal(tradingLevel.AMOUNT) < amount) {
logger.warn(
`X-Com: tradingLevel only allows to receive coins lower than amount of ${tradingLevel.AMOUNT}`,
)
return false
}
return true
}

View File

@ -0,0 +1,42 @@
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { validate, version } from 'uuid'
import { LogError } from '@/server/LogError'
import { VALID_ALIAS_REGEX } from './validateAlias'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
let user: DbUser | null
if (validate(identifier) && version(identifier) === 4) {
user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] })
if (!user) {
throw new LogError('No user found to given identifier', identifier)
}
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
const userContact = await DbUserContact.findOne({
where: {
email: identifier,
emailChecked: true,
},
relations: ['user'],
})
if (!userContact) {
throw new LogError('No user with this credentials', identifier)
}
if (!userContact.user) {
throw new LogError('No user to given contact', identifier)
}
user = userContact.user
user.emailContact = userContact
} else if (VALID_ALIAS_REGEX.exec(identifier)) {
user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] })
if (!user) {
throw new LogError('No user found to given identifier', identifier)
}
} else {
throw new LogError('Unknown identifier type', identifier)
}
return user
}

View File

@ -0,0 +1,39 @@
import { Raw } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { LogError } from '@/server/LogError'
export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
const RESERVED_ALIAS = [
'admin',
'email',
'gast',
'gdd',
'gradido',
'guest',
'home',
'root',
'support',
'temp',
'tmp',
'tmp',
'user',
'usr',
'var',
]
export const validateAlias = async (alias: string): Promise<boolean> => {
if (alias.length < 3) throw new LogError('Given alias is too short', alias)
if (alias.length > 20) throw new LogError('Given alias is too long', alias)
if (!alias.match(VALID_ALIAS_REGEX)) throw new LogError('Invalid characters in alias', alias)
if (RESERVED_ALIAS.includes(alias.toLowerCase()))
throw new LogError('Alias is not allowed', alias)
const aliasInUse = await DbUser.find({
where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) },
})
if (aliasInUse.length !== 0) {
throw new LogError('Alias already in use', alias)
}
return true
}

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import createServer from './server/createServer'
import { createServer } from './server/createServer'
// config
import CONFIG from './config'
import { CONFIG } from './config'
async function main() {
// eslint-disable-next-line no-console

View File

@ -13,7 +13,7 @@ import cors from './cors'
import plugins from './plugins'
// config
import CONFIG from '@/config'
import { CONFIG } from '@/config'
// graphql
import schema from '@/graphql/schema'
@ -33,7 +33,7 @@ import { Logger } from 'log4js'
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
const createServer = async (
export const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// context: any = serverContext,
logger: Logger = apolloLogger,

View File

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-empty-interface */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Decimal } from 'decimal.js-light'
expect.extend({
decimalEqual(received, value) {
const pass = new Decimal(value).equals(received.toString())
if (pass) {
return {
message: () => `expected ${received} to not equal ${value}`,
pass: true,
}
} else {
return {
message: () => `expected ${received} to equal ${value}`,
pass: false,
}
}
},
})
interface CustomMatchers<R = unknown> {
decimalEqual(value: number): R
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Expect extends CustomMatchers {}
interface Matchers<R> extends CustomMatchers<R> {}
interface InverseAsymmetricMatchers extends CustomMatchers {}
}
}

View File

@ -4,11 +4,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import { createServer } from '@/server/createServer'
import { logger } from './testSetup'

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

@ -1057,6 +1057,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/ws@^7.0.0":
version "7.4.7"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
@ -1529,6 +1534,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"
@ -5437,16 +5447,16 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@8.3.2, uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.1.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"