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

REFACTOR: Chrysalis Port #5

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ python utils/script_name.py <1> <2> ...
The spammer is a fun tool to really test the limits of the HelloTangle API. It uses the python `requests` library to spam the backend with message requests.

Parameters:
- MESSAGE_COUNT*: An integer in the range [1, 10000].
- NUM_WORKERS*: An integer in the range [1, 1000] and less than or equal to the MESSAGE_COUNT.
- API_ENVIRONMENT: A string with the value of either "dev" or "prod".
- `MESSAGE_COUNT*` (the amount of messages spam)
- Must be in the range [1, 1000]
- `NUM_WORKERS*` (the number of concurrent workers to use)
- Must be in the range [1, 1000]
- Must be less than or equal to the `MESSAGE_COUNT`
- `API_ENVIRONMENT` (the specific API environment to use)
- Must be either "dev" or "prod"
- Default is set to "dev" (which at the moment is really "chrysalis")
17,968 changes: 2,316 additions & 15,652 deletions api/package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hellotangle-api",
"version": "1.0.0",
"version": "1.1.0",
"description": "API for sending quick messages to the IOTA Tangle.",
"author": "Matthew Maxwell",
"private": true,
Expand All @@ -22,7 +22,8 @@
"test:ci": "jest --ci --reporters='default' --reporters='test/core/utils/github-action.reporter'"
},
"dependencies": {
"@iota/core": "^1.0.0-beta.30",
"@hapi/joi": "^17.1.1",
"@iota/client": "^0.6.1",
"@nestjs/common": "^7.6.13",
"@nestjs/config": "^0.6.3",
"@nestjs/core": "^7.6.13",
Expand All @@ -34,6 +35,7 @@
"@sideway/pinpoint": "^2.0.0",
"@types/hapi__joi": "^17.1.6",
"cache-manager": "^3.4.1",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"compression": "^1.7.4",
"helmet": "^4.4.1",
Expand Down
5 changes: 2 additions & 3 deletions api/src/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import { HttpLogger } from '@api/core/http/http.logger';
}),
ConfigModule.forRoot({
validationSchema: Joi.object({
IOTA_NET: Joi.string(),
IOTA_NODE_URL: Joi.string().required(),
IOTA_WALLET_SEED: Joi.string().required(),
NETWORK: Joi.string().required(),
NODE_URL: Joi.string().required(),

DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
Expand Down
8 changes: 5 additions & 3 deletions api/src/core/entities/base.abstract.entity.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { EntityValidationFailedException } from '@api/core/exceptions/base.entity.exceptions';
import { validateOrReject } from 'class-validator';
import { BaseEntity, BeforeInsert, BeforeUpdate } from 'typeorm';

import { EntityDataIsInvalidException } from '@api/core/exceptions/base.entity.exceptions';

import { BaseInterfaceEntity } from '@api/core/entities/base.interface.entity';

export abstract class BaseAbstractEntity<T> extends BaseEntity implements BaseInterfaceEntity<T> {
/**
* Validates entity data before being inserted or updated.
* @throws {@link EntityValidationFailedException} if entity validation fails.
* @throws {@link EntityDataIsInvalidException} if entity data validation fails.
* @internal
*/
@BeforeInsert()
@BeforeUpdate()
private validate(): Promise<void> {
return validateOrReject(this)
.catch((error) => {
throw new EntityValidationFailedException();
const errorKeys = Object.keys(error[0].constraints);
throw new EntityDataIsInvalidException(error[0].constraints[errorKeys[0]]);
});
}
}
16 changes: 4 additions & 12 deletions api/src/core/exceptions/base.entity.exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ export class EntityAlreadyExistsException extends BadRequestException {
}

/**
* Entity contains invalid and/or lacks valid data (i.e. no `id` property exists).
* Entity contains invalid and/or lacks valid data (i.e. no `id` property exists,
* fields are missing).
*/
export class EntityDataIsInvalidException extends BadRequestException {
constructor() {
super('Entity contains invalid and/or lacks valid data.');
constructor(msg: string = '') {
super(`Entity contains invalid and/or lacks valid data. The ${msg}.`);
}
}

Expand All @@ -27,15 +28,6 @@ export class EntityIsImmutableException extends BadRequestException {
}
}

/**
* Entity validation failed, which means that there were fields containing invalid data.
*/
export class EntityValidationFailedException extends BadRequestException {
constructor() {
super('Entity validation failed, which means that there were fields containing invalid data.');
}
}

/**
* Entity save operation fails for a generic reason.
*/
Expand Down
25 changes: 10 additions & 15 deletions api/src/core/repositories/base.abstract.repository.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { DeleteResult, Repository } from 'typeorm';

import { PostgresErrors } from '@api/core/database/postgres.errors';
import {
EntityAlreadyExistsException,
EntityDataIsInvalidException,
UnableToCreateEntityException
} from '@api/core/exceptions/base.entity.exceptions';
import { EntityAlreadyExistsException, EntityDataIsInvalidException } from '@api/core/exceptions/base.entity.exceptions';
import { BaseInterfaceRepository } from '@api/core/repositories/base.interface.repository';
import { Id } from '@api/core/types/id.types';
import { createId } from '@api/core/utils/id.util';
Expand Down Expand Up @@ -47,18 +43,17 @@ export abstract class BaseAbstractRepository<T> implements BaseInterfaceReposito
data = this.prepare(data, []);

return this.entity.save(data)
.catch((error) => {
switch(error.code) {
default:
case PostgresErrors.UNIQUE_VIOLATION:
throw new EntityAlreadyExistsException();
.catch((error) => {
switch(error.code) {
case PostgresErrors.UNIQUE_VIOLATION:
throw new EntityAlreadyExistsException();

case PostgresErrors.NOT_NULL_VIOLATION:
throw new EntityDataIsInvalidException();
}
case PostgresErrors.NOT_NULL_VIOLATION:
throw new EntityDataIsInvalidException();
}

throw new UnableToCreateEntityException();
});
throw error;
});
}

/**
Expand Down
6 changes: 3 additions & 3 deletions api/src/core/utils/id.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createHash } from 'crypto';
import { Id } from '@api/core/types/id.types';

type Digest = 'base64' | 'hex';
const digest: Digest = 'base64';
const digest: Digest = 'hex';
type HashAlgorithm = 'sha256' | 'sha512' | 'md5' | 'RSA-SHA256';
const hashAlgorithm: HashAlgorithm = 'sha512';

Expand Down Expand Up @@ -31,7 +31,7 @@ function createStringHashId(identifier: string, length: number = 64): Id {
.update(identifier + now + generateRandomInt(1_000_000))
.digest(digest)
.toString()
.replace(/[^A-Z0-9]/g, generateRandomChar)
.replace(/[^a-z0-9]/g, generateRandomChar)
.slice(0, length);
}

Expand All @@ -41,7 +41,7 @@ function createStringHashId(identifier: string, length: number = 64): Id {
*/
function generateRandomChar(): string {
const randChars: string[] = [
String.fromCharCode(generateRandomInt(26) + 65), // A-Z
String.fromCharCode(generateRandomInt(26) + 97), // a-z
String.fromCharCode(generateRandomInt(10) + 48) // 0-9
];

Expand Down
8 changes: 5 additions & 3 deletions api/src/message/controllers/message.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Body, Controller, HttpCode, HttpStatus, Inject, Post } from '@nestjs/common';
import { Body, Controller, Get, HttpCode, HttpStatus, Inject, Post } from '@nestjs/common';

import { Routes } from '@api/core/configs/routes.config';
import { SendMessageDto } from '@api/message/dtos/send-message.dto';
import { Message } from '@api/message/entities/message.entity';
import { MESSAGE_SERVICE, MessageServiceInterface } from '@api/message/interfaces/message.service.interface';
import { IOTA_SERVICE, IotaServiceInterface } from "@api/message/interfaces/iota.service.interface";

/**
* The message controller for handling requests related to IOTA protocol communication.
Expand All @@ -12,10 +13,11 @@ import { MESSAGE_SERVICE, MessageServiceInterface } from '@api/message/interface
export class MessageController {
constructor(
@Inject(MESSAGE_SERVICE)
private readonly messageService: MessageServiceInterface
private readonly messageService: MessageServiceInterface,
@Inject(IOTA_SERVICE)
private readonly iotaService: IotaServiceInterface
) { }


/**
* Send a message to a specified address in the IOTA Tangle.
* @param messageDto The message data transfer object (DTO) holding the content and recipient_address information.
Expand Down
28 changes: 15 additions & 13 deletions api/src/message/dtos/send-message.dto.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {
IsAlphanumeric,
IsAscii,
IsDate,
IsDateString,
IsNotEmpty, IsOptional,
IsString,
IsNotEmpty,
IsOptional,
Matches,
MaxLength,
MinLength
} from 'class-validator';

import { MessageAddress, MessageContent } from '@api/message/types/message.types';

const isMainnet: boolean = process.env.NETWORK === 'mainnet';

/**
* The message transfer data object (DTO) for sending messages via the IOTA protocol.
*/
Expand All @@ -20,29 +22,29 @@ export class SendMessageDto {
}

/**
* The content of a message, which __must__ be an ASCII string of at least one character and no more than 256 (512 bytes in TS).
* The content of a message, which __must__ be an ASCII string of at least one character and no more than 512 (1 kilobyte in TS).
*/
@MinLength(1)
@MaxLength(256)
@IsString()
@IsNotEmpty()
@IsAscii()
@MinLength(1)
@MaxLength(512)
content!: MessageContent;

/**
* The receipient address of a message, which __must__ be an alphanumeric string containing exactly 90 characters (180 bytes in TS).
* The receipient address of a message, which __must__ be an alphanumeric string containing exactly 64 characters (128 bytes in TS)
* and prefixed with "atoi".
*/
@MinLength(90)
@MaxLength(90)
@IsString()
@IsAlphanumeric()
@IsNotEmpty()
@IsAlphanumeric()
@MinLength(64)
@MaxLength(64)
@Matches(isMainnet ? /^iota[a-z0-9]{60}$/ : /^atoi[a-z0-9]{60}$/)
recipient_address!: MessageAddress;

/**
* The timestamp that a message was initiated at, ideally set by the client.
*/
@IsDateString()
@IsOptional()
@IsDateString()
initiated_at?: Date;
}
64 changes: 36 additions & 28 deletions api/src/message/entities/message.entity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { IsAlphanumeric, IsDate, IsDefined, IsString, MaxLength, MinLength } from 'class-validator';
import {
IsAlphanumeric, IsAscii,
IsDate, IsHexadecimal,
IsNotEmpty,
Matches,
MaxLength,
MinLength
} from 'class-validator';
import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';

import { BaseAbstractEntity } from '@api/core/entities/base.abstract.entity';
import { MessageHash, MessageContent, MessageAddress } from '@api/message/types/message.types';
import { MessageEntityInterface } from '@api/message/interfaces/message.entity.interface';
import { Id } from '@api/core/types/id.types';

const isMainnet: boolean = process.env.NETWORK === 'mainnet';

/**
* The message entity class containing all relevant properties for IOTA protocol messages.
*/
Expand All @@ -18,61 +27,60 @@ export class Message extends BaseAbstractEntity<Message> implements MessageEntit
}

/**
* The ID of a message, which __must__ be a unique alphanumeric string of exactly 64 characters and __must__ exist to be used in code and persisted in the database.
* The ID of a message, which __must__ be a unique hexadecimal string of exactly 64 characters and __must__ exist to be used in code and persisted in the database.
* @property type VARCHAR
* @property length 64
* @property unique true
* @property nullable false
*/
@IsString()
@IsAlphanumeric()
@IsNotEmpty()
@IsHexadecimal()
@MinLength(64)
@MaxLength(64)
@IsDefined()
@PrimaryColumn({ type: 'varchar', length: 64, unique: true, nullable: false })
public id: Id = '';

/**
* The content of a message, which __must__ be an ASCII string of at least one character and no more than 256 characters and __must__ exist to be used in code and persisted in the database.
* @property type VARCHAR
* @property length 256
* @property type TEXT
* @property unique false
* @property nullable false
*/
@IsString()
@MaxLength(256)
@IsDefined()
@Column({ type: 'varchar', length: 256, unique: false, nullable: false })
@IsNotEmpty()
@IsAscii()
@MinLength(1)
@MaxLength(512)
@Column({ type: 'text', unique: false, nullable: false })
public content: MessageContent = '';

/**
* The receipient address of a message, which __must__ be an alphanumeric string of exactly 90 characters and __must__ exist to be used in code and persisted in the database.
* The receipient address of a message, which __must__ be an alphanumeric string of exactly 64 characters, prefixed with
* "atoi", and __must__ exist to be used in code and persisted in the database.
* @property type VARCHAR
* @property length 90
* @property length 64
* @property unique false
* @property nullable false
*/
@IsString()
@IsNotEmpty()
@IsAlphanumeric()
@MinLength(90)
@MaxLength(90)
@IsDefined()
@Column({ type: 'varchar', length: 90, unique: false, nullable: false })
@MinLength(64)
@MaxLength(64)
@Matches(isMainnet ? /^iota[a-z0-9]{60}$/ : /^atoi[a-z0-9]{60}$/)
@Column({ type: 'varchar', length: 64, unique: false, nullable: false })
public recipient_address: MessageAddress = '';

/**
* The transaction hash of a message, which __must__ be a unique alphanumeric string of exactly 81 characters and __must__ exist to be persisted in the database.
* The transaction hash of a message, which __must__ be a unique hexadecimal string of exactly 64 characters and __must__ exist to be persisted in the database.
* @property type VARCHAR
* @property length 90
* @property length 64
* @property unique true
* @property nullable false
*/
@IsString()
@IsAlphanumeric()
@MinLength(81)
@MaxLength(81)
@IsDefined()
@Column({ type: 'varchar', length: 81, unique: true, nullable: false })
@IsNotEmpty()
@IsHexadecimal()
@MinLength(64)
@MaxLength(64)
@Column({ type: 'varchar', length: 64, unique: true, nullable: false })
public hash?: MessageHash;

/**
Expand All @@ -81,8 +89,8 @@ export class Message extends BaseAbstractEntity<Message> implements MessageEntit
* @property default now
* @property nullable false
*/
@IsNotEmpty()
@IsDate()
@IsDefined()
@CreateDateColumn({ type: 'timestamp', default: () => 'now()', nullable: false })
public initiated_at?: Date;

Expand All @@ -91,8 +99,8 @@ export class Message extends BaseAbstractEntity<Message> implements MessageEntit
* @property type TIMESTAMP
* @property nullable false
*/
@IsNotEmpty()
@IsDate()
@IsDefined()
@UpdateDateColumn({ type: 'timestamp', nullable: false })
public attached_at?: Date;
}
Loading