(null);
8 | const ref = initialRef ?? internalRef;
9 |
10 | useEffect(
11 | function () {
12 | if (!handler) {
13 | return undefined;
14 | }
15 | const listener = function (e: MouseEvent | TouchEvent) {
16 | // Do nothing if clicking ref's element or descendent elements
17 | if (!ref.current || ref.current.contains(e.target as Node)) {
18 | return;
19 | }
20 | handler?.(e);
21 | };
22 | document.addEventListener('mousedown', listener);
23 | document.addEventListener('touchstart', listener);
24 | return function () {
25 | document.removeEventListener('mousedown', listener);
26 | document.removeEventListener('touchstart', listener);
27 | };
28 | },
29 | // Add ref and handler to effect dependencies
30 | // It's worth noting that because passed in handler is a new ...
31 | // ... function on every render that will cause this effect ...
32 | // ... callback/cleanup to run every render. It's not a big deal ...
33 | // ... but to optimize you can wrap handler in useCallback before ...
34 | // ... passing it into this hook.
35 | [ref, handler],
36 | );
37 |
38 | return ref;
39 | };
40 |
--------------------------------------------------------------------------------
/apps/web/src/presentation/react/hooks/usePresenter.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Presenter } from '../../common/base/Presenter';
3 |
4 | // eslint-disable-next-line no-use-before-define
5 | export function usePresenter, S>(presenter: P): S {
6 | const [state, setState] = useState(presenter.state);
7 |
8 | useEffect(() => {
9 | presenter.subscribe(setState);
10 | }, [presenter]);
11 |
12 | return state;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/src/presentation/react/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | :root {
8 | color-scheme: light dark;
9 | font-family: 'Poppins', sans-serif;
10 | font-weight: 400;
11 | line-height: 100%;
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/src/presentation/react/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import App from './App.tsx';
3 | import './index.css';
4 |
5 | ReactDOM.createRoot(document.getElementById('root')!).render();
6 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | plugins: [],
5 | theme: {
6 | extend: {},
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src", "@types"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | });
8 |
--------------------------------------------------------------------------------
/apps/web/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | coverage: {
6 | include: ['src/**/*.ts'],
7 | // instanbul excludes interfaces from coverage
8 | provider: 'istanbul',
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/dockerbuild.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | ENV=$2
5 | BASE='hemilabs/'
6 |
7 | if [ "$ENV" = 'local' ]; then
8 | BASE=''
9 | fi
10 |
11 | docker build --platform linux/amd64 -t ${BASE}cryptochords-api -f Dockerfile.api .
12 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/base/cryptochords-api-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: cryptochords-api
5 | labels:
6 | app: cryptochords-api
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: cryptochords-api
12 | template:
13 | metadata:
14 | labels:
15 | app: cryptochords-api
16 | spec:
17 | containers:
18 | - name: cryptochords-api
19 | image: hemilabs/cryptochords-api
20 | imagePullPolicy: IfNotPresent
21 | ports:
22 | - containerPort: 3000
23 | - containerPort: 3001
24 | imagePullSecrets:
25 | - name: dockerhub-secret
26 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/base/cryptochords-api-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: cryptochords-api-service
5 | spec:
6 | type: ClusterIP # Use NodePort or ClusterIP if you don't have a LoadBalancer provider.
7 | ports:
8 | - name: http-testnet
9 | port: 3000
10 | targetPort: 3000
11 | protocol: TCP
12 | - name: http
13 | port: 3001
14 | targetPort: 3001
15 | protocol: TCP
16 | selector:
17 | app: cryptochords-api
18 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/base/cryptochords-web-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: cryptochords-web
5 | labels:
6 | app: cryptochords-web
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | app: cryptochords-web
12 | template:
13 | metadata:
14 | labels:
15 | app: cryptochords-web
16 | spec:
17 | containers:
18 | - name: cryptochords-web
19 | image: hemilabs/cryptochords-web
20 | imagePullPolicy: IfNotPresent
21 | ports:
22 | - containerPort: 80
23 | imagePullSecrets:
24 | - name: dockerhub-secret
25 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/base/cryptochords-web-ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: cryptochords-web-ingress
5 | annotations:
6 | cert-manager.io/cluster-issuer: letsencrypt-apps-prod-dns01
7 | spec:
8 | ingressClassName: nginx
9 | rules:
10 | - host: cryptochords.hemi.xyz
11 | http:
12 | paths:
13 | - path: /
14 | pathType: Prefix
15 | backend:
16 | service:
17 | name: cryptochords-web-service
18 | port:
19 | number: 80
20 | tls:
21 | - hosts:
22 | - cryptochords.hemi.xyz
23 | secretName: cryptochords-tls
24 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/base/cryptochords-web-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: cryptochords-web-service
5 | spec:
6 | type: ClusterIP
7 | ports:
8 | - port: 80
9 | targetPort: 80
10 | protocol: TCP
11 | selector:
12 | app: cryptochords-web
13 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/base/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 | resources:
4 | - cryptochords-api-deployment.yaml
5 | - cryptochords-api-service.yaml
6 | - cryptochords-web-deployment.yaml
7 | - cryptochords-web-service.yaml
8 | - cryptochords-web-ingress.yaml
9 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/overlays/testnet/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 | resources:
4 | - ../../base
5 | patches:
6 | - target:
7 | group: networking.k8s.io
8 | version: v1
9 | kind: Ingress
10 | name: cryptochords-web-ingress
11 | path: testnet-patch.yaml
12 |
--------------------------------------------------------------------------------
/infrastructure/kustomize/overlays/testnet/testnet-patch.yaml:
--------------------------------------------------------------------------------
1 | - op: replace
2 | path: /spec/rules/0/host
3 | value: cryptochords.hemi.xyz
4 |
5 | - op: replace
6 | path: /spec/tls/0/hosts/0
7 | value: cryptochords.hemi.xyz
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crypto-chords-monorepo",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "turbo dev",
7 | "format:check": "prettier --check .",
8 | "format:fix": "npx prettier --write .",
9 | "lint": "turbo lint",
10 | "test": "turbo run test",
11 | "test:cov": "turbo run test --coverage",
12 | "start:api": "turbo start:api"
13 | },
14 | "devDependencies": {
15 | "prettier": "^3.1.1",
16 | "turbo": "^1.12.2"
17 | },
18 | "engines": {
19 | "node": ">=18"
20 | },
21 | "packageManager": "yarn@1.22.19",
22 | "workspaces": [
23 | "apps/*",
24 | "packages/*"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/shared/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es2021: true,
4 | node: true,
5 | },
6 | extends: 'standard-with-typescript',
7 | overrides: [],
8 | parserOptions: {
9 | ecmaVersion: 'latest',
10 | sourceType: 'module',
11 | project: ['tsconfig.json'],
12 | tsconfigRootDir: __dirname,
13 | },
14 | plugins: ['sonarjs'],
15 | rules: {
16 | '@typescript-eslint/triple-slash-reference': 'off',
17 | '@typescript-eslint/space-before-function-paren': 'off',
18 | '@typescript-eslint/strict-boolean-expressions': 'off',
19 | '@typescript-eslint/no-extraneous-class': 'off',
20 | 'semi': ['error', 'never'],
21 | 'quotes': ['error', 'single'],
22 | 'import/extensions': 'off',
23 | 'import/prefer-default-export': 'off',
24 | 'indent': ['error', 2],
25 | 'class-methods-use-this': 'off',
26 | 'complexity': ['error', 4],
27 | 'sonarjs/cognitive-complexity': ['error', 4],
28 | 'max-depth': ['error', 3],
29 | 'max-statements': ['error', 10],
30 | 'max-lines': [
31 | 'error',
32 | {
33 | max: 130,
34 | skipBlankLines: true,
35 | skipComments: true,
36 | },
37 | ],
38 | 'max-lines-per-function': [
39 | 'error',
40 | {
41 | max: 35,
42 | skipBlankLines: true,
43 | skipComments: true,
44 | },
45 | ],
46 | 'max-nested-callbacks': ['error', 3],
47 | 'max-params': ['error', 3],
48 | 'no-useless-constructor': 'off',
49 | 'max-classes-per-file': 'off',
50 | 'lines-between-class-members': [
51 | 'error',
52 | 'always',
53 | {
54 | exceptAfterSingleLine: true,
55 | },
56 | ],
57 | 'no-shadow': 'off',
58 | '@typescript-eslint/no-shadow': ['error'],
59 | 'max-len': [
60 | 'error',
61 | {
62 | code: 80,
63 | },
64 | ],
65 | 'overrides': [
66 | {
67 | files: ['*.spec.ts'],
68 | rules: {
69 | 'max-nested-callbacks': ['error', 5],
70 | },
71 | },
72 | ],
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cryptochords/shared",
3 | "version": "0.0.0",
4 | "description": "Shared package for CryptoChords Application",
5 | "main": "./dist/cjs/index.js",
6 | "types": "./dist/types/index.d.ts",
7 | "exports": {
8 | ".": {
9 | "require": {
10 | "types": "./dist/types/index.d.ts",
11 | "default": "./dist/cjs/index.js"
12 | },
13 | "import": {
14 | "types": "./dist/types/index.d.ts",
15 | "default": "./dist/esm/index.js"
16 | }
17 | },
18 | "./src/*": "./src/*"
19 | },
20 | "private": true,
21 | "scripts": {
22 | "build:cjs": "tsc -p tsconfig.build.cjs.json",
23 | "build:esm": "tsc -p tsconfig.build.esm.json",
24 | "build": "npm run build:cjs && npm run build:esm",
25 | "lint": "eslint",
26 | "lint:fix": "eslint --fix",
27 | "test": "vitest run",
28 | "test:cov": "vitest run --coverage",
29 | "test:watch": "vitest watch"
30 | },
31 | "author": "",
32 | "license": "MIT",
33 | "dependencies": {
34 | "hemi-viem": "1.7.0",
35 | "uuid": "^9.0.0"
36 | },
37 | "devDependencies": {
38 | "@babel/preset-env": "^7.19.0",
39 | "@types/node": "^18.7.15",
40 | "@types/uuid": "^8.3.4",
41 | "@typescript-eslint/eslint-plugin": "^5.0.0",
42 | "@vitest/coverage-c8": "^0.23.1",
43 | "eslint": "^8.0.1",
44 | "eslint-config-standard-with-typescript": "^22.0.0",
45 | "eslint-plugin-import": "^2.25.2",
46 | "eslint-plugin-n": "^15.0.0",
47 | "eslint-plugin-promise": "^6.0.0",
48 | "eslint-plugin-sonarjs": "^0.15.0",
49 | "ts-node": "^10.9.1",
50 | "typescript": "5.7.2",
51 | "vite": "^5.0.11",
52 | "vitest": "^0.23.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/DomainError.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { DomainError } from './DomainError';
3 |
4 | describe('src/domain/DomainError', () => {
5 | it('should be defined', () => {
6 | expect(DomainError).toBeDefined();
7 | });
8 |
9 | it('should be instance of Error', () => {
10 | expect(new DomainError('TEST_ERROR')).toBeInstanceOf(Error);
11 | });
12 |
13 | describe('constructor', () => {
14 | it('should accept a code parameter', () => {
15 | const errorCode = 'TEST_ERROR';
16 | const domainError = new DomainError(errorCode);
17 |
18 | expect(domainError.toObject().code).toBe(errorCode);
19 | });
20 |
21 | it('should accept a exposable parameter', () => {
22 | const exposable = true;
23 | const domainError = new DomainError('TEST_ERROR', exposable);
24 |
25 | expect(domainError.toObject().exposable).toBe(exposable);
26 | });
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/DomainError.ts:
--------------------------------------------------------------------------------
1 | interface DomainErrorProps {
2 | code: string;
3 | exposable: boolean;
4 | }
5 |
6 | export class DomainError extends Error {
7 | code: string;
8 | exposable: boolean;
9 |
10 | constructor(code: string, exposable: boolean = false) {
11 | super(code);
12 | this.code = code;
13 | this.exposable = exposable;
14 | }
15 |
16 | toObject(): DomainErrorProps {
17 | const { code, exposable } = this;
18 |
19 | return { code, exposable };
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/Entity.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { Entity } from './Entity';
3 | import { ValueObject } from './ValueObject';
4 | import { Uuid } from '../valueObjects/Uuid';
5 |
6 | interface TestProps {
7 | value: string;
8 | }
9 |
10 | class TestEntity extends Entity {
11 | static create(props: TestProps, id: Uuid): TestEntity {
12 | return new TestEntity(props, id);
13 | }
14 |
15 | get value(): string {
16 | return this.props.value;
17 | }
18 | }
19 |
20 | const testId = Uuid.create();
21 |
22 | describe('src/domain/Entity', () => {
23 | it('should be defined', () => {
24 | expect(Entity).toBeDefined();
25 | });
26 |
27 | it('should be instance of ValueObject', () => {
28 | const entity = TestEntity.create({ value: 'test' }, testId);
29 |
30 | expect(entity).toBeInstanceOf(ValueObject);
31 | });
32 |
33 | it('should generate a new id if none is passed to constructor', () => {
34 | const entity = TestEntity.create({ value: 'test' }, testId);
35 |
36 | expect(entity.uuid).toBeDefined();
37 | });
38 |
39 | it('should set the id if it was passed to constructor', () => {
40 | const expectedId = testId;
41 | const entity = TestEntity.create({ value: 'test' }, expectedId);
42 |
43 | expect(entity.uuid).toBe(expectedId);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/Entity.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from './ValueObject';
2 | import { Uuid } from '../valueObjects/Uuid';
3 |
4 | export class Entity extends ValueObject {
5 | uuid: Uuid;
6 |
7 | protected constructor(props: T, uuid: Uuid) {
8 | super(props);
9 | this.uuid = uuid;
10 | }
11 |
12 | get id() {
13 | return this.uuid;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/ValueObject.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { ValueObject } from './ValueObject';
3 |
4 | interface TestProps {
5 | value: string;
6 | }
7 |
8 | class TestValueObject extends ValueObject {
9 | static create(props: TestProps): TestValueObject {
10 | return new TestValueObject(props);
11 | }
12 |
13 | get value(): string {
14 | return this.props.value;
15 | }
16 | }
17 |
18 | describe('src/domain/ValueObject', () => {
19 | it('should be defined', () => {
20 | expect(ValueObject).toBeDefined();
21 | });
22 |
23 | it('should set props attribute from the constructor', () => {
24 | const expectedProps = { value: 'test' };
25 | const entity = TestValueObject.create(expectedProps);
26 |
27 | expect(entity.value).toStrictEqual(expectedProps.value);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/ValueObject.ts:
--------------------------------------------------------------------------------
1 | export class ValueObject {
2 | protected props: T;
3 |
4 | protected constructor(props: T) {
5 | this.props = props;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/event/Event.ts:
--------------------------------------------------------------------------------
1 | export class Event {
2 | timestamp: number;
3 |
4 | protected constructor(protected eventKey: symbol) {
5 | this.timestamp = Date.now();
6 | }
7 |
8 | get key(): symbol {
9 | return this.eventKey;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/event/EventBus.ts:
--------------------------------------------------------------------------------
1 | import { Event } from './Event';
2 | import { EventBusInstance } from './EventBusInstance';
3 | import { EventSubscription } from './EventSubscription';
4 |
5 | export default class EventBus {
6 | static instance = new EventBusInstance();
7 |
8 | static publish(event: Event) {
9 | this.instance.publish(event);
10 | }
11 |
12 | static subscribe(
13 | eventKey: symbol,
14 | listener: EventSubscription,
15 | ): void {
16 | this.instance.subscribe(eventKey, listener as EventSubscription);
17 | }
18 |
19 | static unsubscribe(
20 | eventKey: symbol,
21 | listener: EventSubscription,
22 | ): void {
23 | this.instance.unsubscribe(eventKey, listener);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/event/EventBusInstance.ts:
--------------------------------------------------------------------------------
1 | import { Event } from './Event';
2 | import { EventSubscription } from './EventSubscription';
3 |
4 | export class EventBusInstance {
5 | private listeners = new Map[]>();
6 |
7 | public async publish(event: Event) {
8 | const listeners = this.listeners.get(event.key);
9 | if (!listeners) {
10 | return;
11 | }
12 | listeners.forEach(listener => listener(event));
13 | }
14 |
15 | public subscribe(eventKey: symbol, listener: EventSubscription): void {
16 | let listeners = this.listeners.get(eventKey);
17 | if (!listeners) {
18 | listeners = [];
19 | this.listeners.set(eventKey, listeners);
20 | }
21 | listeners.push(listener);
22 | }
23 |
24 | public unsubscribe(
25 | eventKey: symbol,
26 | listener: EventSubscription,
27 | ): void {
28 | const listeners = this.listeners.get(eventKey);
29 | if (listeners) {
30 | const index = listeners.indexOf(listener);
31 | if (index > -1) {
32 | listeners.splice(index, 1);
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/event/EventSubscription.ts:
--------------------------------------------------------------------------------
1 | import { Event } from './Event';
2 |
3 | export type EventSubscription = (event: T) => Promise;
4 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/event/Observable.ts:
--------------------------------------------------------------------------------
1 | import { Event } from './Event';
2 | import { EventSubscription } from './EventSubscription';
3 |
4 | export interface Observable {
5 | listen(listener: EventSubscription): void;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/base/event/ObservableSet.ts:
--------------------------------------------------------------------------------
1 | import { Event } from './Event';
2 | import { EventSubscription } from './EventSubscription';
3 | import { Observable } from './Observable';
4 |
5 | export class ObservableSet implements Observable {
6 | private listeners: Observable[] = [];
7 |
8 | constructor(...listeners: Observable[]) {
9 | this.listeners = listeners;
10 | }
11 |
12 | listen(listener: EventSubscription): void {
13 | this.listeners.forEach(l => l.listen(listener));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/entities/L2Block.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { Entity } from '../base/Entity';
3 | import { TxType } from '../valueObjects/Txtype';
4 | import { TxTypesEnum } from '../enums/TxTypesEnum';
5 | import { Address } from '../valueObjects/Address';
6 | import { L2Block } from './L2Block';
7 |
8 | describe('src/domain/entities/L2Block', () => {
9 | const txType = TxType.create(TxTypesEnum.Eth);
10 | const address = Address.create('0xbhbfhudhuf');
11 |
12 | it('should be defined', () => {
13 | expect(L2Block).toBeDefined();
14 | });
15 |
16 | it('should be an instance of Entity', () => {
17 | const l2Block = L2Block.create({
18 | txType,
19 | address,
20 | });
21 |
22 | expect(l2Block).toBeInstanceOf(Entity);
23 | });
24 |
25 | describe('create', () => {
26 | const l2Block = L2Block.create({
27 | txType,
28 | address,
29 | });
30 |
31 | it('should set the txType on tagname txType', () => {
32 | expect(l2Block.txType).toBe(txType.value);
33 | });
34 |
35 | it('should set the address on address property', () => {
36 | expect(l2Block.address).toBe(address.value);
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/entities/L2Block.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from '../base/Entity';
2 | import { TxTypesEnum } from '../enums/TxTypesEnum';
3 | import { Address } from '../valueObjects/Address';
4 | import { TxType } from '../valueObjects/Txtype';
5 | import { Uuid } from '../valueObjects/Uuid';
6 |
7 | interface L2BlockProps {
8 | txType: TxType;
9 | address: Address;
10 | }
11 |
12 | interface L2BlockJSON {
13 | txType: string;
14 | address: string;
15 | }
16 |
17 | export class L2Block extends Entity {
18 | private constructor(props: L2BlockProps, id: Uuid) {
19 | super(props, id);
20 | }
21 |
22 | static create(props: L2BlockProps, id?: Uuid): L2Block {
23 | const blockId = id ?? Uuid.create();
24 | return new L2Block(props, blockId);
25 | }
26 |
27 | static fromJSON(json: L2BlockJSON): L2Block {
28 | return L2Block.create({
29 | txType: TxType.create(json.txType as TxTypesEnum),
30 | address: Address.create(json.address),
31 | });
32 | }
33 |
34 | get txType(): string {
35 | return this.props.txType.value;
36 | }
37 |
38 | get address(): string {
39 | return this.props.address.value;
40 | }
41 |
42 | toJSON(): L2BlockJSON {
43 | return {
44 | txType: this.txType,
45 | address: this.address,
46 | };
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/enums/NetworkEnum.ts:
--------------------------------------------------------------------------------
1 | export enum NetworkEnum {
2 | MAINNET = 'mainnet',
3 | TESTNET = 'testnet',
4 | }
5 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/enums/TxTypesEnum.ts:
--------------------------------------------------------------------------------
1 | export enum TxTypesEnum {
2 | Block = 'block',
3 | Eth = 'eth',
4 | Btc = 'btc',
5 | Pop = 'pop',
6 | }
7 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/errors/InvalidAddressError.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { DomainError } from '../base/DomainError';
3 | import { InvalidAddressError } from './InvalidAddressError';
4 |
5 | describe('src/domain/errors/InvalidAddressError', () => {
6 | it('should be defined', () => {
7 | expect(InvalidAddressError).toBeDefined();
8 | });
9 |
10 | it('should be an instance of DomainError', () => {
11 | const error = new InvalidAddressError();
12 |
13 | expect(error).toBeInstanceOf(DomainError);
14 | });
15 |
16 | describe('constructor', () => {
17 | it('should set error code to INVALID_ADDRESS', () => {
18 | const error = new InvalidAddressError();
19 |
20 | expect(error.code).toBe('INVALID_ADDRESS');
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/errors/InvalidAddressError.ts:
--------------------------------------------------------------------------------
1 | import { DomainError } from '../base/DomainError';
2 |
3 | export class InvalidAddressError extends DomainError {
4 | constructor() {
5 | super('INVALID_ADDRESS', true);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/errors/InvalidTimestampError.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { DomainError } from '../base/DomainError';
3 | import { InvalidTimestampError } from './InvalidTimestampError';
4 |
5 | describe('src/domain/errors/InvalidTimestampError', () => {
6 | it('should be defined', () => {
7 | expect(InvalidTimestampError).toBeDefined();
8 | });
9 |
10 | it('should be an instance of DomainError', () => {
11 | const error = new InvalidTimestampError();
12 |
13 | expect(error).toBeInstanceOf(DomainError);
14 | });
15 |
16 | describe('constructor', () => {
17 | it('should set error code to INVALID_TIMESTAMP', () => {
18 | const error = new InvalidTimestampError();
19 |
20 | expect(error.code).toBe('INVALID_TIMESTAMP');
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/errors/InvalidTimestampError.ts:
--------------------------------------------------------------------------------
1 | import { DomainError } from '../base/DomainError';
2 |
3 | export class InvalidTimestampError extends DomainError {
4 | constructor() {
5 | super('INVALID_TIMESTAMP', true);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/errors/InvalidUuidError.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { InvalidUuidError } from './InvalidUuidError';
3 | import { DomainError } from '../base/DomainError';
4 |
5 | describe('src/domain/valueObjects/InvalidUuidError', () => {
6 | it('should be defined', () => {
7 | expect(InvalidUuidError).toBeDefined();
8 | });
9 |
10 | it('should be instance of DomainError', () => {
11 | expect(new InvalidUuidError()).toBeInstanceOf(DomainError);
12 | });
13 |
14 | describe('constructor', () => {
15 | it('should set error code to INVALID_UUID', () => {
16 | const error = new InvalidUuidError();
17 |
18 | expect(error.toObject().code).toBe('INVALID_UUID');
19 | });
20 |
21 | it('should set exposable to false', () => {
22 | const error = new InvalidUuidError();
23 |
24 | expect(error.toObject().exposable).toBeFalsy();
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/errors/InvalidUuidError.ts:
--------------------------------------------------------------------------------
1 | import { DomainError } from '../base/DomainError';
2 |
3 | export class InvalidUuidError extends DomainError {
4 | constructor() {
5 | super('INVALID_UUID', false);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Address.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { Address } from './Address';
3 | import { InvalidAddressError } from '../errors/InvalidAddressError';
4 |
5 | describe('Address', () => {
6 | it('should create a valid Address object', () => {
7 | const addressValue = 'Valid Address';
8 | const address = Address.create(addressValue);
9 | expect(address.value).toBe(addressValue);
10 | });
11 |
12 | it('should throw InvalidAddressError for invalid input', () => {
13 | const invalidInput = 123;
14 | const createInvalidAddress = () => {
15 | Address.create(invalidInput as unknown as string);
16 | };
17 | expect(createInvalidAddress).toThrow(InvalidAddressError);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Address.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from '../base/ValueObject';
2 | import { InvalidAddressError } from '../errors/InvalidAddressError';
3 |
4 | interface AddressProps {
5 | value: string;
6 | }
7 |
8 | export class Address extends ValueObject {
9 | private constructor(name: string) {
10 | super({ value: name });
11 | }
12 |
13 | static create(name: string) {
14 | if (typeof name !== 'string') {
15 | throw new InvalidAddressError();
16 | }
17 |
18 | return new Address(name);
19 | }
20 |
21 | get value() {
22 | return this.props.value;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Timestamp.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { Timestamp } from './Timestamp';
3 | import { InvalidTimestampError } from '../errors/InvalidTimestampError';
4 |
5 | describe('Timestamp', () => {
6 | it('should create a valid Timestamp object', () => {
7 | const timestampValue = 0;
8 | const timestamp = Timestamp.create(timestampValue);
9 | expect(timestamp.value).toBe(timestampValue);
10 | });
11 |
12 | it('should throw InvalidTimestampError for invalid input', () => {
13 | const invalidInput = '123';
14 | const createInvalidTimestamp = () => {
15 | Timestamp.create(invalidInput as unknown as number);
16 | };
17 | expect(createInvalidTimestamp).toThrow(InvalidTimestampError);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Timestamp.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from '../base/ValueObject';
2 | import { InvalidTimestampError } from '../errors/InvalidTimestampError';
3 |
4 | interface TimestampProps {
5 | value: number;
6 | }
7 |
8 | export class Timestamp extends ValueObject {
9 | private constructor(name: number) {
10 | super({ value: name });
11 | }
12 |
13 | static create(name: number) {
14 | if (typeof name !== 'number') {
15 | throw new InvalidTimestampError();
16 | }
17 |
18 | return new Timestamp(name);
19 | }
20 |
21 | get value() {
22 | return this.props.value;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Txtype.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { TxTypesEnum } from '../enums/TxTypesEnum';
3 | import { TxType } from './Txtype';
4 |
5 | describe('TxType', () => {
6 | it('should create a TxType object with a valid transaction type', () => {
7 | Object.values(TxTypesEnum).forEach(txTypeValue => {
8 | const txType = TxType.create(txTypeValue);
9 | expect(txType.value).toBe(txTypeValue);
10 | });
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Txtype.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from '../base/ValueObject';
2 | import { TxTypesEnum } from '../enums/TxTypesEnum';
3 |
4 | interface TxTypeProps {
5 | value: TxTypesEnum;
6 | }
7 |
8 | export class TxType extends ValueObject {
9 | private constructor(txType: TxTypesEnum) {
10 | super({ value: txType });
11 | }
12 |
13 | static create(txType: TxTypesEnum) {
14 | return new TxType(txType);
15 | }
16 |
17 | get value() {
18 | return this.props.value;
19 | }
20 |
21 | get isBlock() {
22 | return this.props.value === TxTypesEnum.Block;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Uuid.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { v4, validate } from 'uuid';
3 | import { InvalidUuidError } from '../errors/InvalidUuidError';
4 | import { Uuid } from './Uuid';
5 | import { ValueObject } from '../base/ValueObject';
6 |
7 | describe('src/domain/valueObjects/Uuid', () => {
8 | it('should be defined', () => {
9 | expect(Uuid).toBeDefined();
10 | });
11 |
12 | it('should be instance of ValueObject', () => {
13 | expect(Uuid.create()).toBeInstanceOf(ValueObject);
14 | });
15 |
16 | describe('create', () => {
17 | describe('when an id is not provided', () => {
18 | it('should generate a valid uuid', () => {
19 | const uuid = Uuid.create();
20 |
21 | expect(validate(uuid.value)).toBeTruthy();
22 | });
23 | });
24 |
25 | describe('when a valid id is provided', () => {
26 | it('should set the id as value', () => {
27 | const validId = v4();
28 | const uuid = Uuid.create(validId);
29 |
30 | expect(uuid.value).toEqual(validId);
31 | });
32 | });
33 |
34 | describe('when an invalid id is provided', () => {
35 | it('should set the id as value', () => {
36 | const invalidId = 'invalid-id';
37 | const test = () => {
38 | Uuid.create(invalidId);
39 | };
40 |
41 | expect(test).toThrowError(InvalidUuidError);
42 | });
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/shared/src/domain/valueObjects/Uuid.ts:
--------------------------------------------------------------------------------
1 | import { v4, validate } from 'uuid';
2 | import { ValueObject } from '../base/ValueObject';
3 | import { InvalidUuidError } from '../errors/InvalidUuidError';
4 |
5 | interface UuidProps {
6 | value: string;
7 | }
8 | export class Uuid extends ValueObject {
9 | private constructor(id: string) {
10 | super({ value: id });
11 | }
12 |
13 | static create(id?: string): Uuid {
14 | if (id && !validate(id)) {
15 | throw new InvalidUuidError();
16 | }
17 |
18 | return new Uuid(id || v4());
19 | }
20 |
21 | get value(): string {
22 | return this.props.value;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './domain/base/DomainError';
2 | export * from './domain/base/Entity';
3 | export * from './domain/base/ValueObject';
4 | export * from './domain/valueObjects/Uuid';
5 | export * from './domain/base/event/Event';
6 | export * from './domain/base/event/Observable';
7 | export * from './domain/base/event/ObservableSet';
8 | export * from './domain/base/event/EventSubscription';
9 | export * from './domain/base/event/EventBus';
10 | export * from './domain/base/event/EventBusInstance';
11 | export * from './domain/entities/L2Block';
12 | export * from './domain/valueObjects/Timestamp';
13 | export * from './domain/enums/TxTypesEnum';
14 | export * from './domain/valueObjects/Txtype';
15 | export * from './domain/valueObjects/Address';
16 | export * from './networks/HemiTestnet';
17 | export * from './networks/HemiMainnet';
18 | export * from './domain/enums/NetworkEnum';
19 |
--------------------------------------------------------------------------------
/packages/shared/src/networks/HemiMainnet.ts:
--------------------------------------------------------------------------------
1 | import { hemi } from 'hemi-viem';
2 |
3 | export const HemiMainnet = hemi;
4 |
--------------------------------------------------------------------------------
/packages/shared/src/networks/HemiTestnet.ts:
--------------------------------------------------------------------------------
1 | import { hemiSepolia } from 'hemi-viem';
2 |
3 | export const HemiTestnet = hemiSepolia;
4 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.build.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.build.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "./dist/cjs",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "declarationDir": "./dist/types"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.build.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.build.json",
3 | "compilerOptions": {
4 | "module": "ESNext",
5 | "target": "ES2022",
6 | "outDir": "./dist/esm",
7 | "declaration": true,
8 | "sourceMap": true,
9 | "declarationMap": true,
10 | "declarationDir": "./dist/types"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "pipeline": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": ["dist/**"]
8 | },
9 | "lint": {
10 | "dependsOn": ["^build", "^lint"]
11 | },
12 | "dev": {
13 | "cache": false,
14 | "persistent": true
15 | },
16 | "format:check": {
17 | "cache": false,
18 | "persistent": true
19 | },
20 | "format:fix": {
21 | "cache": false,
22 | "persistent": true
23 | },
24 | "test": {
25 | "dependsOn": ["build"],
26 | "outputs": []
27 | },
28 | "start:api": {
29 | "cache": false,
30 | "persistent": true
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------