Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create cloud wallet #852

Merged
merged 5 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/api-gateway/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { UtilitiesModule } from './utilities/utilities.module';
import { NotificationModule } from './notification/notification.module';
import { GeoLocationModule } from './geo-location/geo-location.module';
import { CommonConstants } from '@credebl/common/common.constant';
import { CloudWalletModule } from './cloud-wallet/cloud-wallet.module';

@Module({
imports: [
Expand Down Expand Up @@ -56,7 +57,8 @@ import { CommonConstants } from '@credebl/common/common.constant';
WebhookModule,
NotificationModule,
CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }),
GeoLocationModule
GeoLocationModule,
CloudWalletModule
],
controllers: [AppController],
providers: [AppService]
Expand Down
48 changes: 48 additions & 0 deletions apps/api-gateway/src/cloud-wallet/cloud-wallet.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IResponse } from '@credebl/common/interfaces/response.interface';
import { ResponseMessages } from '@credebl/common/response-messages';
import { Controller, Post, Logger, Body, HttpStatus, Res, UseFilters } from '@nestjs/common';
import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto';
import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto';
import { CloudWalletService } from './cloud-wallet.service';
import { CreateCloudWalletDto } from './dtos/cloudWallet.dto';
import { Response } from 'express';
import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler';
import { ApiResponseDto } from '../dtos/apiResponse.dto';

@UseFilters(CustomExceptionFilter)
@Controller()
@ApiTags('cloud-wallet')
@ApiBearerAuth()
@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto })
@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto })
export class CloudWalletController {

private readonly logger = new Logger('cloud-wallet');
constructor(private readonly cloudWalletService: CloudWalletService
) { }

/**
* Create cloud wallet
* @param cloudWalletDetails
* @param res
* @returns sucess message
*/
@Post('/create-wallet')
@ApiOperation({ summary: 'Create outbound out-of-band connection invitation', description: 'Create outbound out-of-band connection invitation' })
@ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto })
async createCloudWallet(
@Res() res: Response,
@Body() cloudWalletDetails: CreateCloudWalletDto
): Promise<Response> {

const cloudWalletData = await this.cloudWalletService.createCloudWallet(cloudWalletDetails);
const finalResponse: IResponse = {
statusCode: HttpStatus.CREATED,
message: ResponseMessages.cloudWallet.success.create,
data: cloudWalletData
};
return res.status(HttpStatus.CREATED).json(finalResponse);

}
}
24 changes: 24 additions & 0 deletions apps/api-gateway/src/cloud-wallet/cloud-wallet.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getNatsOptions } from '@credebl/common/nats.config';
import { CloudWalletController } from './cloud-wallet.controller';
import { CloudWalletService } from './cloud-wallet.service';
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { CommonConstants } from '@credebl/common/common.constant';

@Module({
imports: [

ClientsModule.register([
{
name: 'NATS_CLIENT',
transport: Transport.NATS,
options: getNatsOptions(CommonConstants.CLOUD_WALLET_SERVICE, process.env.API_GATEWAY_NKEY_SEED)
}
])
],
controllers: [CloudWalletController],
providers: [CloudWalletService]
})

export class CloudWalletModule {
}
20 changes: 20 additions & 0 deletions apps/api-gateway/src/cloud-wallet/cloud-wallet.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import { ICreateCloudWallet, IStoredWalletDetails } from '@credebl/common/interfaces/cloud-wallet.interface';
import { Inject, Injectable} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { BaseService } from 'libs/service/base.service';

@Injectable()
export class CloudWalletService extends BaseService {
constructor(@Inject('NATS_CLIENT') private readonly cloudWalletServiceProxy: ClientProxy) {
super('CloudWalletServiceProxy');
}

createCloudWallet(
cloudWalletDetails: ICreateCloudWallet
): Promise<IStoredWalletDetails> {
return this.sendNatsMessage(this.cloudWalletServiceProxy, 'create-cloud-wallet', cloudWalletDetails);
}


}
20 changes: 20 additions & 0 deletions apps/api-gateway/src/cloud-wallet/dtos/cloudWallet.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';

import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotSQLInjection } from '@credebl/common/cast.helper';

export class CreateCloudWalletDto {
@ApiPropertyOptional()
@IsString({ message: 'label must be a string' })
@IsNotEmpty({ message: 'please provide valid label' })
@IsNotSQLInjection({ message: 'label is required.' })
label: string;

@ApiPropertyOptional()
@IsString({ message: 'Image URL must be a string' })
@IsOptional()
@IsNotEmpty({ message: 'please provide valid image URL' })
@IsNotSQLInjection({ message: 'Image URL is required.' })
connectionImageUrl?: string;

}
4 changes: 4 additions & 0 deletions apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export declare enum HandshakeProtocol {
Connections = "https://didcomm.org/connections/1.0",
DidExchange = "https://didcomm.org/didexchange/1.0"
}
16 changes: 16 additions & 0 deletions apps/cloud-wallet/src/cloud-wallet.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Controller } from '@nestjs/common'; // Import the common service in the library
import { CloudWalletService } from './cloud-wallet.service'; // Import the common service in connection module
import { MessagePattern } from '@nestjs/microservices'; // Import the nestjs microservices package
import { ICreateCloudWallet, IStoredWalletDetails } from '@credebl/common/interfaces/cloud-wallet.interface';

@Controller()
export class CloudWalletController {
constructor(private readonly cloudWalletService: CloudWalletService) {}


@MessagePattern({ cmd: 'create-cloud-wallet' })
async createConnectionInvitation(cloudWalletDetails: ICreateCloudWallet): Promise<IStoredWalletDetails> {
return this.cloudWalletService.createCloudWallet(cloudWalletDetails);
}
}
27 changes: 27 additions & 0 deletions apps/cloud-wallet/src/cloud-wallet.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Logger, Module } from '@nestjs/common';
import { CloudWalletController } from './cloud-wallet.controller';
import { CloudWalletService } from './cloud-wallet.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { CommonModule } from '@credebl/common';
import { CacheModule } from '@nestjs/cache-manager';
import { getNatsOptions } from '@credebl/common/nats.config';
import { PrismaService } from '@credebl/prisma-service';
import { CloudWalletRepository } from './cloud-wallet.repository';

@Module({
imports: [
ClientsModule.register([
{
name: 'NATS_CLIENT',
transport: Transport.NATS,
options: getNatsOptions(process.env.CONNECTION_NKEY_SEED)
tipusinghaw marked this conversation as resolved.
Show resolved Hide resolved
}
]),

CommonModule,
CacheModule.register()
],
controllers: [CloudWalletController],
providers: [CloudWalletService, CloudWalletRepository, PrismaService, Logger]
})
export class CloudWalletModule {}
67 changes: 67 additions & 0 deletions apps/cloud-wallet/src/cloud-wallet.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@credebl/prisma-service';
import { CloudWalletType } from '@credebl/enum/enum';
// eslint-disable-next-line camelcase
import { cloud_wallet_user_info } from '@prisma/client';
import { ICloudWalletDetails, IStoredWalletDetails } from '@credebl/common/interfaces/cloud-wallet.interface';


@Injectable()
export class CloudWalletRepository {
constructor(
private readonly prisma: PrismaService,
private readonly logger: Logger
) {}


// eslint-disable-next-line camelcase
async getCloudWalletDetails(type: CloudWalletType): Promise<cloud_wallet_user_info> {
try {
const agentDetails = await this.prisma.cloud_wallet_user_info.findFirstOrThrow({
where: {
type
}
});
return agentDetails;
} catch (error) {
this.logger.error(`Error in getCloudWalletBaseAgentDetails: ${error.message}`);
throw error;
}
}
// eslint-disable-next-line camelcase
async storeCloudWalletDetails(cloudWalletDetails: ICloudWalletDetails): Promise<IStoredWalletDetails> {
try {
const {createdBy, label, lastChangedBy, tenantId, type, userId, agentApiKey, agentEndpoint, email, key, connectionImageUrl} = cloudWalletDetails;

return await this.prisma.cloud_wallet_user_info.create({
data: {
label,
tenantId,
email,
type,
createdBy,
lastChangedBy,
userId,
agentEndpoint,
agentApiKey,
key,
connectionImageUrl
},
select: {
email: true,
connectionImageUrl: true,
createDateTime: true,
id: true,
tenantId: true,
label: true,
lastChangedDateTime: true

}
});
} catch (error) {
this.logger.error(`Error in storeCloudWalletDetails: ${error.message}`);
throw error;
}
}

}
82 changes: 82 additions & 0 deletions apps/cloud-wallet/src/cloud-wallet.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable camelcase */
import { CommonService } from '@credebl/common';
import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { ICloudWalletDetails, ICreateCloudWallet, IStoredWalletDetails } from '@credebl/common/interfaces/cloud-wallet.interface';
import { CloudWalletRepository } from './cloud-wallet.repository';
import { ResponseMessages } from '@credebl/common/response-messages';
import { CloudWalletType } from '@credebl/enum/enum';
import { CommonConstants } from '@credebl/common/common.constant';

@Injectable()
export class CloudWalletService {
constructor(
private readonly commonService: CommonService,
@Inject('NATS_CLIENT') private readonly cloudWalletServiceProxy: ClientProxy,
private readonly cloudWalletRepository: CloudWalletRepository,
private readonly logger: Logger,
@Inject(CACHE_MANAGER) private cacheService: Cache
) {}


/**
* Create clous wallet
* @param cloudWalletDetails
* @returns cloud wallet details
*/
async createCloudWallet(cloudWalletDetails: ICreateCloudWallet): Promise<IStoredWalletDetails> {
try {
// TODO - Add userId fetch logic
const {label, connectionImageUrl, email, userId} = cloudWalletDetails;
const agentPayload = {
config: {
label,
connectionImageUrl
}
};
const baseWalletDetails = await this.cloudWalletRepository.getCloudWalletDetails(CloudWalletType.BASE_WALLET);
if (!baseWalletDetails) {
throw new NotFoundException(ResponseMessages.cloudWallet.error.baseWalletNotFound);
}
const {agentEndpoint, agentApiKey} = baseWalletDetails;
const decryptedApiKey = await this.commonService.decryptPassword(agentApiKey);
const url = `${agentEndpoint}${CommonConstants.URL_SHAGENT_CREATE_TENANT}`;
const createCloudWalletResponse = await this.commonService.httpPost(url, agentPayload, { headers: { authorization: decryptedApiKey } });
if (!createCloudWalletResponse && !createCloudWalletResponse.id) {
throw new InternalServerErrorException(ResponseMessages.cloudWallet.error.createCloudWallet, {
cause: new Error(),
description: ResponseMessages.errorMessages.serverError
});
}

const walletKey = await this.commonService.dataEncryption(createCloudWalletResponse.config.walletConfig.key);

if (!walletKey) {
throw new BadRequestException(ResponseMessages.cloudWallet.error.encryptCloudWalletKey, {
cause: new Error(),
description: ResponseMessages.errorMessages.serverError
});
}
const cloudWalletResponse: ICloudWalletDetails = {
createdBy: userId,
label,
lastChangedBy: userId,
tenantId: createCloudWalletResponse.id,
type: CloudWalletType.SUB_WALLET,
userId,
agentApiKey,
agentEndpoint,
email,
key: walletKey,
connectionImageUrl
};
const storeCloudWalletDetails = await this.cloudWalletRepository.storeCloudWalletDetails(cloudWalletResponse);
return storeCloudWalletDetails;
} catch (error) {
this.logger.error(`[createCloudWallet] - error in create cloud wallet: ${error}`);
await this.commonService.handleError(error);
}
}
}
23 changes: 23 additions & 0 deletions apps/cloud-wallet/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NestFactory } from '@nestjs/core';
import { CloudWalletModule } from './cloud-wallet.module';
import { HttpExceptionFilter } from 'libs/http-exception.filter';
import { Logger } from '@nestjs/common';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { getNatsOptions } from '@credebl/common/nats.config';
import { CommonConstants } from '@credebl/common/common.constant';

const logger = new Logger();

async function bootstrap(): Promise<void> {

const app = await NestFactory.createMicroservice<MicroserviceOptions>(CloudWalletModule, {
transport: Transport.NATS,
options: getNatsOptions(CommonConstants.CONNECTION_SERVICE, process.env.CONNECTION_NKEY_SEED)
tipusinghaw marked this conversation as resolved.
Show resolved Hide resolved
});

app.useGlobalFilters(new HttpExceptionFilter());

await app.listen();
logger.log('Cloud-wallet Microservice is listening to NATS ');
}
bootstrap();
9 changes: 9 additions & 0 deletions apps/cloud-wallet/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
9 changes: 9 additions & 0 deletions apps/cloud-wallet/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/cloud-wallet"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
3 changes: 2 additions & 1 deletion libs/common/src/common.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ VERIFICATION_SERVICE = 'verification',
ECOSYSTEM_SERVICE = 'ecosystem',
WEBHOOK_SERVICE = 'webhook',
NOTIFICATION_SERVICE = 'notification',
GEO_LOCATION_SERVICE = 'geo-location'
GEO_LOCATION_SERVICE = 'geo-location',
CLOUD_WALLET_SERVICE = 'cloud-wallet'
}

export const postgresqlErrorCodes = [];
Expand Down
Loading