Merge pull request #3594 from gradido/3214-feature-x-com-sendcoins-4-activate-email-and-event-actions-in-federation-modul

feat(backend): send emails after x-com-tx
This commit is contained in:
clauspeterhuebner 2026-02-23 22:40:46 +01:00 committed by GitHub
commit b2ab427e22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1097 additions and 27 deletions

View File

@ -15,6 +15,8 @@ import {
EncryptedTransferArgs, EncryptedTransferArgs,
fullName, fullName,
interpretEncryptedTransferArgs, interpretEncryptedTransferArgs,
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
TransactionTypeId, TransactionTypeId,
} from 'core' } from 'core'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
@ -29,6 +31,7 @@ import {
User as DbUser, User as DbUser,
findModeratorCreatingContributionLink, findModeratorCreatingContributionLink,
findTransactionLinkByCode, findTransactionLinkByCode,
findUserByIdentifier,
getHomeCommunity, getHomeCommunity,
getLastTransaction, getLastTransaction,
} from 'database' } from 'database'
@ -660,6 +663,62 @@ export class TransactionLinkResolver {
if (methodLogger.isDebugEnabled()) { if (methodLogger.isDebugEnabled()) {
methodLogger.debug('Disburse JWT was sent successfully with result=', result) methodLogger.debug('Disburse JWT was sent successfully with result=', result)
} }
const senderUser = await findUserByIdentifier(senderGradidoId, senderCommunityUuid)
if (!senderUser) {
const errmsg = `Sender user not found with identifier=${senderGradidoId}`
methodLogger.error(errmsg)
throw new LogError(errmsg)
}
const recipientUser = await findUserByIdentifier(recipientGradidoId, recipientCommunityUuid)
if (!recipientUser) {
const errmsg = `Recipient user not found with identifier=${recipientGradidoId}`
methodLogger.error(errmsg)
throw new LogError(errmsg)
}
if (recipientUser.emailContact?.email !== null) {
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(
'Sending TransactionLinkRedeem Email to recipient=' +
recipientUser.firstName +
' ' +
recipientUser.lastName +
'sender=' +
senderUser.firstName +
' ' +
senderUser.lastName,
)
}
try {
await sendTransactionLinkRedeemedEmail({
firstName: recipientUser.firstName,
lastName: recipientUser.lastName,
email: recipientUser.emailContact.email,
language: recipientUser.language,
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
senderEmail: senderUser.emailContact?.email,
transactionMemo: memo,
transactionAmount: new Decimal(amount),
})
} catch (e) {
const errmsg = `Send TransactionLinkRedeem Email to recipient failed with error=${e}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
} else {
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(
'Sender or Recipient are foreign users with no email contact, not sending Transaction Received Email: recipient=' +
recipientUser.firstName +
' ' +
recipientUser.lastName +
'sender=' +
senderUser.firstName +
' ' +
senderUser.lastName,
)
}
}
} catch (e) { } catch (e) {
const errmsg = `Disburse JWT was not sent successfully with error=${e}` const errmsg = `Disburse JWT was not sent successfully with error=${e}`
methodLogger.error(errmsg) methodLogger.error(errmsg)

View File

@ -0,0 +1,55 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
import { Command } from './Command'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.BaseCommand.${method}`)
export abstract class BaseCommand<T = any> implements Command<T> {
protected abstract requiredFields: string[]
protected constructor(protected readonly params: any[]) {
// this.validateRequiredFields();
}
abstract execute(): Promise<string | boolean | null | Error>
private validateRequiredFields(): void {
const methodLogger = createLogger(`validateRequiredFields`)
if (!this.requiredFields || this.requiredFields.length === 0) {
methodLogger.debug(`validateRequiredFields() no required fields`)
return
}
methodLogger.debug(
`validateRequiredFields() requiredFields=${JSON.stringify(this.requiredFields)}`,
)
/*
const commandArgs = JSON.parse(this.params[0])
const missingFields = this.requiredFields.filter(field =>
commandArgs.{ field } === undefined || commandArgs.{ field } === null || commandArgs.{ field } === ''
);
methodLogger.debug(`validateRequiredFields() missingFields=${JSON.stringify(missingFields)}`)
if (missingFields.length > 0) {
methodLogger.error(`validateRequiredFields() missing fields: ${missingFields.join(', ')}`)
throw new Error(`Missing required fields for ${this.constructor.name}: ${missingFields.join(', ')}`);
}
*/
}
validate(): boolean {
const methodLogger = createLogger(`validate`)
methodLogger.debug(
`validate() requiredFields=${JSON.stringify(this.requiredFields)} params=${JSON.stringify(this.params)}`,
)
/*
const isValid = this.requiredFields.every(field =>
this.params[field] !== undefined &&
this.params[field] !== null &&
this.params[field] !== ''
);
methodLogger.debug(`validate() isValid=${isValid}`)
*/
return true
}
}

View File

@ -0,0 +1,4 @@
export interface Command<_T = any> {
execute(): Promise<string | boolean | null | Error>
validate?(): boolean
}

View File

@ -0,0 +1,77 @@
// core/src/command/CommandExecutor.ts
import { getLogger } from 'log4js'
import { CommandJwtPayloadType } from 'shared'
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
import { interpretEncryptedTransferArgs } from '../graphql/logic/interpretEncryptedTransferArgs'
import { CommandResult } from '../graphql/model/CommandResult'
import { EncryptedTransferArgs } from '../graphql/model/EncryptedTransferArgs'
import { Command } from './Command'
import { CommandFactory } from './CommandFactory'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.CommandExecutor.${method}`)
export class CommandExecutor {
async executeCommand<T>(command: Command<T>): Promise<CommandResult> {
const methodLogger = createLogger(`executeCommand`)
methodLogger.debug(`executeCommand() command=${command.constructor.name}`)
try {
if (command.validate && !command.validate()) {
const errmsg = `Command validation failed for command=${command.constructor.name}`
methodLogger.error(errmsg)
return { success: false, error: errmsg }
}
methodLogger.debug(`executeCommand() executing command=${command.constructor.name}`)
const result = await command.execute()
methodLogger.debug(`executeCommand() executed result=${result}`)
return { success: true, data: result }
} catch (error) {
methodLogger.error(`executeCommand() error=${error}`)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
}
}
}
async executeEncryptedCommand<_T>(encryptedArgs: EncryptedTransferArgs): Promise<CommandResult> {
const methodLogger = createLogger(`executeEncryptedCommand`)
try {
// Decrypt the command data
const commandArgs = (await interpretEncryptedTransferArgs(
encryptedArgs,
)) as CommandJwtPayloadType
if (!commandArgs) {
const errmsg = `invalid commandArgs payload of requesting community with publicKey=${encryptedArgs.publicKey}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`executeEncryptedCommand() commandArgs=${JSON.stringify(commandArgs)}`)
}
const command = CommandFactory.getInstance().createCommand(
commandArgs.commandName,
commandArgs.commandArgs,
)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`executeEncryptedCommand() command=${JSON.stringify(command)}`)
}
// Execute the command
const result = await this.executeCommand(command)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`executeCommand() result=${JSON.stringify(result)}`)
}
return result
} catch (error) {
methodLogger.error(`executeEncryptedCommand() error=${error}`)
const errorResult: CommandResult = {
success: false,
error: error instanceof Error ? error.message : 'Failed to process command',
}
return errorResult
}
}
}

View File

@ -0,0 +1,81 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
import { BaseCommand } from './BaseCommand'
import { Command } from './Command'
import { ICommandConstructor } from './CommandTypes'
// import { ICommandConstructor } from './CommandTypes';
import { SendEmailCommand } from './commands/SendEmailCommand'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.CommandFactory.${method}`)
export class CommandFactory {
private static instance: CommandFactory
private commands: Map<string, ICommandConstructor> = new Map()
private constructor() {}
static getInstance(): CommandFactory {
if (!CommandFactory.instance) {
CommandFactory.instance = new CommandFactory()
}
return CommandFactory.instance
}
registerCommand<T>(name: string, commandClass: ICommandConstructor<T>): void {
const methodLogger = createLogger(`registerCommand`)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`registerCommand() name=${name}, commandClass=${commandClass.name}`)
}
this.commands.set(name, commandClass)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`registerCommand() commands=${JSON.stringify(this.commands.entries())}`)
}
}
createCommand<T>(name: string, params: string[]): Command<T> {
const methodLogger = createLogger(`createCommand`)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`createCommand() name=${name} params=${JSON.stringify(params)}`)
}
const CommandClass = this.commands.get(name)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(
`createCommand() name=${name} commandClass=${CommandClass ? CommandClass.name : 'null'}`,
)
}
if (CommandClass === undefined) {
const errmsg = `Command ${name} not found`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
/*
try {
const command = new CommandClass(params) as Command<T>;
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`createCommand() command=${JSON.stringify(command)}`)
}
return command;
} catch (error) {
const errmsg = `Failed to create command ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`;
methodLogger.error(errmsg);
throw new Error(errmsg);
}
*/
let command: BaseCommand
switch (CommandClass.name) {
case 'SendEmailCommand':
command = new SendEmailCommand(params)
break
default: {
const errmsg = `Command ${name} not found`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
}
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`createCommand() created command=${JSON.stringify(command)}`)
}
return command
}
}

View File

@ -0,0 +1,5 @@
import { Command } from './Command'
export interface ICommandConstructor<T = any> {
new (params: any): Command<T>
}

View File

@ -0,0 +1,145 @@
import { findUserByUuids } from 'database'
import Decimal from 'decimal.js-light'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { sendTransactionReceivedEmail } from '../../emails/sendEmailVariants'
import { BaseCommand } from '../BaseCommand'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.commands.SendEmailCommand.${method}`)
export interface SendEmailCommandParams {
mailType: string
senderComUuid: string
senderGradidoId: string
receiverComUuid: string
receiverGradidoId: string
memo?: string
amount?: string
}
export class SendEmailCommand extends BaseCommand<
Record<string, unknown> | boolean | null | Error
> {
static readonly SEND_MAIL_COMMAND = 'SEND_MAIL_COMMAND'
protected requiredFields: string[] = [
'mailType',
'senderComUuid',
'senderGradidoId',
'receiverComUuid',
'receiverGradidoId',
]
protected sendEmailCommandParams: SendEmailCommandParams
constructor(params: any[]) {
const methodLogger = createLogger(`constructor`)
methodLogger.debug(`constructor() params=${JSON.stringify(params)}`)
super(params)
this.sendEmailCommandParams = JSON.parse(params[0]) as SendEmailCommandParams
}
validate(): boolean {
const baseValid = super.validate()
if (!baseValid) {
return false
}
// Additional validations
return true
}
async execute(): Promise<string | boolean | null | Error> {
const methodLogger = createLogger(`execute`)
methodLogger.debug(
`execute() sendEmailCommandParams=${JSON.stringify(this.sendEmailCommandParams)}`,
)
let result: string
if (!this.validate()) {
throw new Error('Invalid command parameters')
}
// find sender user
methodLogger.debug(
`find sender user: ${this.sendEmailCommandParams.senderComUuid} ${this.sendEmailCommandParams.senderGradidoId}`,
)
const senderUser = await findUserByUuids(
this.sendEmailCommandParams.senderComUuid,
this.sendEmailCommandParams.senderGradidoId,
true,
)
methodLogger.debug(`senderUser=${JSON.stringify(senderUser)}`)
if (!senderUser) {
const errmsg = `Sender user not found: ${this.sendEmailCommandParams.senderComUuid} ${this.sendEmailCommandParams.senderGradidoId}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
methodLogger.debug(
`find recipient user: ${this.sendEmailCommandParams.receiverComUuid} ${this.sendEmailCommandParams.receiverGradidoId}`,
)
const recipientUser = await findUserByUuids(
this.sendEmailCommandParams.receiverComUuid,
this.sendEmailCommandParams.receiverGradidoId,
)
methodLogger.debug(`recipientUser=${JSON.stringify(recipientUser)}`)
if (!recipientUser) {
const errmsg = `Recipient user not found: ${this.sendEmailCommandParams.receiverComUuid} ${this.sendEmailCommandParams.receiverGradidoId}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
const emailParams = {
firstName: recipientUser.firstName,
lastName: recipientUser.lastName,
email: recipientUser.emailContact.email,
language: recipientUser.language,
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
senderEmail: senderUser.emailId !== null ? senderUser.emailContact.email : null,
memo: this.sendEmailCommandParams.memo || '',
transactionAmount: new Decimal(this.sendEmailCommandParams.amount || 0).abs(),
}
methodLogger.debug(`emailParams=${JSON.stringify(emailParams)}`)
switch (this.sendEmailCommandParams.mailType) {
case 'sendTransactionReceivedEmail': {
const emailResult = await sendTransactionReceivedEmail(emailParams)
result = this.getEmailResult(emailResult)
break
}
default:
throw new Error(`Unknown mail type: ${this.sendEmailCommandParams.mailType}`)
}
try {
// Example: const result = await emailService.sendEmail(this.params);
return result
} catch (error) {
methodLogger.error('Error executing SendEmailCommand:', error)
throw error
}
}
private getEmailResult(result: Record<string, unknown> | boolean | null | Error): string {
const methodLogger = createLogger(`getEmailResult`)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`result=${JSON.stringify(result)}`)
}
let emailResult: string
if (result === null) {
emailResult = `result is null`
} else if (typeof result === 'boolean') {
emailResult = `result is ${result}`
} else if (result instanceof Error) {
emailResult = `error-message is ${result.message}`
} else if (typeof result === 'object') {
// {"accepted":["stage5@gradido.net"],"rejected":[],"ehlo":["PIPELINING","SIZE 25600000","ETRN","AUTH DIGEST-MD5 CRAM-MD5 PLAIN LOGIN","ENHANCEDSTATUSCODES","8BITMIME","DSN","CHUNKING"],"envelopeTime":23,"messageTime":135,"messageSize":37478,"response":"250 2.0.0 Ok: queued as C45C2100BD7","envelope":{"from":"stage5@gradido.net","to":["stage5@gradido.net"]},"messageId":"<d269161f-f3d2-2c96-49c0-58154366271b@gradido.net>"
const accepted = (result as Record<string, unknown>).accepted
const messageSize = (result as Record<string, unknown>).messageSize
const response = (result as Record<string, unknown>).response
const envelope = JSON.stringify((result as Record<string, unknown>).envelope)
emailResult = `accepted=${accepted}, messageSize=${messageSize}, response=${response}, envelope=${envelope}`
} else {
emailResult = `result is unknown type`
}
return emailResult
}
}

View File

@ -0,0 +1,11 @@
import { CommandFactory } from './CommandFactory'
import { SendEmailCommand } from './commands/SendEmailCommand'
// Import other commands...
export function initializeCommands(): void {
const factory = CommandFactory.getInstance()
// Register all commands
factory.registerCommand(SendEmailCommand.SEND_MAIL_COMMAND, SendEmailCommand)
// Register other commands...
}

View File

@ -175,18 +175,18 @@ export const sendTransactionReceivedEmail = (
data: EmailCommonData & { data: EmailCommonData & {
senderFirstName: string senderFirstName: string
senderLastName: string senderLastName: string
senderEmail: string senderEmail: string | null
memo: string memo: string
transactionAmount: Decimal transactionAmount: Decimal
}, },
): Promise<Record<string, unknown> | boolean | null | Error> => { ): Promise<Record<string, unknown> | boolean | null | Error> => {
return sendEmailTranslated({ return sendEmailTranslated({
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
template: 'transactionReceived', template: data.senderEmail !== null ? 'transactionReceived' : 'transactionReceivedNoSender',
locals: { locals: {
...data, ...data,
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
...getEmailCommonLocales(), ...(data.senderEmail !== null ? getEmailCommonLocales() : { locale: data.language }),
}, },
}) })
} }

View File

@ -0,0 +1,24 @@
extend ../layout.pug
block content
//
mixin mailto(email, subject)
- var formattedSubject = encodeURIComponent(subject)
a(class!=attributes.class href=`mailto:${email}?subject=${formattedSubject}`)
block
- var subject= t('emails.transactionReceived.replySubject', { senderFirstName, senderLastName, transactionAmount })
h2= t('emails.transactionReceived.title', { senderFirstName, senderLastName, transactionAmount })
.text-block
include ../includes/salutation.pug
p
= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName })
.content
h2= t('emails.general.message')
.child-left
div(class="p_content")= memo
a.button-3(href=`${communityURL}/transactions`) #{t('emails.general.toAccount')}

View File

@ -0,0 +1 @@
= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })

View File

@ -0,0 +1,45 @@
import { FederatedCommunity as DbFederatedCommunity } from 'database'
import { GraphQLClient } from 'graphql-request'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../../config/const'
import { EncryptedTransferArgs } from '../../../graphql/model/EncryptedTransferArgs'
import { ensureUrlEndsWithSlash } from '../../../util/utilities'
import { sendCommand as sendCommandQuery } from './query/sendCommand'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.CommandClient`)
export class CommandClient {
dbCom: DbFederatedCommunity
endpoint: string
client: GraphQLClient
constructor(dbCom: DbFederatedCommunity) {
this.dbCom = dbCom
this.endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion).concat('/')
this.client = new GraphQLClient(this.endpoint, {
method: 'POST',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
}
async sendCommand(args: EncryptedTransferArgs): Promise<boolean> {
logger.debug(`sendCommand at ${this.endpoint} for args:`, args)
try {
const { data } = await this.client.rawRequest<{ success: boolean }>(sendCommandQuery, {
args,
})
if (!data?.success) {
logger.warn('sendCommand without response data from endpoint', this.endpoint)
return false
}
logger.debug('sendCommand successfully started with endpoint', this.endpoint)
return true
} catch (err) {
logger.error('error on sendCommand: ', err)
return false
}
}
}

View File

@ -0,0 +1,11 @@
import { gql } from 'graphql-request'
export const sendCommand = gql`
mutation ($args: EncryptedTransferArgs!) {
sendCommand(encryptedArgs: $args) {
success
data
error
}
}
`

View File

@ -0,0 +1,3 @@
import { CommandClient as V1_0_CommandClient } from '../1_0/CommandClient'
export class CommandClient extends V1_0_CommandClient {}

View File

@ -0,0 +1,55 @@
import { ApiVersionType } from 'core'
import { FederatedCommunity as DbFederatedCommunity } from 'database'
import { CommandClient as V1_0_CommandClient } from './1_0/CommandClient'
import { CommandClient as V1_1_CommandClient } from './1_1/CommandClient'
type CommandClient = V1_0_CommandClient | V1_1_CommandClient
interface CommandClientInstance {
id: number
client: CommandClient
}
export class CommandClientFactory {
private static instanceArray: CommandClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
private constructor() {}
private static createCommandClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return new V1_0_CommandClient(dbCom)
case ApiVersionType.V1_1:
return new V1_1_CommandClient(dbCom)
default:
return null
}
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(dbCom: DbFederatedCommunity): CommandClient | null {
const instance = CommandClientFactory.instanceArray.find((instance) => instance.id === dbCom.id)
if (instance) {
return instance.client
}
const client = CommandClientFactory.createCommandClient(dbCom)
if (client) {
CommandClientFactory.instanceArray.push({
id: dbCom.id,
client,
} as CommandClientInstance)
}
return client
}
}

View File

@ -0,0 +1,14 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.logic.processCommand.${method}`)
export async function processCommand(
commandClass: string,
commandMethod: string,
commandArgs: string[],
) {
const methodLogger = createLogger(`processCommand`)
methodLogger.info('processing a command...')
}

View File

@ -16,6 +16,7 @@ import { Decimal } from 'decimal.js-light'
// import { LogError } from '@server/LogError' // import { LogError } from '@server/LogError'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { import {
CommandJwtPayloadType,
encryptAndSign, encryptAndSign,
PendingTransactionState, PendingTransactionState,
SendCoinsJwtPayloadType, SendCoinsJwtPayloadType,
@ -23,11 +24,15 @@ import {
verifyAndDecrypt, verifyAndDecrypt,
} from 'shared' } from 'shared'
import { randombytes_random } from 'sodium-native' import { randombytes_random } from 'sodium-native'
import { SendEmailCommand } from '../../command/commands/SendEmailCommand'
import { CONFIG as CONFIG_CORE } from '../../config' import { CONFIG as CONFIG_CORE } from '../../config'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { sendTransactionLinkRedeemedEmail, sendTransactionReceivedEmail } from '../../emails'
import { CommandClient as V1_0_CommandClient } from '../../federation/client/1_0/CommandClient'
import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/SendCoinsResultLogging.view' import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/SendCoinsResultLogging.view'
import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult' import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult'
import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient' import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient'
import { CommandClientFactory } from '../../federation/client/CommandClientFactory'
import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory' import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory'
import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId' import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId'
import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs' import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs'
@ -167,6 +172,32 @@ export async function processXComCompleteTransaction(
) )
} }
} }
/*
await sendTransactionReceivedEmail({
firstName: foreignUser.firstName,
lastName: foreignUser.lastName,
email: foreignUser.emailContact.email,
language: foreignUser.language,
memo,
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
senderEmail: senderUser.emailContact.email,
transactionAmount: new Decimal(amount),
})
*/
if (dbTransactionLink) {
await sendTransactionLinkRedeemedEmail({
firstName: senderUser.firstName,
lastName: senderUser.lastName,
email: senderUser.emailContact.email,
language: senderUser.language,
senderFirstName: foreignUser.firstName,
senderLastName: foreignUser.lastName,
senderEmail: 'unknown', // foreignUser.emailContact.email,
transactionAmount: new Decimal(amount),
transactionMemo: memo,
})
}
} }
} catch (err) { } catch (err) {
const errmsg = const errmsg =
@ -227,7 +258,7 @@ export async function processXComPendingSendCoins(
const receiverFCom = await DbFederatedCommunity.findOneOrFail({ const receiverFCom = await DbFederatedCommunity.findOneOrFail({
where: { where: {
publicKey: Buffer.from(receiverCom.publicKey), publicKey: receiverCom.publicKey,
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
}, },
}) })
@ -484,6 +515,37 @@ export async function processXComCommittingSendCoins(
sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID
sendCoinsResult.recipAlias = recipient.recipAlias sendCoinsResult.recipAlias = recipient.recipAlias
} }
// ** after successfull settle of the pending transaction on sender side we have to send a trigger to the recipient community to send an email to the x-com-tx recipient
const cmdClient = CommandClientFactory.getInstance(receiverFCom)
if (cmdClient instanceof V1_0_CommandClient) {
const payload = new CommandJwtPayloadType(
handshakeID,
SendEmailCommand.SEND_MAIL_COMMAND,
SendEmailCommand.name,
[
JSON.stringify({
mailType: 'sendTransactionReceivedEmail',
senderComUuid: senderCom.communityUuid,
senderGradidoId: sender.gradidoID,
receiverComUuid: receiverCom.communityUuid,
receiverGradidoId: sendCoinsResult.recipGradidoID,
memo: pendingTx.memo,
amount: pendingTx.amount,
}),
],
)
const jws = await encryptAndSign(
payload,
senderCom.privateJwtKey!,
receiverCom.publicJwtKey!,
)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = handshakeID
cmdClient.sendCommand(args)
}
} catch (err) { } catch (err) {
methodLogger.error( methodLogger.error(
`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`, `Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`,

View File

@ -1,5 +1,13 @@
import { Community as DbCommunity, User as DbUser, findForeignUserByUuids } from 'database' import {
Community as DbCommunity,
User as DbUser,
UserContact as DbUserContact,
findForeignUserByUuids,
UserContactLoggingView,
UserLoggingView,
} from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { UserContactType } from 'shared'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult' import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult'
@ -35,17 +43,38 @@ export async function storeForeignUser(
} }
foreignUser.gradidoID = committingResult.recipGradidoID foreignUser.gradidoID = committingResult.recipGradidoID
foreignUser = await DbUser.save(foreignUser) foreignUser = await DbUser.save(foreignUser)
logger.debug('new foreignUser inserted:', foreignUser)
logger.debug('new foreignUser inserted:', new UserLoggingView(foreignUser))
/*
if (committingResult.recipEmail !== null) {
let foreignUserEmail = DbUserContact.create()
foreignUserEmail.email = committingResult.recipEmail!
foreignUserEmail.emailChecked = true
foreignUserEmail.user = foreignUser
foreignUserEmail = await DbUserContact.save(foreignUserEmail)
logger.debug(
'new foreignUserEmail inserted:',
new UserContactLoggingView(foreignUserEmail),
)
foreignUser.emailContact = foreignUserEmail
foreignUser.emailId = foreignUserEmail.id
foreignUser = await DbUser.save(foreignUser)
}
*/
return foreignUser return foreignUser
} else if ( } else if (
user.firstName !== committingResult.recipFirstName || user.firstName !== committingResult.recipFirstName ||
user.lastName !== committingResult.recipLastName || user.lastName !== committingResult.recipLastName ||
user.alias !== committingResult.recipAlias user.alias !== committingResult.recipAlias /* ||
(user.emailContact === null && committingResult.recipEmail !== null) ||
(user.emailContact !== null &&
user.emailContact?.email !== null &&
user.emailContact?.email !== committingResult.recipEmail)
*/
) { ) {
logger.warn( logger.debug(
'foreignUser still exists, but with different name or alias:', 'foreignUser still exists, but with different name or alias:',
user, new UserLoggingView(user),
committingResult, committingResult,
) )
if (committingResult.recipFirstName !== null) { if (committingResult.recipFirstName !== null) {
@ -57,11 +86,39 @@ export async function storeForeignUser(
if (committingResult.recipAlias !== null) { if (committingResult.recipAlias !== null) {
user.alias = committingResult.recipAlias user.alias = committingResult.recipAlias
} }
/*
if (!user.emailContact && committingResult.recipEmail !== null) {
logger.debug(
'creating new userContact:',
new UserContactLoggingView(user.emailContact),
committingResult,
)
let foreignUserEmail = DbUserContact.create()
foreignUserEmail.type = UserContactType.USER_CONTACT_EMAIL
foreignUserEmail.email = committingResult.recipEmail!
foreignUserEmail.emailChecked = true
foreignUserEmail.user = user
foreignUserEmail.userId = user.id
foreignUserEmail = await DbUserContact.save(foreignUserEmail)
logger.debug(
'new foreignUserEmail inserted:',
new UserContactLoggingView(foreignUserEmail),
)
user.emailContact = foreignUserEmail
user.emailId = foreignUserEmail.id
} else if (user.emailContact && committingResult.recipEmail != null) {
const userContact = user.emailContact
userContact.email = committingResult.recipEmail
user.emailContact = await DbUserContact.save(userContact)
user.emailId = userContact.id
logger.debug('foreignUserEmail updated:', new UserContactLoggingView(userContact))
}
*/
await DbUser.save(user) await DbUser.save(user)
logger.debug('update recipient successful.', user) logger.debug('update recipient successful.', new UserLoggingView(user))
return user return user
} else { } else {
logger.debug('foreignUser still exists...:', user) logger.debug('foreignUser still exists...:', new UserLoggingView(user))
return user return user
} }
} catch (err) { } catch (err) {

View File

@ -0,0 +1,13 @@
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
export class CommandResult {
@Field(() => Boolean)
success: boolean
@Field(() => String, { nullable: true })
data?: any
@Field(() => String, { nullable: true })
error?: string
}

View File

@ -1,5 +1,9 @@
export * from './command/CommandExecutor'
export * from './command/CommandFactory'
export * from './command/initCommands'
export * from './config/index' export * from './config/index'
export * from './emails' export * from './emails'
export { CommandClient as V1_0_CommandClient } from './federation/client/1_0/CommandClient'
export * from './federation/client/1_0/logging/SendCoinsArgsLogging.view' export * from './federation/client/1_0/logging/SendCoinsArgsLogging.view'
export * from './federation/client/1_0/logging/SendCoinsResultLogging.view' export * from './federation/client/1_0/logging/SendCoinsResultLogging.view'
export * from './federation/client/1_0/model/SendCoinsArgs' export * from './federation/client/1_0/model/SendCoinsArgs'
@ -9,15 +13,19 @@ export * from './federation/client/1_0/query/revertSettledSendCoins'
export * from './federation/client/1_0/query/settleSendCoins' export * from './federation/client/1_0/query/settleSendCoins'
export * from './federation/client/1_0/query/voteForSendCoins' export * from './federation/client/1_0/query/voteForSendCoins'
export { SendCoinsClient as V1_0_SendCoinsClient } from './federation/client/1_0/SendCoinsClient' export { SendCoinsClient as V1_0_SendCoinsClient } from './federation/client/1_0/SendCoinsClient'
export { CommandClient as V1_1_CommandClient } from './federation/client/1_1/CommandClient'
export { SendCoinsClient as V1_1_SendCoinsClient } from './federation/client/1_1/SendCoinsClient' export { SendCoinsClient as V1_1_SendCoinsClient } from './federation/client/1_1/SendCoinsClient'
export * from './federation/client/CommandClientFactory'
export * from './federation/client/SendCoinsClientFactory' export * from './federation/client/SendCoinsClientFactory'
export * from './federation/enum/apiVersionType' export * from './federation/enum/apiVersionType'
export * from './graphql/enum/TransactionTypeId' export * from './graphql/enum/TransactionTypeId'
export * from './graphql/logging/DecayLogging.view' export * from './graphql/logging/DecayLogging.view'
export * from './graphql/logic/interpretEncryptedTransferArgs' export * from './graphql/logic/interpretEncryptedTransferArgs'
export * from './graphql/logic/processCommand'
export * from './graphql/logic/processXComSendCoins' export * from './graphql/logic/processXComSendCoins'
export * from './graphql/logic/settlePendingSenderTransaction' export * from './graphql/logic/settlePendingSenderTransaction'
export * from './graphql/logic/storeForeignUser' export * from './graphql/logic/storeForeignUser'
export * from './graphql/model/CommandResult'
export * from './graphql/model/Decay' export * from './graphql/model/Decay'
export * from './graphql/model/EncryptedTransferArgs' export * from './graphql/model/EncryptedTransferArgs'
export * from './logic' export * from './logic'

View File

@ -17,21 +17,25 @@ export class UserContactLoggingView extends AbstractLoggingView {
public toJSON(): any { public toJSON(): any {
return { return {
id: this.self.id, self: this.self
type: this.self.type, ? {
user: id: this.self.id,
this.showUser && this.self.user type: this.self.type,
? new UserLoggingView(this.self.user).toJSON() user:
: { id: this.self.userId }, this.showUser && this.self.user
email: this.self.email?.substring(0, 3) + '...', ? new UserLoggingView(this.self.user).toJSON()
emailVerificationCode: this.self.emailVerificationCode?.substring(0, 4) + '...', : { id: this.self.userId },
emailOptInTypeId: OptInType[this.self.emailOptInTypeId], email: this.self.email?.substring(0, 3) + '...',
emailResendCount: this.self.emailResendCount, emailVerificationCode: this.self.emailVerificationCode?.substring(0, 4) + '...',
emailChecked: this.self.emailChecked, emailOptInTypeId: OptInType[this.self.emailOptInTypeId],
phone: this.self.phone ? this.self.phone.substring(0, 3) + '...' : undefined, emailResendCount: this.self.emailResendCount,
createdAt: this.dateToString(this.self.createdAt), emailChecked: this.self.emailChecked,
updatedAt: this.dateToString(this.self.updatedAt), phone: this.self.phone ? this.self.phone.substring(0, 3) + '...' : undefined,
deletedAt: this.dateToString(this.self.deletedAt), createdAt: this.dateToString(this.self.createdAt),
updatedAt: this.dateToString(this.self.updatedAt),
deletedAt: this.dateToString(this.self.deletedAt),
}
: undefined,
} }
} }
} }

View File

@ -82,6 +82,17 @@ export async function findForeignUserByUuids(
}) })
} }
export async function findUserByUuids(
communityUuid: string,
gradidoID: string,
foreign: boolean = false,
): Promise<DbUser | null> {
return DbUser.findOne({
where: { foreign, communityUuid, gradidoID },
relations: ['emailContact'],
})
}
export async function findUserNamesByIds(userIds: number[]): Promise<Map<number, string>> { export async function findUserNamesByIds(userIds: number[]): Promise<Map<number, string>> {
const users = await DbUser.find({ const users = await DbUser.find({
select: { id: true, firstName: true, lastName: true, alias: true }, select: { id: true, firstName: true, lastName: true, alias: true },

View File

@ -0,0 +1,273 @@
<mxfile host="Electron" modified="2025-12-18T01:25:36.977Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.0.3 Chrome/114.0.5735.289 Electron/25.8.4 Safari/537.36" etag="ulUa-DwD6iMx1WLL9hxg" version="22.0.3" type="device">
<diagram name="Seite-1" id="pXcQQGq2mbEeDNBOBd4l">
<mxGraphModel dx="1206" dy="702" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="EPUhzRXaTLY6ULSqPNZg-4" value="recepient: backend:&lt;br&gt;TransactionLinkResolver" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="80" y="40" width="140" height="920" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-5" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="70" width="10" height="80" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-6" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;queryTransactionLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-5" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="-0.2381" y="5" relative="1" as="geometry">
<mxPoint x="-40" y="75" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-7" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="160" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-8" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;queryRedeemJwtLink&lt;/div&gt;" style="html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;edgeStyle=orthogonalEdgeStyle;curved=0;rounded=0;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-7" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry relative="1" as="geometry">
<mxPoint x="70" y="140" as="sourcePoint" />
<Array as="points">
<mxPoint x="100" y="170" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-11" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="230" width="10" height="80" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-12" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;redeemTransactionLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-11" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="-0.4286" y="5" relative="1" as="geometry">
<mxPoint x="-40" y="235" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-13" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="393" width="10" height="367" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-14" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;&lt;div style=&quot;line-height: 19px;&quot;&gt;disburseTransactionLink&lt;/div&gt;&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="EPUhzRXaTLY6ULSqPNZg-4" target="EPUhzRXaTLY6ULSqPNZg-13">
<mxGeometry x="-0.4286" y="5" relative="1" as="geometry">
<mxPoint x="-40" y="398" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-9" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;TransactionLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="140" y="170" as="sourcePoint" />
<mxPoint x="40" y="170" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-10" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;RedeemJwtLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="140" y="230" as="sourcePoint" />
<mxPoint x="40" y="230" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-15" value="backend:&lt;br&gt;:TransactionReslover" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="240" y="40" width="120" height="920" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-16" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-15">
<mxGeometry x="55" y="290" width="10" height="50" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-17" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;executeTransaction&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-16" parent="1" source="EPUhzRXaTLY6ULSqPNZg-11">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="335" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-18" value="sender: federation:&lt;br&gt;:DisbursementResolver" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="380" y="40" width="130" height="920" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-19" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-18">
<mxGeometry x="60" y="400" width="10" height="320" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-20" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processDisburseJwtOnSenderCommunity&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-19" parent="1" source="EPUhzRXaTLY6ULSqPNZg-13">
<mxGeometry relative="1" as="geometry">
<mxPoint x="430" y="445" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-22" value="sender: core:&lt;br&gt;processXComSendCoins" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="570" y="40" width="140" height="1080" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-23" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry x="65" y="410" width="10" height="640" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-26" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry x="68" y="435" width="10" height="225" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-27" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processXComPendingSendCoins&lt;/div&gt;" style="html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;edgeStyle=orthogonalEdgeStyle;curved=0;rounded=0;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-26" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry relative="1" as="geometry">
<mxPoint x="70" y="420" as="sourcePoint" />
<Array as="points">
<mxPoint x="100" y="420" />
<mxPoint x="100" y="450" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-43" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry x="68" y="710" width="10" height="270" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-44" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processXComCommittingSendCoins&lt;/div&gt;" style="html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;edgeStyle=orthogonalEdgeStyle;curved=0;rounded=0;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-43" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry relative="1" as="geometry">
<mxPoint x="73" y="690" as="sourcePoint" />
<Array as="points">
<mxPoint x="103" y="720" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-24" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processXComCompleteTransaction&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-23" parent="1" source="EPUhzRXaTLY6ULSqPNZg-19">
<mxGeometry x="-0.027" y="5" relative="1" as="geometry">
<mxPoint x="595" y="455" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-28" value="recepient: federation:&lt;br&gt;SendCoinsResolver" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="840" y="40" width="120" height="1080" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-29" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-28">
<mxGeometry x="55" y="488" width="10" height="50" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-30" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;voteForSendCoins&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-29" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="533" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-32" value="recepient: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="960" y="480" width="120" height="120" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-33" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-32">
<mxGeometry x="55" y="58" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-35" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" source="EPUhzRXaTLY6ULSqPNZg-33" parent="1" target="EPUhzRXaTLY6ULSqPNZg-29">
<mxGeometry relative="1" as="geometry">
<mxPoint x="945" y="613" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-34" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-33" parent="1" source="EPUhzRXaTLY6ULSqPNZg-29">
<mxGeometry relative="1" as="geometry">
<mxPoint x="945" y="543" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-31" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" source="EPUhzRXaTLY6ULSqPNZg-29" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="573" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-36" value="sender: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="715" y="588" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-37" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-36">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-38" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-37">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="673" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-39" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-37">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="653" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-21" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" source="EPUhzRXaTLY6ULSqPNZg-19" parent="1" target="EPUhzRXaTLY6ULSqPNZg-13">
<mxGeometry relative="1" as="geometry">
<mxPoint x="370" y="515" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-41" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-23">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="690" as="sourcePoint" />
<mxPoint x="740" y="690" as="targetPoint" />
<Array as="points">
<mxPoint x="670" y="690" />
<mxPoint x="670" y="720" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-46" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="895" y="788" width="10" height="132" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-47" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;&lt;div style=&quot;line-height: 19px;&quot;&gt;settleSendCoins&lt;/div&gt;&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-46">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="793" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-48" value="recepient: database:&lt;br&gt;Transaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="1210" y="740" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-49" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-48">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-50" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-49" target="EPUhzRXaTLY6ULSqPNZg-58">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1153" y="823" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-51" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;exitX=1;exitY=0;exitDx=0;exitDy=5;exitPerimeter=0;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-59" target="EPUhzRXaTLY6ULSqPNZg-49">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1153" y="803" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-52" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="910" as="targetPoint" />
<mxPoint x="895" y="910" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-53" value="sender: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="715" y="920" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-54" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-53">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-55" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-54" target="EPUhzRXaTLY6ULSqPNZg-43">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="995" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-56" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-54" source="EPUhzRXaTLY6ULSqPNZg-43">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="975" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-57" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="1012" as="sourcePoint" />
<mxPoint x="645" y="1042" as="targetPoint" />
<Array as="points">
<mxPoint x="670" y="1012" />
<mxPoint x="670" y="1042" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-58" value="recepient: federation:&lt;br&gt;settlePendingReceiveTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="990" y="740" width="200" height="220" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-59" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-58">
<mxGeometry x="95" y="60" width="10" height="110" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-60" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-59" target="EPUhzRXaTLY6ULSqPNZg-28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="905" y="823" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-61" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;&lt;div style=&quot;line-height: 19px;&quot;&gt;settlePendingReceiveTransaction&lt;/div&gt;&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-59">
<mxGeometry relative="1" as="geometry">
<mxPoint x="905" y="803" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-62" value="recepient: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="1300" y="810" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-63" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-62">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-64" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;exitPerimeter=0;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-63" target="EPUhzRXaTLY6ULSqPNZg-59">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1160" y="907.5" as="targetPoint" />
<mxPoint x="1335" y="907.5" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-65" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;settled&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;entryPerimeter=0;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-59" target="EPUhzRXaTLY6ULSqPNZg-63">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1165" y="887.5" as="sourcePoint" />
<mxPoint x="1335" y="887.5" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -0,0 +1,17 @@
import { CommandExecutor, CommandResult, EncryptedTransferArgs } from 'core'
import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'
@Resolver()
export class CommandResolver {
private commandExecutor = new CommandExecutor()
@Mutation(() => CommandResult)
async sendCommand(
@Arg('encryptedArgs', () => EncryptedTransferArgs) encryptedArgs: any,
@Ctx() context: any,
): Promise<CommandResult> {
// Convert to EncryptedTransferArgs if needed
const result = await this.commandExecutor.executeEncryptedCommand(encryptedArgs)
return result as unknown as CommandResult
}
}

View File

@ -1,5 +1,6 @@
import { NonEmptyArray } from 'type-graphql' import { NonEmptyArray } from 'type-graphql'
import { AuthenticationResolver } from './resolver/AuthenticationResolver' import { AuthenticationResolver } from './resolver/AuthenticationResolver'
import { CommandResolver } from './resolver/CommandResolver'
import { DisbursementResolver } from './resolver/DisbursementResolver' import { DisbursementResolver } from './resolver/DisbursementResolver'
import { PublicCommunityInfoResolver } from './resolver/PublicCommunityInfoResolver' import { PublicCommunityInfoResolver } from './resolver/PublicCommunityInfoResolver'
import { PublicKeyResolver } from './resolver/PublicKeyResolver' import { PublicKeyResolver } from './resolver/PublicKeyResolver'
@ -8,6 +9,7 @@ import { SendCoinsResolver } from './resolver/SendCoinsResolver'
export const getApiResolvers = (): NonEmptyArray<Function> => { export const getApiResolvers = (): NonEmptyArray<Function> => {
return [ return [
AuthenticationResolver, AuthenticationResolver,
CommandResolver,
DisbursementResolver, DisbursementResolver,
PublicCommunityInfoResolver, PublicCommunityInfoResolver,
PublicKeyResolver, PublicKeyResolver,

View File

@ -1,9 +1,16 @@
import { NonEmptyArray } from 'type-graphql' import { NonEmptyArray } from 'type-graphql'
import { AuthenticationResolver } from '../1_0/resolver/AuthenticationResolver' import { AuthenticationResolver } from '../1_0/resolver/AuthenticationResolver'
import { CommandResolver } from '../1_0/resolver/CommandResolver'
import { PublicCommunityInfoResolver } from '../1_0/resolver/PublicCommunityInfoResolver' import { PublicCommunityInfoResolver } from '../1_0/resolver/PublicCommunityInfoResolver'
import { SendCoinsResolver } from '../1_0/resolver/SendCoinsResolver' import { SendCoinsResolver } from '../1_0/resolver/SendCoinsResolver'
import { PublicKeyResolver } from './resolver/PublicKeyResolver' import { PublicKeyResolver } from './resolver/PublicKeyResolver'
export const getApiResolvers = (): NonEmptyArray<Function> => { export const getApiResolvers = (): NonEmptyArray<Function> => {
return [AuthenticationResolver, PublicCommunityInfoResolver, PublicKeyResolver, SendCoinsResolver] return [
AuthenticationResolver,
CommandResolver,
PublicCommunityInfoResolver,
PublicKeyResolver,
SendCoinsResolver,
]
} }

View File

@ -1,6 +1,7 @@
import 'source-map-support/register' import 'source-map-support/register'
import { defaultCategory, initLogger } from 'config-schema' import { defaultCategory, initLogger } from 'config-schema'
import { initializeCommands } from 'core'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared' import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared'
// config // config
@ -44,6 +45,8 @@ async function main() {
} }
}) })
}) })
initializeCommands()
} }
main().catch((e) => { main().catch((e) => {

View File

@ -4,6 +4,7 @@ export * from './helper'
export * from './jwt/JWT' export * from './jwt/JWT'
export * from './jwt/payloadtypes/AuthenticationJwtPayloadType' export * from './jwt/payloadtypes/AuthenticationJwtPayloadType'
export * from './jwt/payloadtypes/AuthenticationResponseJwtPayloadType' export * from './jwt/payloadtypes/AuthenticationResponseJwtPayloadType'
export * from './jwt/payloadtypes/CommandJwtPayloadType'
export * from './jwt/payloadtypes/DisburseJwtPayloadType' export * from './jwt/payloadtypes/DisburseJwtPayloadType'
export * from './jwt/payloadtypes/EncryptedJWEJwtPayloadType' export * from './jwt/payloadtypes/EncryptedJWEJwtPayloadType'
export * from './jwt/payloadtypes/JwtPayloadType' export * from './jwt/payloadtypes/JwtPayloadType'

View File

@ -0,0 +1,22 @@
import { JwtPayloadType } from './JwtPayloadType'
export class CommandJwtPayloadType extends JwtPayloadType {
static COMMAND_TYPE = 'command'
commandName: string
commandClass: string
commandArgs: string[]
constructor(
handshakeID: string,
commandName: string,
commandClass: string,
commandArgs: string[],
) {
super(handshakeID)
this.tokentype = CommandJwtPayloadType.COMMAND_TYPE
this.commandName = commandName
this.commandClass = commandClass
this.commandArgs = commandArgs
}
}