From 47e94c167164d8102c18f0171f63b9944945aaf2 Mon Sep 17 00:00:00 2001
From: clauspeterhuebner
Date: Tue, 27 Jan 2026 02:10:57 +0100
Subject: [PATCH] sendEmail per CommandPattern
---
backend/src/graphql/schema.ts | 2 +
core/src/command/BaseCommand.ts | 15 ++++
core/src/command/Command.ts | 4 +
core/src/command/CommandExecutor.ts | 68 +++++++++++++++++
core/src/command/CommandFactory.ts | 28 +++++++
core/src/command/CommandRegistry.ts | 24 ++++++
core/src/command/commands/ExampleCommands.ts | 26 +++++++
core/src/command/commands/SendEmailCommand.ts | 75 +++++++++++++++++++
core/src/command/initCommands.ts | 11 +++
.../federation/client/1_0/CommandClient.ts | 44 +++++++++++
.../client/1_0/query/sendCommand.ts | 11 +++
.../federation/client/1_1/CommandClient.ts | 3 +
.../federation/client/CommandClientFactory.ts | 57 ++++++++++++++
core/src/graphql/logic/processCommand.ts | 10 +++
.../src/graphql/logic/processXComSendCoins.ts | 34 +++++++++
core/src/graphql/model/CommandResult.ts | 13 ++++
core/src/graphql/resolver/CommandResolver.ts | 19 +++++
core/src/index.ts | 9 +++
federation/src/graphql/api/1_0/schema.ts | 2 +
federation/src/index.ts | 4 +
shared/src/index.ts | 1 +
.../jwt/payloadtypes/CommandJwtPayloadType.ts | 17 +++++
22 files changed, 477 insertions(+)
create mode 100644 core/src/command/BaseCommand.ts
create mode 100644 core/src/command/Command.ts
create mode 100644 core/src/command/CommandExecutor.ts
create mode 100644 core/src/command/CommandFactory.ts
create mode 100644 core/src/command/CommandRegistry.ts
create mode 100644 core/src/command/commands/ExampleCommands.ts
create mode 100644 core/src/command/commands/SendEmailCommand.ts
create mode 100644 core/src/command/initCommands.ts
create mode 100644 core/src/federation/client/1_0/CommandClient.ts
create mode 100644 core/src/federation/client/1_0/query/sendCommand.ts
create mode 100644 core/src/federation/client/1_1/CommandClient.ts
create mode 100644 core/src/federation/client/CommandClientFactory.ts
create mode 100644 core/src/graphql/logic/processCommand.ts
create mode 100644 core/src/graphql/model/CommandResult.ts
create mode 100644 core/src/graphql/resolver/CommandResolver.ts
create mode 100644 shared/src/jwt/payloadtypes/CommandJwtPayloadType.ts
diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts
index bebc3dbda..77e0d8469 100644
--- a/backend/src/graphql/schema.ts
+++ b/backend/src/graphql/schema.ts
@@ -6,6 +6,7 @@ import { buildSchema } from 'type-graphql'
import { isAuthorized } from './directive/isAuthorized'
import { AiChatResolver } from './resolver/AiChatResolver'
import { BalanceResolver } from './resolver/BalanceResolver'
+import { CommandResolver } from 'core/src/graphql/resolver/CommandResolver'
import { CommunityResolver } from './resolver/CommunityResolver'
import { ContributionLinkResolver } from './resolver/ContributionLinkResolver'
import { ContributionMessageResolver } from './resolver/ContributionMessageResolver'
@@ -25,6 +26,7 @@ export const schema = async (): Promise => {
resolvers: [
AiChatResolver,
BalanceResolver,
+ CommandResolver,
CommunityResolver,
ContributionLinkResolver,
ContributionMessageResolver,
diff --git a/core/src/command/BaseCommand.ts b/core/src/command/BaseCommand.ts
new file mode 100644
index 000000000..a2c75d902
--- /dev/null
+++ b/core/src/command/BaseCommand.ts
@@ -0,0 +1,15 @@
+import { Command } from './Command';
+
+export abstract class BaseCommand implements Command {
+ protected constructor(protected readonly params: any = {}) {}
+
+ abstract execute(): Promise;
+
+ validate(): boolean {
+ return true; // Default implementation, override in subclasses
+ }
+
+ protected validateParams(requiredParams: string[]): boolean {
+ return requiredParams.every(param => this.params[param] !== undefined);
+ }
+}
diff --git a/core/src/command/Command.ts b/core/src/command/Command.ts
new file mode 100644
index 000000000..c95e45af9
--- /dev/null
+++ b/core/src/command/Command.ts
@@ -0,0 +1,4 @@
+export interface Command {
+ execute(): Promise;
+ validate?(): boolean;
+}
diff --git a/core/src/command/CommandExecutor.ts b/core/src/command/CommandExecutor.ts
new file mode 100644
index 000000000..ec7209d20
--- /dev/null
+++ b/core/src/command/CommandExecutor.ts
@@ -0,0 +1,68 @@
+// core/src/command/CommandExecutor.ts
+import { CommandJwtPayloadType } from 'shared';
+import { interpretEncryptedTransferArgs } from '../graphql/logic/interpretEncryptedTransferArgs';
+import { EncryptedTransferArgs } from '../graphql/model/EncryptedTransferArgs';
+import { BaseCommand } from './BaseCommand';
+import { Command } from './Command';
+import { getLogger } from 'log4js';
+import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const';
+import { CommandFactory } from './CommandFactory';
+import { CommandResult } from '../graphql/model/CommandResult';
+
+const createLogger = (method: string) =>
+ getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.CommandExecutor.${method}`)
+
+export class CommandExecutor {
+ async executeCommand(command: Command): Promise {
+ const methodLogger = createLogger(`executeCommand`)
+ try {
+ if (command.validate && !command.validate()) {
+ return { success: false, error: 'Command validation failed' };
+ }
+ methodLogger.debug(`executeCommand() executing command=${command.constructor.name}`)
+ const result = await command.execute();
+ methodLogger.debug(`executeCommand() executed result=${JSON.stringify(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(
+ encryptedArgs: EncryptedTransferArgs
+ ): Promise {
+ 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.params);
+
+ // 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;
+ }
+ }
+}
diff --git a/core/src/command/CommandFactory.ts b/core/src/command/CommandFactory.ts
new file mode 100644
index 000000000..41064ed44
--- /dev/null
+++ b/core/src/command/CommandFactory.ts
@@ -0,0 +1,28 @@
+import { Command } from './Command';
+import { BaseCommand } from './BaseCommand';
+
+export class CommandFactory {
+ private static instance: CommandFactory;
+ private commands: Map Command> = new Map();
+
+ private constructor() {}
+
+ static getInstance(): CommandFactory {
+ if (!CommandFactory.instance) {
+ CommandFactory.instance = new CommandFactory();
+ }
+ return CommandFactory.instance;
+ }
+
+ registerCommand(name: string, commandClass: new (params: any) => Command): void {
+ this.commands.set(name, commandClass);
+ }
+
+ createCommand(name: string, params: any = {}): Command {
+ const CommandClass = this.commands.get(name);
+ if (!CommandClass) {
+ throw new Error(`Command ${name} not found`);
+ }
+ return new CommandClass(params) as Command;
+ }
+}
diff --git a/core/src/command/CommandRegistry.ts b/core/src/command/CommandRegistry.ts
new file mode 100644
index 000000000..321ca370d
--- /dev/null
+++ b/core/src/command/CommandRegistry.ts
@@ -0,0 +1,24 @@
+// core/src/command/CommandRegistry.ts
+import { ICommand } from './CommandTypes';
+
+export class CommandRegistry {
+ private static instance: CommandRegistry;
+ private commands: Map ICommand> = new Map();
+
+ private constructor() {}
+
+ static getInstance(): CommandRegistry {
+ if (!CommandRegistry.instance) {
+ CommandRegistry.instance = new CommandRegistry();
+ }
+ return CommandRegistry.instance;
+ }
+
+ static registerCommand(type: string, commandClass: new (params: any) => ICommand): void {
+ this.getInstance().commands.set(type, commandClass);
+ }
+
+ static getCommandClass(type: string): (new (params: any) => ICommand) | undefined {
+ return this.getInstance().commands.get(type);
+ }
+}
diff --git a/core/src/command/commands/ExampleCommands.ts b/core/src/command/commands/ExampleCommands.ts
new file mode 100644
index 000000000..9fcc3787e
--- /dev/null
+++ b/core/src/command/commands/ExampleCommands.ts
@@ -0,0 +1,26 @@
+// core/src/command/commands/ExampleCommand.ts
+import { BaseCommand } from '../BaseCommand';
+import { CommandRegistry } from '../CommandRegistry';
+
+export interface ExampleCommandParams {
+ someData: string;
+}
+
+export class ExampleCommand extends BaseCommand<{ processed: boolean }> {
+ constructor(params: ExampleCommandParams) {
+ super('EXAMPLE_COMMAND', params);
+ }
+
+ validate(): boolean {
+ return !!this.params.someData;
+ }
+
+ async execute(): Promise<{ processed: boolean }> {
+ // Command implementation here
+ console.log('Executing ExampleCommand with data:', this.params.someData);
+ return { processed: true };
+ }
+}
+
+// Register the command
+CommandRegistry.registerCommand('EXAMPLE_COMMAND', ExampleCommand);
diff --git a/core/src/command/commands/SendEmailCommand.ts b/core/src/command/commands/SendEmailCommand.ts
new file mode 100644
index 000000000..f9abc51c7
--- /dev/null
+++ b/core/src/command/commands/SendEmailCommand.ts
@@ -0,0 +1,75 @@
+import { BaseCommand } from '../BaseCommand';
+import { sendTransactionReceivedEmail } from '../../emails/sendEmailVariants';
+import { findForeignUserByUuids, findUserByIdentifier } from 'database';
+import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const';
+import { getLogger } from 'log4js';
+
+const createLogger = (method: string) =>
+ getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.CommandExecutor.${method}`)
+
+export class SendEmailCommand extends BaseCommand<{ success: boolean }> {
+ static readonly SEND_MAIL_COMMAND = 'SEND_MAIL_COMMAND';
+
+ constructor(params: {
+ mailType: string,
+ senderComUuid: string,
+ senderGradidoId: string,
+ receiverComUuid: string,
+ receiverGradidoId: string,
+ memo?: string,
+ amount?: number,
+ }) {
+ super(params);
+ }
+
+ validate(): boolean {
+ return this.validateParams(['mailType', 'senderComUuid', 'senderGradidoId', 'receiverComUuid', 'receiverGradidoId']);
+ }
+
+ async execute(): Promise<{ success: boolean }> {
+ const methodLogger = createLogger(`execute`)
+ if (!this.validate()) {
+ throw new Error('Invalid command parameters');
+ }
+ // find sender user
+ const senderUser = await findForeignUserByUuids(this.params.senderComUuid, this.params.senderGradidoId);
+ if (!senderUser) {
+ const errmsg = `Sender user not found: ${this.params.senderComUuid} ${this.params.senderGradidoId}`;
+ methodLogger.error(errmsg);
+ throw new Error(errmsg);
+ }
+ const recipientUser = await findUserByIdentifier(this.params.receiverGradidoId, this.params.receiverComUuid);
+ if (!recipientUser) {
+ const errmsg = `Recipient user not found: ${this.params.receiverComUuid} ${this.params.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.emailContact?.email,
+ memo: this.params.memo || '',
+ transactionAmount: this.params.amount || 0,
+ };
+ switch(this.params.mailType) {
+ case 'sendTransactionReceivedEmail':
+ await sendTransactionReceivedEmail(emailParams);
+ break;
+ default:
+ throw new Error(`Unknown mail type: ${this.params.mailType}`);
+ }
+
+ try {
+ // Example: const result = await emailService.sendEmail(this.params);
+ return { success: true };
+ } catch (error) {
+ methodLogger.error('Error executing SendEmailCommand:', error);
+ throw error;
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/command/initCommands.ts b/core/src/command/initCommands.ts
new file mode 100644
index 000000000..631335d26
--- /dev/null
+++ b/core/src/command/initCommands.ts
@@ -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...
+ }
diff --git a/core/src/federation/client/1_0/CommandClient.ts b/core/src/federation/client/1_0/CommandClient.ts
new file mode 100644
index 000000000..448615765
--- /dev/null
+++ b/core/src/federation/client/1_0/CommandClient.ts
@@ -0,0 +1,44 @@
+import { EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core'
+import { FederatedCommunity as DbFederatedCommunity } from 'database'
+import { GraphQLClient } from 'graphql-request'
+import { getLogger } from 'log4js'
+import { LOG4JS_BASE_CATEGORY_NAME } from '../../../config/const'
+import { sendCommand } 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 {
+ logger.debug(`sendCommand at ${this.endpoint} for args:`, args)
+ try {
+ const { data } = await this.client.rawRequest<{ success: boolean }>(sendCommand, {
+ 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
+ }
+ }
+}
diff --git a/core/src/federation/client/1_0/query/sendCommand.ts b/core/src/federation/client/1_0/query/sendCommand.ts
new file mode 100644
index 000000000..906833c82
--- /dev/null
+++ b/core/src/federation/client/1_0/query/sendCommand.ts
@@ -0,0 +1,11 @@
+import { gql } from 'graphql-request'
+
+export const sendCommand = gql`
+ mutation ($args: EncryptedTransferArgs!) {
+ sendCommand(encryptedArgs: $args) {
+ success
+ data
+ error
+ }
+ }
+`
diff --git a/core/src/federation/client/1_1/CommandClient.ts b/core/src/federation/client/1_1/CommandClient.ts
new file mode 100644
index 000000000..2d0b35152
--- /dev/null
+++ b/core/src/federation/client/1_1/CommandClient.ts
@@ -0,0 +1,3 @@
+import { CommandClient as V1_0_CommandClient } from 'core/src/federation/client/1_0/CommandClient'
+
+export class CommandClient extends V1_0_CommandClient {}
diff --git a/core/src/federation/client/CommandClientFactory.ts b/core/src/federation/client/CommandClientFactory.ts
new file mode 100644
index 000000000..a03fb493a
--- /dev/null
+++ b/core/src/federation/client/CommandClientFactory.ts
@@ -0,0 +1,57 @@
+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
+ }
+}
diff --git a/core/src/graphql/logic/processCommand.ts b/core/src/graphql/logic/processCommand.ts
new file mode 100644
index 000000000..355f71892
--- /dev/null
+++ b/core/src/graphql/logic/processCommand.ts
@@ -0,0 +1,10 @@
+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...')
+}
diff --git a/core/src/graphql/logic/processXComSendCoins.ts b/core/src/graphql/logic/processXComSendCoins.ts
index 7b2ca3dc6..345c6e406 100644
--- a/core/src/graphql/logic/processXComSendCoins.ts
+++ b/core/src/graphql/logic/processXComSendCoins.ts
@@ -16,6 +16,7 @@ import { Decimal } from 'decimal.js-light'
// import { LogError } from '@server/LogError'
import { getLogger } from 'log4js'
import {
+ CommandJwtPayloadType,
encryptAndSign,
PendingTransactionState,
SendCoinsJwtPayloadType,
@@ -30,6 +31,7 @@ import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/
import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult'
import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient'
import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory'
+import { CommandClientFactory } from '../../federation/client/CommandClientFactory'
import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId'
import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs'
import { calculateSenderBalance } from '../../util/calculateSenderBalance'
@@ -37,6 +39,8 @@ import { fullName } from '../../util/utilities'
import { settlePendingSenderTransaction } from './settlePendingSenderTransaction'
import { storeForeignUser } from './storeForeignUser'
import { storeLinkAsRedeemed } from './storeLinkAsRedeemed'
+import { V1_0_CommandClient } from '../..'
+import { SendEmailCommand } from '../../command/commands/SendEmailCommand'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.processXComSendCoins.${method}`)
@@ -511,6 +515,36 @@ export async function processXComCommittingSendCoins(
sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID
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) {
methodLogger.error(
`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`,
diff --git a/core/src/graphql/model/CommandResult.ts b/core/src/graphql/model/CommandResult.ts
new file mode 100644
index 000000000..38b7e2dd4
--- /dev/null
+++ b/core/src/graphql/model/CommandResult.ts
@@ -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;
+}
diff --git a/core/src/graphql/resolver/CommandResolver.ts b/core/src/graphql/resolver/CommandResolver.ts
new file mode 100644
index 000000000..eeedf9c34
--- /dev/null
+++ b/core/src/graphql/resolver/CommandResolver.ts
@@ -0,0 +1,19 @@
+// backend/src/graphql/resolver/CommandResolver.ts
+import { Resolver, Mutation, Arg, Ctx } from 'type-graphql';
+import { CommandExecutor } from '../../command/CommandExecutor';
+import { CommandResult } from '../model/CommandResult';
+
+@Resolver()
+export class CommandResolver {
+ private commandExecutor = new CommandExecutor();
+
+ @Mutation(() => CommandResult)
+ async executeCommand(
+ @Arg('encryptedArgs', () => Object) encryptedArgs: any,
+ @Ctx() context: any
+ ): Promise {
+ // Convert to EncryptedTransferArgs if needed
+ const result = await this.commandExecutor.executeEncryptedCommand(encryptedArgs);
+ return result as unknown as CommandResult;
+ }
+}
diff --git a/core/src/index.ts b/core/src/index.ts
index ddc204f5b..1c7817fb4 100644
--- a/core/src/index.ts
+++ b/core/src/index.ts
@@ -1,4 +1,7 @@
export * from './config/index'
+export * from './command/CommandExecutor'
+export * from './command/CommandFactory'
+export * from './command/initCommands'
export * from './emails'
export * from './federation/client/1_0/logging/SendCoinsArgsLogging.view'
export * from './federation/client/1_0/logging/SendCoinsResultLogging.view'
@@ -8,18 +11,24 @@ export * from './federation/client/1_0/query/revertSendCoins'
export * from './federation/client/1_0/query/revertSettledSendCoins'
export * from './federation/client/1_0/query/settleSendCoins'
export * from './federation/client/1_0/query/voteForSendCoins'
+export { CommandClient as V1_0_CommandClient } from './federation/client/1_0/CommandClient'
+export { CommandClient as V1_1_CommandClient } from './federation/client/1_1/CommandClient'
export { SendCoinsClient as V1_0_SendCoinsClient } from './federation/client/1_0/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/enum/apiVersionType'
export * from './graphql/enum/TransactionTypeId'
export * from './graphql/logging/DecayLogging.view'
export * from './graphql/logic/interpretEncryptedTransferArgs'
+export * from './graphql/logic/processCommand'
export * from './graphql/logic/processXComSendCoins'
export * from './graphql/logic/settlePendingSenderTransaction'
export * from './graphql/logic/storeForeignUser'
+export * from './graphql/model/CommandResult'
export * from './graphql/model/Decay'
export * from './graphql/model/EncryptedTransferArgs'
+export * from './graphql/resolver/CommandResolver'
export * from './logic'
export * from './util/calculateSenderBalance'
export * from './util/utilities'
diff --git a/federation/src/graphql/api/1_0/schema.ts b/federation/src/graphql/api/1_0/schema.ts
index 90ed13cb1..9c4741549 100644
--- a/federation/src/graphql/api/1_0/schema.ts
+++ b/federation/src/graphql/api/1_0/schema.ts
@@ -1,5 +1,6 @@
import { NonEmptyArray } from 'type-graphql'
import { AuthenticationResolver } from './resolver/AuthenticationResolver'
+import { CommandResolver } from 'core'
import { DisbursementResolver } from './resolver/DisbursementResolver'
import { PublicCommunityInfoResolver } from './resolver/PublicCommunityInfoResolver'
import { PublicKeyResolver } from './resolver/PublicKeyResolver'
@@ -8,6 +9,7 @@ import { SendCoinsResolver } from './resolver/SendCoinsResolver'
export const getApiResolvers = (): NonEmptyArray => {
return [
AuthenticationResolver,
+ CommandResolver,
DisbursementResolver,
PublicCommunityInfoResolver,
PublicKeyResolver,
diff --git a/federation/src/index.ts b/federation/src/index.ts
index 421934c58..5dea4703f 100644
--- a/federation/src/index.ts
+++ b/federation/src/index.ts
@@ -7,6 +7,7 @@ import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared'
import { CONFIG } from './config'
import { LOG4JS_BASE_CATEGORY_NAME } from './config/const'
import { createServer } from './server/createServer'
+import { initializeCommands } from 'core'
async function main() {
const startTime = new Date()
@@ -44,6 +45,9 @@ async function main() {
}
})
})
+
+ initializeCommands()
+
}
main().catch((e) => {
diff --git a/shared/src/index.ts b/shared/src/index.ts
index aff9f4ea9..ba3a5ad23 100644
--- a/shared/src/index.ts
+++ b/shared/src/index.ts
@@ -4,6 +4,7 @@ export * from './helper'
export * from './jwt/JWT'
export * from './jwt/payloadtypes/AuthenticationJwtPayloadType'
export * from './jwt/payloadtypes/AuthenticationResponseJwtPayloadType'
+export * from './jwt/payloadtypes/CommandJwtPayloadType'
export * from './jwt/payloadtypes/DisburseJwtPayloadType'
export * from './jwt/payloadtypes/EncryptedJWEJwtPayloadType'
export * from './jwt/payloadtypes/JwtPayloadType'
diff --git a/shared/src/jwt/payloadtypes/CommandJwtPayloadType.ts b/shared/src/jwt/payloadtypes/CommandJwtPayloadType.ts
new file mode 100644
index 000000000..f41832466
--- /dev/null
+++ b/shared/src/jwt/payloadtypes/CommandJwtPayloadType.ts
@@ -0,0 +1,17 @@
+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
+ }
+}