Merge pull request #3200 from gradido/2947-refactor-the-existing-sendcoins-resolver-methode-to-distingue-between-local-transaction-and-x-transaction

refactor(backend): x-sendcoins-3: refactor transactionResolver to distingue between local and x-com-transaction
This commit is contained in:
clauspeterhuebner 2023-10-05 12:12:44 +02:00 committed by GitHub
commit a874368ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 498 additions and 140 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: 85,
lines: 84,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -19,7 +19,7 @@ const constants = {
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

@ -6,10 +6,10 @@ import { backendLogger as logger } from '@/server/logger'
import { SendCoinsArgs } from './model/SendCoinsArgs'
import { SendCoinsResult } from './model/SendCoinsResult'
import { revertSendCoins } from './query/revertSendCoins'
import { revertSettledSendCoins } from './query/revertSettledSendCoins'
import { settleSendCoins } from './query/settleSendCoins'
import { voteForSendCoins } from './query/voteForSendCoins'
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 {
@ -31,16 +31,16 @@ export class SendCoinsClient {
})
}
voteForSendCoins = async (args: SendCoinsArgs): Promise<SendCoinsResult> => {
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?.vote) {
logger.warn('X-Com: voteForSendCoins failed with: ', data)
logger.debug('X-Com: voteForSendCoins failed with: ', data)
return new SendCoinsResult()
}
const result = new SendCoinsResult()
@ -48,7 +48,11 @@ export class SendCoinsClient {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
result.recipGradidoID = data.voteForSendCoins.recipGradidoID
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
result.recipName = data.voteForSendCoins.recipName
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) {
@ -56,12 +60,12 @@ export class SendCoinsClient {
}
}
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) {
@ -81,12 +85,12 @@ export class SendCoinsClient {
}
}
settleSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
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(settleSendCoins, { 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?.settleSendCoins) {
@ -108,12 +112,12 @@ export class SendCoinsClient {
}
}
revertSettledSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
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(revertSettledSendCoins, { args })
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) {

View File

@ -13,5 +13,11 @@ export class SendCoinsResult {
recipGradidoID: string | null
@Field(() => String, { nullable: true })
recipName: string | null
recipFirstName: string | null
@Field(() => String, { nullable: true })
recipLastName: string | null
@Field(() => String, { nullable: true })
recipAlias: string | null
}

View File

@ -5,37 +5,9 @@ export const voteForSendCoins = gql`
voteForSendCoins(data: $args) {
vote
recipGradidoID
recipName
recipFirstName
recipLastName
recipAlias
}
}
`
/*
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
voteForSendCoins(
data: {
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
}
) {
vote
recipGradidoID
recipName
}
}
*/

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

@ -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,7 @@
/* 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'
@ -20,11 +21,13 @@ 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'
@ -35,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'
@ -66,7 +74,9 @@ export const executeTransaction = async (
],
})
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
throw new LogError('There are still pending Transactions for Sender and/or Recipient')
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`,
)
}
if (sender.id === recipient.id) {
@ -239,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({
@ -253,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[] = []
@ -323,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}`)
@ -351,19 +385,84 @@ export class TransactionResolver {
{ recipientCommunityIdentifier, recipientIdentifier, amount, memo }: TransactionSendArgs,
@Ctx() context: Context,
): Promise<boolean> {
logger.info(
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

@ -1,4 +1,3 @@
/*
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
@ -7,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'
@ -20,28 +20,42 @@ 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) {
@ -49,42 +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.recipientCommunityUuid = receiverCom.communityUuid
? receiverCom.communityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
args.recipientUserIdentifier = recipient.gradidoID
if (receiverCom.communityUuid) {
args.recipientCommunityUuid = receiverCom.communityUuid
}
args.recipientUserIdentifier = recipientIdentifier
args.creationDate = creationDate.toISOString()
args.amount = amount
args.memo = memo
args.senderCommunityUuid = senderCom.communityUuid ? senderCom.communityUuid : 'homeCom-UUID'
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 sendCoinsResult = await client.voteForSendCoins(args)
logger.debug(`X-Com: returnd from voteForSendCoins:`, sendCoinsResult)
if (sendCoinsResult) {
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 = sendCoinsResult.recipName
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)
@ -109,45 +137,49 @@ 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) {
logger.error(`Error:`, err)
throw new LogError(`Error:`, err)
}
return true
return new SendCoinsResult()
}
export async function processXComCommittingSendCoins(
receiverFCom: DbFederatedCommunity,
receiverCom: DbCommunity,
senderCom: DbCommunity,
creationDate: Date,
amount: Decimal,
memo: string,
sender: dbUser,
recipient: dbUser,
): Promise<boolean> {
recipUuid: string,
): Promise<SendCoinsResult> {
const sendCoinsResult = new SendCoinsResult()
try {
logger.debug(
`XCom: processXComCommittingSendCoins...`,
receiverFCom,
receiverCom,
senderCom,
creationDate,
amount,
memo,
sender,
recipient,
recipUuid,
)
// first find pending Tx with given parameters
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: senderCom.communityUuid ? senderCom.communityUuid : 'homeCom-UUID',
userCommunityUuid: senderCom.communityUuid ?? 'homeCom-UUID',
userGradidoID: sender.gradidoID,
userName: fullName(sender.firstName, sender.lastName),
linkedUserCommunityUuid: receiverCom.communityUuid
? receiverCom.communityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
linkedUserGradidoID: recipient.gradidoID,
linkedUserCommunityUuid:
receiverCom.communityUuid ?? CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
linkedUserGradidoID: recipUuid,
typeId: TransactionTypeId.SEND,
state: PendingTransactionState.NEW,
balanceDate: creationDate,
@ -155,6 +187,13 @@ export async function processXComCommittingSendCoins(
})
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) {
@ -166,7 +205,7 @@ export async function processXComCommittingSendCoins(
args.recipientUserIdentifier = pendingTx.linkedUserGradidoID
}
args.creationDate = pendingTx.balanceDate.toISOString()
args.amount = pendingTx.amount
args.amount = pendingTx.amount.mul(-1)
args.memo = pendingTx.memo
args.senderCommunityUuid = pendingTx.userCommunityUuid
args.senderUserUuid = pendingTx.userGradidoID
@ -179,7 +218,24 @@ export async function processXComCommittingSendCoins(
if (acknowledge) {
// settle the pending transaction on receiver-side was successfull, so now settle the sender side
try {
await settlePendingSenderTransaction(senderCom, sender, pendingTx)
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
@ -205,7 +261,7 @@ export async function processXComCommittingSendCoins(
}
} catch (err) {
logger.error(`Error:`, err)
sendCoinsResult.vote = false
}
return true
return sendCoinsResult
}
*/

View File

@ -1,12 +1,13 @@
/* 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'
@ -65,6 +66,7 @@ export async function settlePendingSenderTransaction(
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
@ -73,15 +75,13 @@ export async function settlePendingSenderTransaction(
pendingTx.amount,
pendingTx.balanceDate,
)
if (sendBalance?.balance !== pendingTx.balance) {
throw new LogError(
`X-Com: Calculation-Error on receiver balance: receiveBalance=${sendBalance?.balance}, pendingTx.balance=${pendingTx.balance}`,
)
if (!sendBalance) {
throw new LogError(`Sender has not enough GDD or amount is < 0', sendBalance`)
}
transactionSend.balance = pendingTx.balance
transactionSend.balance = sendBalance?.balance ?? new Decimal(0)
transactionSend.balanceDate = pendingTx.balanceDate
transactionSend.decay = pendingTx.decay
transactionSend.decayStart = pendingTx.decayStart
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)
@ -94,7 +94,7 @@ export async function settlePendingSenderTransaction(
await queryRunner.commitTransaction()
logger.info(`commit send Transaction successful...`)
--*
/*
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
await EVENT_TRANSACTION_RECEIVE(
@ -103,7 +103,7 @@ export async function settlePendingSenderTransaction(
transactionReceive,
transactionReceive.amount,
)
*--
*/
// trigger to send transaction via dlt-connector
// void sendTransactionsToDltConnector()
} catch (e) {
@ -113,7 +113,7 @@ export async function settlePendingSenderTransaction(
await queryRunner.release()
releaseLock()
}
--*
/*
void sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
@ -141,7 +141,6 @@ export async function settlePendingSenderTransaction(
} finally {
releaseLock()
}
*--
*/
return true
}
*/

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

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

@ -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: 75,
lines: 77,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -13,5 +13,11 @@ export class SendCoinsResult {
recipGradidoID: string | null
@Field(() => String, { nullable: true })
recipName: string | null
recipFirstName: string | null
@Field(() => String, { nullable: true })
recipLastName: string | null
@Field(() => String, { nullable: true })
recipAlias: string | null
}

View File

@ -50,7 +50,9 @@ describe('SendCoinsResolver', () => {
voteForSendCoins(data: $args) {
vote
recipGradidoID
recipName
recipFirstName
recipLastName
recipAlias
}
}`
const settleSendCoinsMutation = `
@ -204,7 +206,9 @@ describe('SendCoinsResolver', () => {
data: {
voteForSendCoins: {
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipName: 'recipUser-FirstName recipUser-LastName',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
},
},

View File

@ -99,8 +99,10 @@ export class SendCoinsResolver {
await DbPendingTransaction.insert(pendingTx)
result.vote = true
result.recipName = pendingTx.userName
result.recipGradidoID = pendingTx.userGradidoID
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)