Merge branch 'master' into 3204-release-1_23_3

This commit is contained in:
clauspeterhuebner 2023-10-05 12:13:50 +02:00 committed by GitHub
commit c9ebb0ff3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2333 additions and 458 deletions

View File

@ -1,4 +1,5 @@
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
CONFIG_VERSION=v20.2023-09-19
# Server
JWT_SECRET=$JWT_SECRET

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 86,
lines: 84,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -12,14 +12,14 @@ 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
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v19.2023-08-25',
EXPECTED: 'v20.2023-09-19',
CURRENT: '',
},
}
@ -122,8 +122,11 @@ if (
}
const federation = {
FEDERATION_BACKEND_SEND_ON_API: process.env.FEDERATION_BACKEND_SEND_ON_API ?? '1_0',
FEDERATION_VALIDATE_COMMUNITY_TIMER:
Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
FEDERATION_XCOM_SENDCOINS_ENABLED:
process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' || false,
// default value for community-uuid is equal uuid of stage-3
FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID:
process.env.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID ?? '56a55482-909e-46a4-bfa2-cd025e894ebc',

View File

@ -5,8 +5,11 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { SendCoinsArgs } from './model/SendCoinsArgs'
import { revertSendCoins } from './query/revertSendCoins'
import { voteForSendCoins } from './query/voteForSendCoins'
import { SendCoinsResult } from './model/SendCoinsResult'
import { revertSendCoins as revertSendCoinsQuery } from './query/revertSendCoins'
import { revertSettledSendCoins as revertSettledSendCoinsQuery } from './query/revertSettledSendCoins'
import { settleSendCoins as settleSendCoinsQuery } from './query/settleSendCoins'
import { voteForSendCoins as voteForSendCoinsQuery } from './query/voteForSendCoins'
// eslint-disable-next-line camelcase
export class SendCoinsClient {
@ -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,66 +31,111 @@ export class SendCoinsClient {
})
}
voteForSendCoins = async (args: SendCoinsArgs): Promise<string | undefined> => {
async voteForSendCoins(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 })
const { data } = await this.client.rawRequest(voteForSendCoinsQuery, { 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.debug('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.recipFirstName = data.voteForSendCoins.recipFirstName
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
result.recipLastName = data.voteForSendCoins.recipLastName
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
result.recipAlias = data.voteForSendCoins.recipAlias
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)
}
}
revertSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
async revertSendCoins(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 })
const { data } = await this.client.rawRequest(revertSendCoinsQuery, { 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}'...`)
async settleSendCoins(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(settleSendCoinsQuery, { 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,
)
}
}
async revertSettledSendCoins(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(revertSettledSendCoinsQuery, { 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,
)
}
}
*/
}

View File

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

View File

@ -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,15 @@ 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 })
recipFirstName: string | null
@Field(() => String, { nullable: true })
recipLastName: string | null
@Field(() => String, { nullable: true })
recipAlias: string | null
}

View File

@ -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
)
}
`
*/

View File

@ -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
)
}
*/

View 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
)
}
*/

View File

@ -1,25 +1,13 @@
import { gql } from 'graphql-request'
export const voteForSendCoins = gql`
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
recipFirstName
recipLastName
recipAlias
}
}
`

View File

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

View File

@ -5,24 +5,26 @@ import { KlickTipp } from './KlickTipp'
@ObjectType()
export class User {
constructor(user: dbUser) {
this.id = user.id
this.gradidoID = user.gradidoID
this.alias = user.alias
if (user.emailContact) {
this.emailChecked = user.emailContact.emailChecked
constructor(user: dbUser | null) {
if (user) {
this.id = user.id
this.gradidoID = user.gradidoID
this.alias = user.alias
if (user.emailContact) {
this.emailChecked = user.emailContact.emailChecked
}
this.firstName = user.firstName
this.lastName = user.lastName
this.deletedAt = user.deletedAt
this.createdAt = user.createdAt
this.language = user.language
this.publisherId = user.publisherId
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
this.klickTipp = null
this.hasElopage = null
this.hideAmountGDD = user.hideAmountGDD
this.hideAmountGDT = user.hideAmountGDT
}
this.firstName = user.firstName
this.lastName = user.lastName
this.deletedAt = user.deletedAt
this.createdAt = user.createdAt
this.language = user.language
this.publisherId = user.publisherId
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
this.klickTipp = null
this.hasElopage = null
this.hideAmountGDD = user.hideAmountGDD
this.hideAmountGDT = user.hideAmountGDT
}
@Field(() => Int)

View File

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

View File

@ -1,9 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Connection, In } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { DltTransaction } from '@entity/DltTransaction'
import { Event as DbEvent } from '@entity/Event'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { ApolloServerTestClient } from 'apollo-server-testing'
@ -12,7 +17,11 @@ import { GraphQLError } from 'graphql'
import { cleanDB, testEnvironment } from '@test/helpers'
import { logger } from '@test/testSetup'
import { CONFIG } from '@/config'
import { EventType } from '@/event/Events'
import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs'
import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult'
import { SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient'
import { userFactory } from '@/seeds/factory/user'
import {
confirmContribution,
@ -46,7 +55,7 @@ beforeAll(async () => {
afterAll(async () => {
await cleanDB()
await con.close()
await con.destroy() // close()
})
let bobData: any
@ -56,12 +65,38 @@ let user: User[]
let bob: User
let peter: User
let homeCom: DbCommunity
let foreignCom: DbCommunity
let fedForeignCom: DbFederatedCommunity
describe('send coins', () => {
beforeAll(async () => {
peter = await userFactory(testEnv, peterLustig)
bob = await userFactory(testEnv, bobBaumeister)
await userFactory(testEnv, stephenHawking)
await userFactory(testEnv, garrickOllivander)
homeCom = DbCommunity.create()
homeCom.communityUuid = '7f474922-b6d8-4b64-8cd0-ebf0a1d875aa'
homeCom.creationDate = new Date('2000-01-01')
homeCom.description = 'homeCom description'
homeCom.foreign = false
homeCom.name = 'homeCom name'
homeCom.privateKey = Buffer.from('homeCom privateKey')
homeCom.publicKey = Buffer.from('homeCom publicKey')
homeCom.url = 'homeCom url'
homeCom = await DbCommunity.save(homeCom)
foreignCom = DbCommunity.create()
foreignCom.communityUuid = '7f474922-b6d8-4b64-8cd0-cea0a1d875bb'
foreignCom.creationDate = new Date('2000-06-06')
foreignCom.description = 'foreignCom description'
foreignCom.foreign = true
foreignCom.name = 'foreignCom name'
foreignCom.privateKey = Buffer.from('foreignCom privateKey')
foreignCom.publicKey = Buffer.from('foreignCom publicKey')
foreignCom.url = 'foreignCom_url'
foreignCom.authenticatedAt = new Date('2000-06-12')
foreignCom = await DbCommunity.save(foreignCom)
bobData = {
email: 'bob@baumeister.de',
@ -91,6 +126,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'wrong@email.com',
amount: 100,
memo: 'test test',
@ -119,6 +155,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'stephen@hawking.uk',
amount: 100,
memo: 'test test',
@ -148,6 +185,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'garrick@ollivander.com',
amount: 100,
memo: 'test test',
@ -184,6 +222,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'bob@baumeister.de',
amount: 100,
memo: 'test test',
@ -207,6 +246,7 @@ describe('send coins', () => {
const { errors: errorObjects } = await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 100,
memo: 'Test',
@ -238,6 +278,7 @@ describe('send coins', () => {
const { errors: errorObjects } = await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
@ -270,6 +311,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 100,
memo: 'testing',
@ -319,6 +361,7 @@ describe('send coins', () => {
const { errors: errorObjects } = await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: -50,
memo: 'testing negative',
@ -350,6 +393,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 50,
memo: 'unrepeatable memo',
@ -456,6 +500,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: peter?.gradidoID,
amount: 10,
memo: 'send via gradido ID',
@ -496,6 +541,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'bob',
amount: 6.66,
memo: 'send via alias',
@ -558,12 +604,75 @@ describe('send coins', () => {
})
})
describe('X-Com send coins via gradido ID', () => {
beforeAll(async () => {
CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED = true
fedForeignCom = DbFederatedCommunity.create()
fedForeignCom.apiVersion = '1_0'
fedForeignCom.foreign = true
fedForeignCom.publicKey = Buffer.from('foreignCom publicKey')
fedForeignCom.endPoint = 'http://foreignCom_url/api'
fedForeignCom.lastAnnouncedAt = new Date('2000-06-09')
fedForeignCom.verifiedAt = new Date('2000-06-10')
fedForeignCom = await DbFederatedCommunity.save(fedForeignCom)
jest
.spyOn(SendCoinsClient.prototype, 'voteForSendCoins')
.mockImplementation(async (args: SendCoinsArgs): Promise<SendCoinsResult> => {
logger.debug('mock of voteForSendCoins...', args)
return Promise.resolve({
vote: true,
recipFirstName: peter.firstName,
recipLastName: peter.lastName,
recipGradidoID: args.recipientUserIdentifier,
recipAlias: peter.alias,
})
})
jest
.spyOn(SendCoinsClient.prototype, 'settleSendCoins')
.mockImplementation(async (args: SendCoinsArgs): Promise<boolean> => {
logger.debug('mock of settleSendCoins...', args)
return Promise.resolve(true)
})
await mutate({
mutation: login,
variables: bobData,
})
})
afterAll(() => {
jest.clearAllMocks()
})
it('sends the coins', async () => {
await expect(
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: foreignCom.communityUuid,
recipientIdentifier: peter?.gradidoID,
amount: 10,
memo: 'x-com send via gradido ID',
},
}),
).resolves.toMatchObject({
data: {
sendCoins: true,
},
errors: undefined,
})
})
})
describe('more transactions to test semaphore', () => {
it('sends the coins four times in a row', async () => {
await expect(
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 10,
memo: 'first transaction',
@ -580,6 +689,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 20,
memo: 'second transaction',
@ -596,6 +706,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 30,
memo: 'third transaction',
@ -612,6 +723,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 40,
memo: 'fourth transaction',

View File

@ -3,6 +3,8 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection, In, IsNull } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User as dbUser } from '@entity/User'
@ -12,17 +14,20 @@ 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'
import { User } from '@model/User'
import { RIGHTS } from '@/auth/RIGHTS'
import { CONFIG } from '@/config'
import {
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
} from '@/emails/sendEmailVariants'
import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Events'
import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult'
import { Context, getUser } from '@/server/context'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
@ -33,9 +38,14 @@ import { calculateBalance } from '@/util/validate'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
import { BalanceResolver } from './BalanceResolver'
import { isCommunityAuthenticated, isHomeCommunity } from './util/communities'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { getLastTransaction } from './util/getLastTransaction'
import { getTransactionList } from './util/getTransactionList'
import {
processXComCommittingSendCoins,
processXComPendingSendCoins,
} from './util/processXComSendCoins'
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
import { transactionLinkSummary } from './util/transactionLinkSummary'
@ -51,6 +61,24 @@ 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 exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`,
)
}
if (sender.id === recipient.id) {
throw new LogError('Sender and Recipient are the same', sender.id)
}
@ -221,12 +249,21 @@ export class TransactionResolver {
// find involved users; I am involved
const involvedUserIds: number[] = [user.id]
const involvedRemoteUsers: User[] = []
userTransactions.forEach((transaction: dbTransaction) => {
if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) {
involvedUserIds.push(transaction.linkedUserId)
}
if (!transaction.linkedUserId && transaction.linkedUserGradidoID) {
const remoteUser = new User(null)
remoteUser.gradidoID = transaction.linkedUserGradidoID
remoteUser.firstName = transaction.linkedUserName
remoteUser.lastName = '(GradidoID: ' + transaction.linkedUserGradidoID + ')'
involvedRemoteUsers.push(remoteUser)
}
})
logger.debug(`involvedUserIds=${involvedUserIds}`)
logger.debug(`involvedUserIds=`, involvedUserIds)
logger.debug(`involvedRemoteUsers=`, involvedRemoteUsers)
// We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser.find({
@ -235,7 +272,7 @@ export class TransactionResolver {
relations: ['emailContact'],
})
const involvedUsers = involvedDbUsers.map((u) => new User(u))
logger.debug(`involvedUsers=${involvedUsers}`)
logger.debug(`involvedUsers=`, involvedUsers)
const self = new User(user)
const transactions: Transaction[] = []
@ -305,10 +342,25 @@ export class TransactionResolver {
// transactions
userTransactions.forEach((userTransaction: dbTransaction) => {
/*
const linkedUser =
userTransaction.typeId === TransactionTypeId.CREATION
? communityUser
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
*/
let linkedUser: User | undefined
if (userTransaction.typeId === TransactionTypeId.CREATION) {
linkedUser = communityUser
logger.debug('CREATION-linkedUser=', linkedUser)
} else if (userTransaction.linkedUserId) {
linkedUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
logger.debug('local linkedUser=', linkedUser)
} else if (userTransaction.linkedUserCommunityUuid) {
linkedUser = involvedRemoteUsers.find(
(u) => u.gradidoID === userTransaction.linkedUserGradidoID,
)
logger.debug('remote linkedUser=', linkedUser)
}
transactions.push(new Transaction(userTransaction, self, linkedUser))
})
logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`)
@ -330,23 +382,87 @@ 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})`,
logger.debug(
`sendCoins(recipientCommunityIdentifier=${recipientCommunityIdentifier}, recipientIdentifier=${recipientIdentifier}, amount=${amount}, memo=${memo})`,
)
const homeCom = await DbCommunity.findOneOrFail({ where: { foreign: false } })
const senderUser = getUser(context)
// validate recipient user
const recipientUser = await findUserByIdentifier(recipientIdentifier)
if (!recipientUser) {
throw new LogError('The recipient user was not found', recipientUser)
}
if (!recipientCommunityIdentifier || (await isHomeCommunity(recipientCommunityIdentifier))) {
// processing sendCoins within sender and recepient are both in home community
// validate recipient user
const recipientUser = await findUserByIdentifier(recipientIdentifier)
if (!recipientUser) {
throw new LogError('The recipient user was not found', recipientUser)
}
await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
} else {
// processing a x-community sendCoins
logger.debug('X-Com: processing a x-community transaction...')
if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) {
throw new LogError('X-Community sendCoins disabled per configuration!')
}
if (!(await isCommunityAuthenticated(recipientCommunityIdentifier))) {
throw new LogError('recipient commuity is connected, but still not authenticated yet!')
}
const recipCom = await DbCommunity.findOneOrFail({
where: { communityUuid: recipientCommunityIdentifier },
})
logger.debug('recipient commuity: ', recipCom)
let pendingResult: SendCoinsResult
let committingResult: SendCoinsResult
const creationDate = new Date()
try {
pendingResult = await processXComPendingSendCoins(
recipCom,
homeCom,
creationDate,
amount,
memo,
senderUser,
recipientIdentifier,
)
logger.debug('processXComPendingSendCoins result: ', pendingResult)
if (pendingResult.vote && pendingResult.recipGradidoID) {
logger.debug('vor processXComCommittingSendCoins... ')
committingResult = await processXComCommittingSendCoins(
recipCom,
homeCom,
creationDate,
amount,
memo,
senderUser,
pendingResult.recipGradidoID,
)
logger.debug('processXComCommittingSendCoins result: ', committingResult)
if (!committingResult.vote) {
logger.fatal('FATAL ERROR: on processXComCommittingSendCoins for', committingResult)
throw new LogError(
'FATAL ERROR: on processXComCommittingSendCoins with ',
recipientCommunityIdentifier,
recipientIdentifier,
amount.toString(),
memo,
)
}
}
} catch (err) {
throw new LogError(
'ERROR: on processXComCommittingSendCoins with ',
recipientCommunityIdentifier,
recipientIdentifier,
amount.toString(),
memo,
err,
)
}
}
return true
}
}

View File

@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { Connection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { Decimal } from 'decimal.js-light'
import { GraphQLError } from 'graphql'
@ -48,9 +49,21 @@ describe('semaphore', () => {
let bibisTransactionLinkCode = ''
let bibisOpenContributionId = -1
let bobsOpenContributionId = -1
let homeCom: DbCommunity
beforeAll(async () => {
const now = new Date()
homeCom = DbCommunity.create()
homeCom.communityUuid = 'homeCom-UUID'
homeCom.creationDate = new Date('2000-01-01')
homeCom.description = 'homeCom description'
homeCom.foreign = false
homeCom.name = 'homeCom name'
homeCom.privateKey = Buffer.from('homeCom privateKey')
homeCom.publicKey = Buffer.from('homeCom publicKey')
homeCom.url = 'homeCom url'
homeCom = await DbCommunity.save(homeCom)
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister)
@ -157,6 +170,7 @@ describe('semaphore', () => {
const bibisTransaction = mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'bob@baumeister.de',
amount: '50',
memo: 'Das ist für dich, Bob',
@ -177,6 +191,7 @@ describe('semaphore', () => {
const bobsTransaction = mutate({
mutation: sendCoins,
variables: {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'bibi@bloxberg.de',
amount: '50',
memo: 'Das ist für dich, Bibi',

View File

@ -0,0 +1,53 @@
import { Community as DbCommunity } from '@entity/Community'
export async function isHomeCommunity(communityIdentifier: string): Promise<boolean> {
const homeCommunity = await DbCommunity.findOne({
where: [
{ foreign: false, communityUuid: communityIdentifier },
{ foreign: false, name: communityIdentifier },
{ foreign: false, url: communityIdentifier },
],
})
if (homeCommunity) {
return true
} else {
return false
}
}
export async function getCommunityUrl(communityIdentifier: string): Promise<string> {
const community = await DbCommunity.findOneOrFail({
where: [
{ communityUuid: communityIdentifier },
{ name: communityIdentifier },
{ url: communityIdentifier },
],
})
return community.url
}
export async function isCommunityAuthenticated(communityIdentifier: string): Promise<boolean> {
const community = await DbCommunity.findOne({
where: [
{ communityUuid: communityIdentifier },
{ name: communityIdentifier },
{ url: communityIdentifier },
],
})
if (community?.authenticatedAt) {
return true
} else {
return false
}
}
export async function getCommunityName(communityIdentifier: string): Promise<string> {
const community = await DbCommunity.findOne({
where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }],
})
if (community?.name) {
return community.name
} else {
return ''
}
}

View File

@ -6,6 +6,7 @@ import { Decimal } from 'decimal.js-light'
import { CONFIG } from '@/config'
import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs'
import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult'
// eslint-disable-next-line camelcase
import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient'
import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory'
@ -16,28 +17,45 @@ 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,
senderCom: DbCommunity,
creationDate: Date,
amount: Decimal,
memo: string,
sender: dbUser,
recipient: dbUser,
): Promise<boolean> {
recipientIdentifier: string,
): Promise<SendCoinsResult> {
let voteResult: SendCoinsResult
try {
logger.debug(
`XCom: processXComPendingSendCoins...`,
receiverFCom,
receiverCom,
senderCom,
creationDate,
amount,
memo,
sender,
recipient,
recipientIdentifier,
)
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: recipientIdentifier, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: recipientIdentifier, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`,
)
}
// first calculate the sender balance and check if the transaction is allowed
const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate)
if (!senderBalance) {
@ -45,44 +63,56 @@ export async function processXComPendingSendCoins(
}
logger.debug(`X-Com: calculated senderBalance = `, senderBalance)
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
where: {
publicKey: receiverCom.publicKey,
apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API,
},
})
const client = SendCoinsClientFactory.getInstance(receiverFCom)
// eslint-disable-next-line camelcase
if (client instanceof V1_0_SendCoinsClient) {
const args = new SendCoinsArgs()
args.communityReceiverIdentifier = receiverCom.communityUuid
? receiverCom.communityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
args.userReceiverIdentifier = recipient.gradidoID
if (receiverCom.communityUuid) {
args.recipientCommunityUuid = receiverCom.communityUuid
}
args.recipientUserIdentifier = recipientIdentifier
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)
if (senderCom.communityUuid) {
args.senderCommunityUuid = senderCom.communityUuid
}
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) {
voteResult = await client.voteForSendCoins(args)
logger.debug(`X-Com: returned from voteForSendCoins:`, voteResult)
if (voteResult.vote) {
logger.debug(`X-Com: prepare pendingTransaction for sender...`)
// writing the pending transaction on receiver-side was successfull, so now write the sender side
try {
const pendingTx = DbPendingTransaction.create()
pendingTx.amount = amount.mul(-1)
pendingTx.balance = senderBalance ? senderBalance.balance : new Decimal(0)
pendingTx.balance = senderBalance.balance
pendingTx.balanceDate = creationDate
pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0)
pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
? receiverCom.communityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
pendingTx.linkedUserGradidoID = recipient.gradidoID
pendingTx.linkedUserName = recipientName
if (receiverCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
}
if (voteResult.recipGradidoID) {
pendingTx.linkedUserGradidoID = voteResult.recipGradidoID
}
if (voteResult.recipFirstName && voteResult.recipLastName) {
pendingTx.linkedUserName = fullName(voteResult.recipFirstName, voteResult.recipLastName)
}
pendingTx.memo = memo
pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.SEND
if (senderCom.communityUuid) pendingTx.userCommunityUuid = senderCom.communityUuid
pendingTx.userId = sender.id
pendingTx.userGradidoID = sender.gradidoID
pendingTx.userName = fullName(sender.firstName, sender.lastName)
logger.debug(`X-Com: initialized sender pendingTX=`, pendingTx)
@ -107,10 +137,131 @@ export async function processXComPendingSendCoins(
)
}
logger.debug(`voteForSendCoins()-1_0... successfull`)
} else {
logger.error(
`X-Com: break with error on writing pendingTransaction for recipient...`,
voteResult,
)
}
return voteResult
}
} catch (err) {
throw new LogError(`Error:`, err)
}
return new SendCoinsResult()
}
export async function processXComCommittingSendCoins(
receiverCom: DbCommunity,
senderCom: DbCommunity,
creationDate: Date,
amount: Decimal,
memo: string,
sender: dbUser,
recipUuid: string,
): Promise<SendCoinsResult> {
const sendCoinsResult = new SendCoinsResult()
try {
logger.debug(
`XCom: processXComCommittingSendCoins...`,
receiverCom,
senderCom,
creationDate,
amount,
memo,
sender,
recipUuid,
)
// first find pending Tx with given parameters
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: senderCom.communityUuid ?? 'homeCom-UUID',
userGradidoID: sender.gradidoID,
userName: fullName(sender.firstName, sender.lastName),
linkedUserCommunityUuid:
receiverCom.communityUuid ?? CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
linkedUserGradidoID: recipUuid,
typeId: TransactionTypeId.SEND,
state: PendingTransactionState.NEW,
balanceDate: creationDate,
memo,
})
if (pendingTx) {
logger.debug(`X-Com: find pending Tx for settlement:`, pendingTx)
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
where: {
publicKey: receiverCom.publicKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API,
},
})
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.mul(-1)
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 {
sendCoinsResult.vote = await settlePendingSenderTransaction(
senderCom,
sender,
pendingTx,
)
if (sendCoinsResult.vote) {
if (pendingTx.linkedUserName) {
sendCoinsResult.recipFirstName = pendingTx.linkedUserName.slice(
0,
pendingTx.linkedUserName.indexOf(' '),
)
sendCoinsResult.recipLastName = pendingTx.linkedUserName.slice(
pendingTx.linkedUserName.indexOf(' '),
pendingTx.linkedUserName.length,
)
}
sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID
}
} 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)
sendCoinsResult.vote = false
}
return true
return sendCoinsResult
}

View File

@ -0,0 +1,146 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getConnection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { User as DbUser } from '@entity/User'
import { Decimal } from 'decimal.js-light'
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.linkedUserCommunityUuid = pendingTx.linkedUserCommunityUuid
transactionSend.linkedUserGradidoID = pendingTx.linkedUserGradidoID
transactionSend.linkedUserName = pendingTx.linkedUserName
transactionSend.amount = pendingTx.amount
const sendBalance = await calculateSenderBalance(
senderUser.id,
pendingTx.amount,
pendingTx.balanceDate,
)
if (!sendBalance) {
throw new LogError(`Sender has not enough GDD or amount is < 0', sendBalance`)
}
transactionSend.balance = sendBalance?.balance ?? new Decimal(0)
transactionSend.balanceDate = pendingTx.balanceDate
transactionSend.decay = sendBalance.decay.decay // pendingTx.decay
transactionSend.decayStart = sendBalance.decay.start // pendingTx.decayStart
transactionSend.previous = pendingTx.previous
transactionSend.linkedTransactionId = pendingTx.linkedTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`send Transaction inserted: ${dbTransaction}`)
// and mark the pendingTx in the pending_transactions table as settled
pendingTx.state = PendingTransactionState.SETTLED
await queryRunner.manager.save(DbPendingTransaction, pendingTx)
await queryRunner.commitTransaction()
logger.info(`commit send Transaction successful...`)
/*
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
await EVENT_TRANSACTION_RECEIVE(
recipient,
sender,
transactionReceive,
transactionReceive.amount,
)
*/
// trigger to send transaction via dlt-connector
// void sendTransactionsToDltConnector()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('X-Com: send Transaction was not successful', e)
} finally {
await queryRunner.release()
releaseLock()
}
/*
void sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
email: recipient.emailContact.email,
language: recipient.language,
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
senderEmail: sender.emailContact.email,
transactionAmount: amount,
})
if (transactionLink) {
void sendTransactionLinkRedeemedEmail({
firstName: sender.firstName,
lastName: sender.lastName,
email: sender.emailContact.email,
language: sender.language,
senderFirstName: recipient.firstName,
senderLastName: recipient.lastName,
senderEmail: recipient.emailContact.email,
transactionAmount: amount,
transactionMemo: memo,
})
}
logger.info(`finished executeTransaction successfully`)
} finally {
releaseLock()
}
*/
return true
}

View File

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

View File

@ -78,9 +78,31 @@ export const sendActivationEmail = gql`
}
`
/*
export const sendCoins = gql`
mutation ($recipientIdentifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(recipientIdentifier: $recipientIdentifier, amount: $amount, memo: $memo)
mutation ($identifier: String!, $amount: Decimal!, $memo: String!, $communityIdentifier: String) {
sendCoins(
identifier: $identifier
amount: $amount
memo: $memo
communityIdentifier: $communityIdentifier
)
}
`
*/
export const sendCoins = gql`
mutation (
$recipientCommunityIdentifier: String!
$recipientIdentifier: String!
$amount: Decimal!
$memo: String!
) {
sendCoins(
recipientCommunityIdentifier: $recipientCommunityIdentifier
recipientIdentifier: $recipientIdentifier
amount: $amount
memo: $memo
)
}
`

View File

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

View File

@ -58,6 +58,8 @@ const virtualLinkTransaction = (
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
userCommunityUuid: 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)
}

View File

@ -0,0 +1,163 @@
/* eslint-disable no-use-before-define */
import { Decimal } from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Contribution } from '../Contribution'
import { DltTransaction } from '../DltTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
previous: number | null
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
name: 'transaction_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
transactionLinkId?: number | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date | null
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({
name: 'user_community_uuid',
type: 'varchar',
length: 36,
nullable: 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
}

View File

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

View File

@ -0,0 +1,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`;')
}

View File

@ -1,4 +1,5 @@
CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
CONFIG_VERSION=v3.2023-04-26
# Database
DB_HOST=localhost

View File

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

View File

@ -67,7 +67,7 @@ services:
context: ./mariadb
target: mariadb_server
environment:
- MARIADB_ALLOW_EMPTY_PASSWORD=1
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1
- MARIADB_USER=root
networks:
- internal-net

View File

@ -1,4 +1,5 @@
CONFIG_VERSION=$FEDERATION_CONFIG_VERSION
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
CONFIG_VERSION=v2.2023-08-24
LOG_LEVEL=$LOG_LEVEL
# this is set fix to false, because it is important for 'production' environments. only set to true if a graphql-playground should be in use

View File

@ -6,7 +6,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 76,
lines: 77,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -16,7 +16,9 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
"dependencies": {
"@types/uuid": "8.3.4",
"apollo-server-express": "^2.25.2",
"await-semaphore": "0.1.3",
"class-validator": "^0.13.2",
"cors": "2.8.5",
"cross-env": "^7.0.3",
@ -27,7 +29,8 @@
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1",
"reflect-metadata": "^0.1.13",
"type-graphql": "^1.1.1"
"type-graphql": "^1.1.1",
"uuid": "8.3.2"
},
"devDependencies": {
"@types/express": "4.17.12",

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
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 })
recipFirstName: string | null
@Field(() => String, { nullable: true })
recipLastName: string | null
@Field(() => String, { nullable: true })
recipAlias: string | null
}

View File

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

View File

@ -4,11 +4,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,102 @@ 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
recipFirstName
recipLastName
recipAlias
}
}`
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 +150,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 +183,34 @@ 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',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
},
},
}),
)
@ -215,83 +219,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 +271,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 +304,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 +332,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
}

View File

@ -1,78 +1,108 @@
// 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.recipFirstName = receiverUser.firstName
result.recipLastName = receiverUser.lastName
result.recipAlias = receiverUser.alias
result.recipGradidoID = receiverUser.gradidoID
logger.debug(`voteForSendCoins()-1_0... successfull`)
} catch (err) {
throw new LogError(`Error in voteForSendCoins: `, err)
@ -82,49 +112,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 +159,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 +182,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
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import CONFIG from '@/config'
import { Community as DbCommunity } from '@entity/Community'
import { Decimal } from 'decimal.js-light'
import { federationLogger as logger } from '@/server/logger'
export async function checkTradingLevel(homeCom: DbCommunity, amount: Decimal): Promise<boolean> {
const tradingLevel = CONFIG.FEDERATION_TRADING_LEVEL
if (homeCom.url !== tradingLevel.RECEIVER_COMMUNITY_URL) {
logger.warn(
`X-Com: tradingLevel allows to receive coins only 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1057,6 +1057,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/ws@^7.0.0":
version "7.4.7"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
@ -1529,6 +1534,11 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
await-semaphore@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
babel-jest@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444"
@ -5437,16 +5447,16 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@8.3.2, uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.1.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"

View File

@ -1,4 +1,4 @@
#########################################################################################################
# mariadb server
#########################################################################################################
FROM mariadb/server:10.5 as mariadb_server
FROM mariadb:10.5 as mariadb_server