mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3194 from gradido/3187-feature-x-sendcoind-23-invoke-settlement-of-x-pending-tx
feat(federation): x-com sendcoins 23: invoke settlement of pending tx
This commit is contained in:
commit
046285071e
@ -7,7 +7,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 86,
|
||||
lines: 85,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -5,7 +5,10 @@ import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { SendCoinsArgs } from './model/SendCoinsArgs'
|
||||
import { SendCoinsResult } from './model/SendCoinsResult'
|
||||
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
|
||||
@ -20,7 +23,7 @@ export class SendCoinsClient {
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
this.client = new GraphQLClient(this.endpoint, {
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
@ -28,27 +31,26 @@ export class SendCoinsClient {
|
||||
})
|
||||
}
|
||||
|
||||
voteForSendCoins = async (args: SendCoinsArgs): Promise<string | undefined> => {
|
||||
voteForSendCoins = async (args: SendCoinsArgs): Promise<SendCoinsResult> => {
|
||||
logger.debug('X-Com: voteForSendCoins against endpoint=', this.endpoint)
|
||||
try {
|
||||
logger.debug(`X-Com: SendCoinsClient: voteForSendCoins with args=`, args)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(voteForSendCoins, { args })
|
||||
logger.debug(`X-Com: SendCoinsClient: after rawRequest...data:`, data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.voteForSendCoins?.voteForSendCoins) {
|
||||
logger.warn(
|
||||
'X-Com: voteForSendCoins failed with: ',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
data.voteForSendCoins.voteForSendCoins,
|
||||
)
|
||||
return
|
||||
if (!data?.voteForSendCoins?.vote) {
|
||||
logger.warn('X-Com: voteForSendCoins failed with: ', data)
|
||||
return new SendCoinsResult()
|
||||
}
|
||||
logger.debug(
|
||||
'X-Com: voteForSendCoins successful with result=',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
data.voteForSendCoins,
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return data.voteForSendCoins.voteForSendCoins
|
||||
const result = new SendCoinsResult()
|
||||
result.vote = true
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
||||
result.recipGradidoID = data.voteForSendCoins.recipGradidoID
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
||||
result.recipName = data.voteForSendCoins.recipName
|
||||
logger.debug('X-Com: voteForSendCoins successful with result=', result)
|
||||
return result
|
||||
} catch (err) {
|
||||
throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err)
|
||||
}
|
||||
@ -57,37 +59,79 @@ export class SendCoinsClient {
|
||||
revertSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
|
||||
logger.debug('X-Com: revertSendCoins against endpoint=', this.endpoint)
|
||||
try {
|
||||
logger.debug(`X-Com: SendCoinsClient: revertSendCoins with args=`, args)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(revertSendCoins, { args })
|
||||
logger.debug(`X-Com: SendCoinsClient: after revertSendCoins: data=`, data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.revertSendCoins?.revertSendCoins) {
|
||||
if (!data?.revertSendCoins) {
|
||||
logger.warn('X-Com: revertSendCoins without response data from endpoint', this.endpoint)
|
||||
return false
|
||||
}
|
||||
logger.debug(`X-Com: revertSendCoins successful from endpoint=${this.endpoint}`)
|
||||
logger.debug(
|
||||
`X-Com: SendCoinsClient: revertSendCoins successful from endpoint=${this.endpoint}`,
|
||||
)
|
||||
return true
|
||||
} catch (err) {
|
||||
logger.error(`X-Com: revertSendCoins failed for endpoint=${this.endpoint}`, err)
|
||||
logger.error(
|
||||
`X-Com: SendCoinsClient: revertSendCoins failed for endpoint=${this.endpoint}`,
|
||||
err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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 {
|
||||
logger.debug(`X-Com: SendCoinsClient: settleSendCoins with args=`, args)
|
||||
// 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 })
|
||||
logger.debug(`X-Com: SendCoinsClient: after settleSendCoins: data=`, data)
|
||||
// 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) {
|
||||
logger.warn(
|
||||
'X-Com: SendCoinsClient: settleSendCoins without response data from endpoint',
|
||||
this.endpoint,
|
||||
)
|
||||
return false
|
||||
}
|
||||
logger.debug(`X-Com: commitSendCoins successful from endpoint=${this.endpoint}`)
|
||||
logger.debug(
|
||||
`X-Com: SendCoinsClient: 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: SendCoinsClient: settleSendCoins failed for endpoint=${this.endpoint}`,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
revertSettledSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
|
||||
logger.debug(`X-Com: revertSettledSendCoins against endpoint='${this.endpoint}'...`)
|
||||
try {
|
||||
logger.debug(`X-Com: SendCoinsClient: revertSettledSendCoins with args=`, args)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(revertSettledSendCoins, { args })
|
||||
logger.debug(`X-Com: SendCoinsClient: after revertSettledSendCoins: data=`, data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.revertSettledSendCoins) {
|
||||
logger.warn(
|
||||
'X-Com: SendCoinsClient: revertSettledSendCoins without response data from endpoint',
|
||||
this.endpoint,
|
||||
)
|
||||
return false
|
||||
}
|
||||
logger.debug(
|
||||
`X-Com: SendCoinsClient: revertSettledSendCoins successful from endpoint=${this.endpoint}`,
|
||||
)
|
||||
return true
|
||||
} catch (err) {
|
||||
throw new LogError(
|
||||
`X-Com: SendCoinsClient: revertSettledSendCoins failed for endpoint=${this.endpoint}`,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@ObjectType()
|
||||
export class SendCoinsResult {
|
||||
constructor() {
|
||||
this.vote = false
|
||||
@ -9,9 +9,9 @@ export class SendCoinsResult {
|
||||
@Field(() => Boolean)
|
||||
vote: boolean
|
||||
|
||||
@Field(() => String)
|
||||
receiverFirstName: string
|
||||
@Field(() => String, { nullable: true })
|
||||
recipGradidoID: string | null
|
||||
|
||||
@Field(() => String)
|
||||
receiverLastName: string
|
||||
@Field(() => String, { nullable: true })
|
||||
recipName: string | null
|
||||
}
|
||||
|
||||
@ -1,25 +1,30 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const revertSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
revertSendCoins(data: $args)
|
||||
}
|
||||
`
|
||||
/*
|
||||
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
|
||||
)
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const revertSettledSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
revertSettledSendCoins(data: $args)
|
||||
}
|
||||
`
|
||||
/*
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
*/
|
||||
30
backend/src/federation/client/1_0/query/settleSendCoins.ts
Normal file
30
backend/src/federation/client/1_0/query/settleSendCoins.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const settleSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
settleSendCoins(data: $args)
|
||||
}
|
||||
`
|
||||
/*
|
||||
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
|
||||
)
|
||||
}
|
||||
*/
|
||||
@ -1,25 +1,41 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const voteForSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
voteForSendCoins(data: $args) {
|
||||
vote
|
||||
recipGradidoID
|
||||
recipName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/*
|
||||
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
|
||||
creationDate: $creationDate
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
communitySenderIdentifier: $communitySenderIdentifier
|
||||
userSenderIdentifier: $userSenderIdentifier
|
||||
userSenderName: $userSenderName
|
||||
)
|
||||
data: {
|
||||
recipientCommunityUuid: $recipientCommunityUuid
|
||||
recipientUserIdentifier: $recipientUserIdentifier
|
||||
creationDate: $creationDate
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
senderCommunityUuid: $senderCommunityUuid
|
||||
senderUserUuid: $senderUserUuid
|
||||
senderUserName: $senderUserName
|
||||
}
|
||||
) {
|
||||
vote
|
||||
recipGradidoID
|
||||
recipName
|
||||
}
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -165,6 +165,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 +272,11 @@ export class TransactionLinkResolver {
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
/* local transaction will not carry homeComUuid for local users
|
||||
if (homeCom.communityUuid) {
|
||||
transaction.userCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
*/
|
||||
transaction.userGradidoID = user.gradidoID
|
||||
transaction.userName = fullName(user.firstName, user.lastName)
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { getConnection, In, IsNull } 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'
|
||||
@ -51,6 +53,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)
|
||||
}
|
||||
@ -330,13 +348,12 @@ export class TransactionResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async sendCoins(
|
||||
@Args()
|
||||
{ /* recipientCommunityIdentifier, */ recipientIdentifier, amount, memo }: TransactionSendArgs,
|
||||
{ recipientCommunityIdentifier, recipientIdentifier, amount, memo }: TransactionSendArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(
|
||||
`sendCoins(recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`,
|
||||
`sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`,
|
||||
)
|
||||
|
||||
const senderUser = getUser(context)
|
||||
|
||||
// validate recipient user
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/*
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
|
||||
@ -16,6 +17,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,
|
||||
@ -26,6 +29,7 @@ export async function processXComPendingSendCoins(
|
||||
sender: dbUser,
|
||||
recipient: dbUser,
|
||||
): Promise<boolean> {
|
||||
|
||||
try {
|
||||
logger.debug(
|
||||
`XCom: processXComPendingSendCoins...`,
|
||||
@ -49,22 +53,20 @@ 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)
|
||||
if (recipientName) {
|
||||
const sendCoinsResult = await client.voteForSendCoins(args)
|
||||
logger.debug(`X-Com: returnd from voteForSendCoins:`, sendCoinsResult)
|
||||
if (sendCoinsResult) {
|
||||
// writing the pending transaction on receiver-side was successfull, so now write the sender side
|
||||
try {
|
||||
const pendingTx = DbPendingTransaction.create()
|
||||
@ -77,7 +79,7 @@ export async function processXComPendingSendCoins(
|
||||
? receiverCom.communityUuid
|
||||
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
|
||||
pendingTx.linkedUserGradidoID = recipient.gradidoID
|
||||
pendingTx.linkedUserName = recipientName
|
||||
pendingTx.linkedUserName = sendCoinsResult.recipName
|
||||
pendingTx.memo = memo
|
||||
pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null
|
||||
pendingTx.state = PendingTransactionState.NEW
|
||||
@ -114,3 +116,96 @@ 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 acknowledge = await client.settleSendCoins(args)
|
||||
logger.debug(`X-Com: returnd from settleSendCoins:`, acknowledge)
|
||||
if (acknowledge) {
|
||||
// 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
|
||||
}
|
||||
*/
|
||||
|
||||
@ -0,0 +1,147 @@
|
||||
/* 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
|
||||
}
|
||||
*/
|
||||
34
backend/src/seeds/community/index.ts
Normal file
34
backend/src/seeds/community/index.ts
Normal 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}`)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -58,6 +58,8 @@ const virtualLinkTransaction = (
|
||||
userName: null,
|
||||
linkedUserGradidoID: null,
|
||||
linkedUserName: null,
|
||||
userCommunityUuid: null,
|
||||
linkedUserCommunityUuid: null,
|
||||
}
|
||||
return new Transaction(linkDbTransaction, user)
|
||||
}
|
||||
@ -92,6 +94,8 @@ const virtualDecayTransaction = (
|
||||
userName: null,
|
||||
linkedUserGradidoID: null,
|
||||
linkedUserName: null,
|
||||
userCommunityUuid: null,
|
||||
linkedUserCommunityUuid: null,
|
||||
}
|
||||
return new Transaction(decayDbTransaction, user)
|
||||
}
|
||||
|
||||
@ -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: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userCommunityUuid: string | null
|
||||
|
||||
@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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0070-add_dlt_transactions_table/Transaction'
|
||||
export { Transaction } from './0072-add_communityuuid_to_transactions_table/Transaction'
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
/* 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`;',
|
||||
)
|
||||
/* the migration of the HomeCom-UUID for local users in the transactions table will be skipped
|
||||
and be solved with the future users table migration for treating home- and foreign-users including
|
||||
homeCom- and foreignCom-UUIDs
|
||||
|
||||
// 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 && 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`,
|
||||
)
|
||||
}
|
||||
// leads to an error in case of empty communties table during CD/CI-pipeline-tests
|
||||
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`;')
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 76,
|
||||
lines: 75,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
12
federation/src/graphql/api/1_0/const/const.ts
Normal file
12
federation/src/graphql/api/1_0/const/const.ts
Normal 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
|
||||
@ -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, {
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
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
|
||||
}
|
||||
|
||||
17
federation/src/graphql/api/1_0/model/SendCoinsResult.ts
Normal file
17
federation/src/graphql/api/1_0/model/SendCoinsResult.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class SendCoinsResult {
|
||||
constructor() {
|
||||
this.vote = false
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
vote: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
recipGradidoID: string | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
recipName: string | null
|
||||
}
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -4,11 +4,14 @@ 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 Decimal from 'decimal.js-light'
|
||||
import { SendCoinsArgs } from '../model/SendCoinsArgs'
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'], con: Connection
|
||||
// let query: ApolloServerTestClient['query']
|
||||
@ -21,18 +24,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,115 +46,100 @@ afterAll(async () => {
|
||||
|
||||
describe('SendCoinsResolver', () => {
|
||||
const voteForSendCoinsMutation = `
|
||||
mutation (
|
||||
$communityReceiverIdentifier: String!
|
||||
$userReceiverIdentifier: String!
|
||||
$creationDate: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
$communitySenderIdentifier: String!
|
||||
$userSenderIdentifier: String!
|
||||
$userSenderName: String!
|
||||
) {
|
||||
voteForSendCoins(
|
||||
communityReceiverIdentifier: $communityReceiverIdentifier
|
||||
userReceiverIdentifier: $userReceiverIdentifier
|
||||
creationDate: $creationDate
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
communitySenderIdentifier: $communitySenderIdentifier
|
||||
userSenderIdentifier: $userSenderIdentifier
|
||||
userSenderName: $userSenderName
|
||||
)
|
||||
}
|
||||
`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
voteForSendCoins(data: $args) {
|
||||
vote
|
||||
recipGradidoID
|
||||
recipName
|
||||
}
|
||||
}`
|
||||
const settleSendCoinsMutation = `
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
settleSendCoins(data: $args)
|
||||
}`
|
||||
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
|
||||
)
|
||||
}
|
||||
`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
revertSendCoins(data: $args)
|
||||
}`
|
||||
const revertSettledSendCoinsMutation = `
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
revertSettledSendCoins(data: $args)
|
||||
}`
|
||||
|
||||
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()
|
||||
const args = new SendCoinsArgs()
|
||||
args.recipientCommunityUuid = 'invalid foreignCom'
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = new Date().toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: voteForSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: 'invalid foreignCom',
|
||||
userReceiverIdentifier: 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),
|
||||
},
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('voteForSendCoins with wrong communityReceiverIdentifier')],
|
||||
errors: [new GraphQLError('voteForSendCoins with wrong recipientCommunityUuid')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -160,25 +148,29 @@ describe('SendCoinsResolver', () => {
|
||||
describe('unknown recipient user', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = 'invalid recipient'
|
||||
args.creationDate = new Date().toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: voteForSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: foreignCom.communityUuid,
|
||||
userReceiverIdentifier: '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),
|
||||
},
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'voteForSendCoins with unknown userReceiverIdentifier in the community=',
|
||||
'voteForSendCoins with unknown recipientUserIdentifier in the community=',
|
||||
),
|
||||
],
|
||||
}),
|
||||
@ -189,24 +181,32 @@ describe('SendCoinsResolver', () => {
|
||||
describe('valid X-Com-TX voted', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = new Date().toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: voteForSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: foreignCom.communityUuid,
|
||||
userReceiverIdentifier: 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),
|
||||
},
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
voteForSendCoins: 'recipUser-FirstName recipUser-LastName',
|
||||
voteForSendCoins: {
|
||||
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
|
||||
recipName: 'recipUser-FirstName recipUser-LastName',
|
||||
vote: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
@ -215,83 +215,50 @@ describe('SendCoinsResolver', () => {
|
||||
})
|
||||
|
||||
describe('revertSendCoins', () => {
|
||||
let homeCom: DbCommunity
|
||||
let foreignCom: DbCommunity
|
||||
let sendUser: DbUser
|
||||
let recipUser: DbUser
|
||||
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)
|
||||
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
await mutate({
|
||||
mutation: voteForSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: foreignCom.communityUuid,
|
||||
userReceiverIdentifier: recipUser.gradidoID,
|
||||
creationDate: creationDate.toISOString(),
|
||||
amount: 100,
|
||||
memo: 'X-Com-TX memo',
|
||||
communitySenderIdentifier: homeCom.communityUuid,
|
||||
userSenderIdentifier: sendUser.gradidoID,
|
||||
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
|
||||
},
|
||||
variables: { args },
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown recipient community', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
args.recipientCommunityUuid = 'invalid foreignCom'
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: revertSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: 'invalid foreignCom',
|
||||
userReceiverIdentifier: recipUser.gradidoID,
|
||||
creationDate: creationDate.toISOString(),
|
||||
amount: 100,
|
||||
memo: 'X-Com-TX memo',
|
||||
communitySenderIdentifier: homeCom.communityUuid,
|
||||
userSenderIdentifier: sendUser.gradidoID,
|
||||
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
|
||||
},
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('revertSendCoins with wrong communityReceiverIdentifier')],
|
||||
errors: [new GraphQLError('revertSendCoins with wrong recipientCommunityUuid')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -300,25 +267,29 @@ describe('SendCoinsResolver', () => {
|
||||
describe('unknown recipient user', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = 'invalid recipient'
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: revertSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: foreignCom.communityUuid,
|
||||
userReceiverIdentifier: 'invalid recipient',
|
||||
creationDate: creationDate.toISOString(),
|
||||
amount: 100,
|
||||
memo: 'X-Com-TX memo',
|
||||
communitySenderIdentifier: homeCom.communityUuid,
|
||||
userSenderIdentifier: sendUser.gradidoID,
|
||||
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
|
||||
},
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'revertSendCoins with unknown userReceiverIdentifier in the community=',
|
||||
'revertSendCoins with unknown recipientUserIdentifier in the community=',
|
||||
),
|
||||
],
|
||||
}),
|
||||
@ -329,19 +300,23 @@ describe('SendCoinsResolver', () => {
|
||||
describe('valid X-Com-TX reverted', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: revertSendCoinsMutation,
|
||||
variables: {
|
||||
communityReceiverIdentifier: foreignCom.communityUuid,
|
||||
userReceiverIdentifier: recipUser.gradidoID,
|
||||
creationDate: creationDate.toISOString(),
|
||||
amount: 100,
|
||||
memo: 'X-Com-TX memo',
|
||||
communitySenderIdentifier: homeCom.communityUuid,
|
||||
userSenderIdentifier: sendUser.gradidoID,
|
||||
userSenderName: fullName(sendUser.firstName, sendUser.lastName),
|
||||
},
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
@ -353,4 +328,249 @@ describe('SendCoinsResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('settleSendCoins', () => {
|
||||
const creationDate = new Date()
|
||||
|
||||
beforeEach(async () => {
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
await mutate({
|
||||
mutation: voteForSendCoinsMutation,
|
||||
variables: { args },
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown recipient community', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
args.recipientCommunityUuid = 'invalid foreignCom'
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: settleSendCoinsMutation,
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('settleSendCoins with wrong recipientCommunityUuid')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown recipient user', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = 'invalid recipient'
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: settleSendCoinsMutation,
|
||||
variables: { args },
|
||||
}),
|
||||
).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()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: settleSendCoinsMutation,
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
settleSendCoins: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('revertSettledSendCoins', () => {
|
||||
const creationDate = new Date()
|
||||
|
||||
beforeEach(async () => {
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
await mutate({
|
||||
mutation: voteForSendCoinsMutation,
|
||||
variables: { args },
|
||||
})
|
||||
await mutate({
|
||||
mutation: settleSendCoinsMutation,
|
||||
variables: { args },
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown recipient community', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
args.recipientCommunityUuid = 'invalid foreignCom'
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: revertSettledSendCoinsMutation,
|
||||
variables: { args },
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('revertSettledSendCoins with wrong recipientCommunityUuid')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown recipient user', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = 'invalid recipient'
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: revertSettledSendCoinsMutation,
|
||||
variables: { args },
|
||||
}),
|
||||
).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()
|
||||
const args = new SendCoinsArgs()
|
||||
if (foreignCom.communityUuid) {
|
||||
args.recipientCommunityUuid = foreignCom.communityUuid
|
||||
}
|
||||
args.recipientUserIdentifier = recipUser.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = new Decimal(100)
|
||||
args.memo = 'X-Com-TX memo'
|
||||
if (homeCom.communityUuid) {
|
||||
args.senderCommunityUuid = homeCom.communityUuid
|
||||
}
|
||||
args.senderUserUuid = sendUser.gradidoID
|
||||
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: revertSettledSendCoinsMutation,
|
||||
variables: { args },
|
||||
}),
|
||||
).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
|
||||
}
|
||||
|
||||
@ -1,78 +1,106 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Args, Mutation, Resolver } from 'type-graphql'
|
||||
import { Arg, Args, Mutation, Resolver } from 'type-graphql'
|
||||
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 Decimal from 'decimal.js-light'
|
||||
import { calculateRecipientBalance } from '../util/calculateRecipientBalance'
|
||||
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'
|
||||
import { SendCoinsResult } from '../model/SendCoinsResult'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class SendCoinsResolver {
|
||||
@Mutation(() => String)
|
||||
@Mutation(() => SendCoinsResult)
|
||||
async voteForSendCoins(
|
||||
@Args()
|
||||
{
|
||||
communityReceiverIdentifier,
|
||||
userReceiverIdentifier,
|
||||
creationDate,
|
||||
amount,
|
||||
memo,
|
||||
communitySenderIdentifier,
|
||||
userSenderIdentifier,
|
||||
userSenderName,
|
||||
}: SendCoinsArgs,
|
||||
): Promise<string | null> {
|
||||
logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`)
|
||||
let result: string | null = null
|
||||
@Arg('data')
|
||||
args: SendCoinsArgs,
|
||||
): Promise<SendCoinsResult> {
|
||||
logger.debug(
|
||||
`voteForSendCoins() via apiVersion=1_0 ...`,
|
||||
args.recipientCommunityUuid,
|
||||
args.recipientUserIdentifier,
|
||||
args.creationDate,
|
||||
args.amount.toString(),
|
||||
args.memo,
|
||||
args.senderCommunityUuid,
|
||||
args.senderUserUuid,
|
||||
args.senderUserName,
|
||||
)
|
||||
const result = new SendCoinsResult()
|
||||
// first check if receiver community is correct
|
||||
const homeCom = await DbCommunity.findOneBy({
|
||||
communityUuid: communityReceiverIdentifier,
|
||||
communityUuid: args.recipientCommunityUuid,
|
||||
})
|
||||
if (!homeCom) {
|
||||
throw new LogError(
|
||||
`voteForSendCoins with wrong communityReceiverIdentifier`,
|
||||
communityReceiverIdentifier,
|
||||
`voteForSendCoins with wrong recipientCommunityUuid`,
|
||||
args.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(args.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,
|
||||
)
|
||||
}
|
||||
const openSenderPendingTx = await DbPendingTransaction.count({
|
||||
where: [
|
||||
{ userGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW },
|
||||
{ linkedUserGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW },
|
||||
],
|
||||
})
|
||||
const openReceiverPendingTx = await DbPendingTransaction.count({
|
||||
where: [
|
||||
{ userGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW },
|
||||
{ linkedUserGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW },
|
||||
],
|
||||
})
|
||||
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
|
||||
throw new LogError(
|
||||
`There exist still ongoing 'Pending-Transactions' for the involved users on receiver-side!`,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const txDate = new Date(creationDate)
|
||||
const receiveBalance = await calculateRecipientBalance(receiverUser.id, amount, txDate)
|
||||
const txDate = new Date(args.creationDate)
|
||||
const receiveBalance = await calculateRecipientBalance(receiverUser.id, args.amount, txDate)
|
||||
const pendingTx = DbPendingTransaction.create()
|
||||
pendingTx.amount = amount
|
||||
pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0)
|
||||
pendingTx.amount = args.amount
|
||||
pendingTx.balance = receiveBalance ? receiveBalance.balance : args.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.memo = memo
|
||||
pendingTx.linkedUserCommunityUuid = args.senderCommunityUuid
|
||||
pendingTx.linkedUserGradidoID = args.senderUserUuid
|
||||
pendingTx.linkedUserName = args.senderUserName
|
||||
pendingTx.memo = args.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 = args.recipientCommunityUuid
|
||||
pendingTx.userGradidoID = receiverUser.gradidoID
|
||||
pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName)
|
||||
|
||||
await DbPendingTransaction.insert(pendingTx)
|
||||
result = pendingTx.userName
|
||||
result.vote = true
|
||||
result.recipName = pendingTx.userName
|
||||
result.recipGradidoID = pendingTx.userGradidoID
|
||||
logger.debug(`voteForSendCoins()-1_0... successfull`)
|
||||
} catch (err) {
|
||||
throw new LogError(`Error in voteForSendCoins: `, err)
|
||||
@ -82,49 +110,43 @@ export class SendCoinsResolver {
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async revertSendCoins(
|
||||
@Args()
|
||||
{
|
||||
communityReceiverIdentifier,
|
||||
userReceiverIdentifier,
|
||||
creationDate,
|
||||
amount,
|
||||
memo,
|
||||
communitySenderIdentifier,
|
||||
userSenderIdentifier,
|
||||
userSenderName,
|
||||
}: SendCoinsArgs,
|
||||
@Arg('data')
|
||||
args: 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: args.recipientCommunityUuid,
|
||||
})
|
||||
if (!homeCom) {
|
||||
throw new LogError(
|
||||
`revertSendCoins with wrong communityReceiverIdentifier`,
|
||||
communityReceiverIdentifier,
|
||||
`revertSendCoins with wrong recipientCommunityUuid`,
|
||||
args.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(args.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: args.recipientCommunityUuid,
|
||||
userGradidoID: receiverUser.gradidoID,
|
||||
state: PendingTransactionState.NEW,
|
||||
typeId: TransactionTypeId.RECEIVE,
|
||||
balanceDate: new Date(creationDate),
|
||||
linkedUserCommunityUuid: communitySenderIdentifier,
|
||||
linkedUserGradidoID: userSenderIdentifier,
|
||||
balanceDate: new Date(args.creationDate),
|
||||
linkedUserCommunityUuid: args.senderCommunityUuid,
|
||||
linkedUserGradidoID: args.senderUserUuid,
|
||||
})
|
||||
logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx)
|
||||
if (pendingTx && pendingTx.amount.toString() === amount.toString()) {
|
||||
if (pendingTx && pendingTx.amount.toString() === args.amount.toString()) {
|
||||
logger.debug('XCom: revertSendCoins matching pendingTX for remove...')
|
||||
try {
|
||||
await pendingTx.remove()
|
||||
@ -135,21 +157,21 @@ export class SendCoinsResolver {
|
||||
} else {
|
||||
logger.debug(
|
||||
'XCom: revertSendCoins NOT matching pendingTX for remove:',
|
||||
pendingTx?.amount,
|
||||
amount,
|
||||
pendingTx?.amount.toString(),
|
||||
args.amount.toString(),
|
||||
)
|
||||
throw new LogError(
|
||||
`Can't find in revertSendCoins the pending receiver TX for args=`,
|
||||
communityReceiverIdentifier,
|
||||
userReceiverIdentifier,
|
||||
args.recipientCommunityUuid,
|
||||
args.recipientUserIdentifier,
|
||||
PendingTransactionState.NEW,
|
||||
TransactionTypeId.RECEIVE,
|
||||
creationDate,
|
||||
amount,
|
||||
memo,
|
||||
communitySenderIdentifier,
|
||||
userSenderIdentifier,
|
||||
userSenderName,
|
||||
args.creationDate,
|
||||
args.amount,
|
||||
args.memo,
|
||||
args.senderCommunityUuid,
|
||||
args.senderUserUuid,
|
||||
args.senderUserName,
|
||||
)
|
||||
}
|
||||
logger.debug(`revertSendCoins()-1_0... successfull`)
|
||||
@ -158,4 +180,146 @@ export class SendCoinsResolver {
|
||||
throw new LogError(`Error in revertSendCoins: `, err)
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async settleSendCoins(
|
||||
@Arg('data')
|
||||
args: SendCoinsArgs,
|
||||
): Promise<boolean> {
|
||||
logger.debug(
|
||||
`settleSendCoins() via apiVersion=1_0 ...userCommunityUuid=${
|
||||
args.recipientCommunityUuid
|
||||
}, userGradidoID=${args.recipientUserIdentifier}, balanceDate=${
|
||||
args.creationDate
|
||||
},amount=${args.amount.valueOf()}, memo=${args.memo}, linkedUserCommunityUuid = ${
|
||||
args.senderCommunityUuid
|
||||
}, userSenderIdentifier=${args.senderUserUuid}, userSenderName=${args.senderUserName}`,
|
||||
)
|
||||
// first check if receiver community is correct
|
||||
const homeCom = await DbCommunity.findOneBy({
|
||||
communityUuid: args.recipientCommunityUuid,
|
||||
})
|
||||
if (!homeCom) {
|
||||
throw new LogError(
|
||||
`settleSendCoins with wrong recipientCommunityUuid`,
|
||||
args.recipientCommunityUuid,
|
||||
)
|
||||
}
|
||||
let receiverUser
|
||||
try {
|
||||
// second check if receiver user exists in this community
|
||||
receiverUser = await findUserByIdentifier(args.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: args.recipientCommunityUuid,
|
||||
userGradidoID: receiverUser.gradidoID,
|
||||
state: PendingTransactionState.NEW,
|
||||
typeId: TransactionTypeId.RECEIVE,
|
||||
balanceDate: new Date(args.creationDate),
|
||||
linkedUserCommunityUuid: args.senderCommunityUuid,
|
||||
linkedUserGradidoID: args.senderUserUuid,
|
||||
})
|
||||
logger.debug('XCom: settleSendCoins found pendingTX=', pendingTx?.toString())
|
||||
if (
|
||||
pendingTx &&
|
||||
pendingTx.amount.toString() === args.amount.toString() &&
|
||||
pendingTx.memo === args.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=`,
|
||||
args.recipientCommunityUuid,
|
||||
args.recipientUserIdentifier,
|
||||
PendingTransactionState.NEW,
|
||||
TransactionTypeId.RECEIVE,
|
||||
args.creationDate,
|
||||
args.amount,
|
||||
args.memo,
|
||||
args.senderCommunityUuid,
|
||||
args.senderUserUuid,
|
||||
args.senderUserName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async revertSettledSendCoins(
|
||||
@Arg('data')
|
||||
args: SendCoinsArgs,
|
||||
): Promise<boolean> {
|
||||
logger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`)
|
||||
// first check if receiver community is correct
|
||||
const homeCom = await DbCommunity.findOneBy({
|
||||
communityUuid: args.recipientCommunityUuid,
|
||||
})
|
||||
if (!homeCom) {
|
||||
throw new LogError(
|
||||
`revertSettledSendCoins with wrong recipientCommunityUuid`,
|
||||
args.recipientCommunityUuid,
|
||||
)
|
||||
}
|
||||
let receiverUser
|
||||
try {
|
||||
// second check if receiver user exists in this community
|
||||
receiverUser = await findUserByIdentifier(args.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: args.recipientCommunityUuid,
|
||||
userGradidoID: args.recipientUserIdentifier,
|
||||
state: PendingTransactionState.SETTLED,
|
||||
typeId: TransactionTypeId.RECEIVE,
|
||||
balanceDate: new Date(args.creationDate),
|
||||
linkedUserCommunityUuid: args.senderCommunityUuid,
|
||||
linkedUserGradidoID: args.senderUserUuid,
|
||||
})
|
||||
logger.debug('XCom: revertSettledSendCoins found pendingTX=', pendingTx)
|
||||
if (
|
||||
pendingTx &&
|
||||
pendingTx.amount.toString() === args.amount.toString() &&
|
||||
pendingTx.memo === args.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=`,
|
||||
args.recipientCommunityUuid,
|
||||
args.recipientUserIdentifier,
|
||||
PendingTransactionState.SETTLED,
|
||||
TransactionTypeId.RECEIVE,
|
||||
args.creationDate,
|
||||
args.amount,
|
||||
args.memo,
|
||||
args.senderCommunityUuid,
|
||||
args.senderUserUuid,
|
||||
args.senderUserName,
|
||||
)
|
||||
}
|
||||
logger.debug(`revertSendCoins()-1_0... successfull`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 calculateRecipientBalance(
|
||||
userId: number,
|
||||
@ -0,0 +1,158 @@
|
||||
/* 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)
|
||||
logger.debug(`LastTransaction vs PendingTransaction`)
|
||||
logger.debug(`balance:`, lastTransaction?.balance.toString(), pendingTx.balance.toString())
|
||||
logger.debug(
|
||||
`balanceDate:`,
|
||||
lastTransaction?.balanceDate.toISOString(),
|
||||
pendingTx.balanceDate.toISOString(),
|
||||
)
|
||||
logger.debug(`GradidoID:`, lastTransaction?.userGradidoID, pendingTx.userGradidoID)
|
||||
logger.debug(`Name:`, lastTransaction?.userName, pendingTx.userName)
|
||||
logger.debug(`amount:`, lastTransaction?.amount.toString(), pendingTx.amount.toString())
|
||||
logger.debug(`memo:`, lastTransaction?.memo, pendingTx.memo)
|
||||
logger.debug(
|
||||
`linkedUserGradidoID:`,
|
||||
lastTransaction?.linkedUserGradidoID,
|
||||
pendingTx.linkedUserGradidoID,
|
||||
)
|
||||
logger.debug(`linkedUserName:`, lastTransaction?.linkedUserName, pendingTx.linkedUserName)
|
||||
// now the last Tx must be the equivalant to the pendingTX
|
||||
if (
|
||||
lastTransaction &&
|
||||
lastTransaction.balance.comparedTo(pendingTx.balance) === 0 &&
|
||||
lastTransaction.balanceDate.toISOString() === pendingTx.balanceDate.toISOString() &&
|
||||
lastTransaction.userGradidoID === pendingTx.userGradidoID &&
|
||||
lastTransaction.userName === pendingTx.userName &&
|
||||
lastTransaction.amount.comparedTo(pendingTx.amount) === 0 &&
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
/* 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,
|
||||
)
|
||||
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
|
||||
}
|
||||
4
federation/src/graphql/util/TRANSACTIONS_LOCK.ts
Normal file
4
federation/src/graphql/util/TRANSACTIONS_LOCK.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Semaphore } from 'await-semaphore'
|
||||
|
||||
const CONCURRENT_TRANSACTIONS = 1
|
||||
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)
|
||||
25
federation/src/graphql/util/checkTradingLevel.ts
Normal file
25
federation/src/graphql/util/checkTradingLevel.ts
Normal 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 with 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
|
||||
}
|
||||
42
federation/src/graphql/util/findUserByIdentifier.ts
Normal file
42
federation/src/graphql/util/findUserByIdentifier.ts
Normal 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
|
||||
}
|
||||
39
federation/src/graphql/util/validateAlias.ts
Normal file
39
federation/src/graphql/util/validateAlias.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
37
federation/test/extensions.ts
Normal file
37
federation/test/extensions.ts
Normal 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 {}
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'. */
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user