├── .dockerignore ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ ├── deploy-testnet.yml │ └── js-checks.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── CONTRIBUTING.md ├── CodingChallenge.md ├── Dockerfile.api ├── Dockerfile.web ├── LICENSE ├── README.md ├── apps ├── api │ ├── .env │ ├── .eslintrc.json │ ├── .gitignore │ ├── .svg │ ├── README.md │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── application │ │ │ ├── helpers │ │ │ │ ├── BroadcastToClients.ts │ │ │ │ └── broadcastToClients.spec.ts │ │ │ └── polling │ │ │ │ ├── pollingService.spec.ts │ │ │ │ └── pollingService.ts │ │ ├── domain │ │ │ ├── base │ │ │ │ ├── Entity.spec.ts │ │ │ │ └── Entity.ts │ │ │ ├── enums │ │ │ │ ├── BlockTypesEnum.spec.ts │ │ │ │ └── BlockTypesEnum.ts │ │ │ ├── errors │ │ │ │ ├── InvalidAddressError.spec.ts │ │ │ │ └── InvalidAddressError.ts │ │ │ └── repositories │ │ │ │ └── BlockRepository.ts │ │ ├── infrastructure │ │ │ └── repositories │ │ │ │ ├── blockPolling.spec.ts │ │ │ │ ├── blockPolling.ts │ │ │ │ ├── blockWebsocket.spec.ts │ │ │ │ └── blockWebsocket.ts │ │ ├── presentation │ │ │ ├── ExpressServer.spec.ts │ │ │ ├── ExpressServer.ts │ │ │ └── routes │ │ │ │ ├── PollingRoute.spec.ts │ │ │ │ └── PollingRoute.ts │ │ ├── server.spec.ts │ │ └── server.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── vitest.config.ts │ └── webSocketTest.html └── web │ ├── .env │ ├── .env.production │ ├── .eslintrc.json │ ├── .gitignore │ ├── @types │ ├── tonejs-instruments.d.ts │ └── vite-env.d.ts │ ├── README.md │ ├── index.html │ ├── nginx.conf │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── image │ │ ├── background │ │ ├── desktop.svg │ │ └── mobile.svg │ │ ├── close-button.svg │ │ ├── crypto-chords-icon.svg │ │ ├── crypto-chords.svg │ │ ├── cube │ │ ├── blue.png │ │ ├── green.png │ │ ├── orange.png │ │ └── purple.png │ │ ├── keyboard │ │ ├── base.svg │ │ └── keys │ │ │ ├── black.svg │ │ │ ├── blue │ │ │ ├── black.svg │ │ │ ├── white-left.svg │ │ │ ├── white-middle.svg │ │ │ ├── white-right.svg │ │ │ └── white.svg │ │ │ ├── green │ │ │ ├── black.svg │ │ │ ├── white-left.svg │ │ │ ├── white-middle.svg │ │ │ ├── white-right.svg │ │ │ └── white.svg │ │ │ ├── orange │ │ │ ├── black.svg │ │ │ ├── white-left.svg │ │ │ ├── white-middle.svg │ │ │ ├── white-right.svg │ │ │ └── white.svg │ │ │ ├── purple │ │ │ ├── black.svg │ │ │ ├── white-left.svg │ │ │ ├── white-middle.svg │ │ │ ├── white-right.svg │ │ │ └── white.svg │ │ │ ├── white-left.svg │ │ │ ├── white-middle.svg │ │ │ ├── white-right.svg │ │ │ └── white.svg │ │ ├── nav-button.svg │ │ └── social │ │ ├── discord.svg │ │ ├── linkedin.svg │ │ └── x.svg │ ├── src │ ├── application │ │ ├── ObservableService.ts │ │ ├── Service.ts │ │ ├── events │ │ │ └── ServiceResponseEvent.ts │ │ └── services │ │ │ ├── CreateCube │ │ │ ├── CreateCubeDtos.ts │ │ │ ├── CreateCubeService.spec.ts │ │ │ └── CreateCubeService.ts │ │ │ ├── CreateKeyboard │ │ │ ├── CreateKeyboardDtos.ts │ │ │ ├── CreateKeyboardService.spec.ts │ │ │ └── CreateKeyboardService.ts │ │ │ ├── CreateTransaction │ │ │ ├── CreateTransactionDtos.ts │ │ │ └── CreateTransactionService.ts │ │ │ ├── GetCubes │ │ │ ├── GetCubesDtos.ts │ │ │ ├── GetCubesService.spec.ts │ │ │ └── GetCubesService.ts │ │ │ ├── GetKeyboard │ │ │ ├── GetKeyboardDtos.ts │ │ │ └── GetKeyboardService.ts │ │ │ ├── GetOptions │ │ │ ├── GetOptionsDtos.ts │ │ │ └── GetOptionsService.ts │ │ │ ├── GetSelectedNetwork │ │ │ ├── GetSelectedNetworkResponseDto.ts │ │ │ └── GetSelectedNetworkService.ts │ │ │ ├── ListNetworks │ │ │ ├── ListNetworksResponseDto.ts │ │ │ ├── ListNetworksService.ts │ │ │ └── NetworkDto.ts │ │ │ ├── ListTransactions │ │ │ ├── ListTransactionsDtos.ts │ │ │ └── ListTransactionsService.ts │ │ │ ├── LoadInstrument │ │ │ ├── LoadInstrumentDtos.ts │ │ │ └── LoadInstrumentService.ts │ │ │ ├── PlaySound │ │ │ ├── PlaySoundDtos.ts │ │ │ └── PlaySoundService.ts │ │ │ ├── PressKey │ │ │ ├── PressKeyDtos.ts │ │ │ └── PressKeyService.ts │ │ │ ├── RecalculateCubePositions │ │ │ ├── RecalculateCubePositionsDtos.ts │ │ │ └── RecalculateCubePositionsService.ts │ │ │ ├── ReleaseKey │ │ │ ├── ReleaseKeyDtos.ts │ │ │ └── ReleaseKeyService.ts │ │ │ ├── SetInstrument │ │ │ ├── SetInstrumentDtos.ts │ │ │ └── SetInstrumentService.ts │ │ │ ├── SetMuted │ │ │ ├── SetMutedDtos.ts │ │ │ └── SetMutedService.ts │ │ │ ├── StopSound │ │ │ ├── StopSoundDtos.ts │ │ │ └── StopSoundService.ts │ │ │ └── SwitchNetwork │ │ │ ├── SwitchNetworkRequestDto.ts │ │ │ ├── SwitchNetworkResponseDto.ts │ │ │ └── SwitchNetworkService.ts │ ├── domain │ │ ├── entities │ │ │ ├── Cube.spec.ts │ │ │ ├── Cube.ts │ │ │ ├── Network.spec.ts │ │ │ ├── Network.ts │ │ │ ├── Transaction.spec.ts │ │ │ └── Transaction.ts │ │ ├── enum │ │ │ ├── InstrumentEnum.ts │ │ │ ├── KeyShapeEnum.ts │ │ │ ├── PitchClassEnum.ts │ │ │ └── TransactionColorEnum.ts │ │ ├── errors │ │ │ ├── InvalidCubePositionError.ts │ │ │ ├── InvalidIdPitchClassError.ts │ │ │ └── InvalidUnitIntervalError.ts │ │ ├── events │ │ │ └── TransactionCreatedEvent.ts │ │ ├── factories │ │ │ ├── KeyboardFactory.test.ts │ │ │ └── KeyboardFactory.ts │ │ ├── repositories │ │ │ ├── CubeRepository.ts │ │ │ ├── KeyboardRepository.ts │ │ │ ├── NetworkRepository.ts │ │ │ ├── OptionsRepository.ts │ │ │ └── TransactionRepository.ts │ │ ├── services │ │ │ └── SoundService.ts │ │ └── valueObjects │ │ │ ├── Instrument.ts │ │ │ ├── Key.ts │ │ │ ├── KeyShape.ts │ │ │ ├── Keyboard.ts │ │ │ ├── Options.ts │ │ │ ├── Pitch.ts │ │ │ ├── PitchClass.spec.ts │ │ │ ├── PitchClass.ts │ │ │ ├── TransactionColor.spec.ts │ │ │ ├── TransactionColor.ts │ │ │ ├── UnitInterval.spec.ts │ │ │ ├── UnitInterval.ts │ │ │ ├── UnitIntervalRange.spec.ts │ │ │ └── UnitIntervalRange.ts │ ├── infrastructure │ │ ├── repositories │ │ │ ├── InMemoryCubeRepository.spec.ts │ │ │ ├── InMemoryCubeRepository.ts │ │ │ ├── InMemoryKeyboardRepository.ts │ │ │ ├── LimitedInMemoryTransactionRepository.ts │ │ │ ├── LocalStorageOptionsRepository.ts │ │ │ └── StaticNetworksRepository.ts │ │ └── services │ │ │ └── ToneJs.ts │ └── presentation │ │ ├── common │ │ ├── base │ │ │ ├── Presenter.ts │ │ │ ├── StateController.spec.ts │ │ │ └── StateController.ts │ │ ├── context │ │ │ ├── domainServices.ts │ │ │ ├── presenters.ts │ │ │ ├── repositories.ts │ │ │ └── services.ts │ │ └── presenter │ │ │ ├── app │ │ │ ├── AppPresenter.ts │ │ │ └── AppPresenterState.ts │ │ │ ├── cubes │ │ │ ├── CubesPresenter.spec.ts │ │ │ ├── CubesPresenter.ts │ │ │ └── CubesPresenterState.ts │ │ │ ├── keyboard │ │ │ ├── KeyboardPresenter.spec.ts │ │ │ ├── KeyboardPresenter.ts │ │ │ └── KeyboardPresenterState.ts │ │ │ ├── options │ │ │ ├── OptionsPresenter.ts │ │ │ └── OptionsPresenterState.ts │ │ │ └── transactions │ │ │ ├── TransactionsPresenter.ts │ │ │ └── TransactionsPresenterState.ts │ │ └── react │ │ ├── App.tsx │ │ ├── components │ │ ├── Card.tsx │ │ ├── Checkbox.tsx │ │ ├── Cube.tsx │ │ ├── Cubes.tsx │ │ ├── Header.tsx │ │ ├── JoinCommunityButton.tsx │ │ ├── Key.tsx │ │ ├── Keyboard.tsx │ │ ├── Logo.tsx │ │ ├── MainContent.tsx │ │ ├── Menu.tsx │ │ ├── NavButton.tsx │ │ ├── NavItem.tsx │ │ ├── NavItems.tsx │ │ ├── NavMenu.tsx │ │ ├── NetworkSwitch.tsx │ │ ├── Options.tsx │ │ ├── Select.tsx │ │ ├── Social.tsx │ │ ├── Transaction.tsx │ │ ├── Transactions.tsx │ │ └── icons │ │ │ ├── CheckMark.tsx │ │ │ └── Chevron.tsx │ │ ├── context.ts │ │ ├── hooks │ │ ├── useOnClickOutside.ts │ │ └── usePresenter.ts │ │ ├── index.css │ │ └── main.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts ├── dockerbuild.sh ├── infrastructure └── kustomize │ ├── base │ ├── cryptochords-api-deployment.yaml │ ├── cryptochords-api-service.yaml │ ├── cryptochords-web-deployment.yaml │ ├── cryptochords-web-ingress.yaml │ ├── cryptochords-web-service.yaml │ └── kustomization.yaml │ └── overlays │ └── testnet │ ├── kustomization.yaml │ └── testnet-patch.yaml ├── package-lock.json ├── package.json ├── packages └── shared │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── domain │ │ ├── base │ │ │ ├── DomainError.spec.ts │ │ │ ├── DomainError.ts │ │ │ ├── Entity.spec.ts │ │ │ ├── Entity.ts │ │ │ ├── ValueObject.spec.ts │ │ │ ├── ValueObject.ts │ │ │ └── event │ │ │ │ ├── Event.ts │ │ │ │ ├── EventBus.ts │ │ │ │ ├── EventBusInstance.ts │ │ │ │ ├── EventSubscription.ts │ │ │ │ ├── Observable.ts │ │ │ │ └── ObservableSet.ts │ │ ├── entities │ │ │ ├── L2Block.spec.ts │ │ │ └── L2Block.ts │ │ ├── enums │ │ │ ├── NetworkEnum.ts │ │ │ └── TxTypesEnum.ts │ │ ├── errors │ │ │ ├── InvalidAddressError.spec.ts │ │ │ ├── InvalidAddressError.ts │ │ │ ├── InvalidTimestampError.spec.ts │ │ │ ├── InvalidTimestampError.ts │ │ │ ├── InvalidUuidError.spec.ts │ │ │ └── InvalidUuidError.ts │ │ └── valueObjects │ │ │ ├── Address.spec.ts │ │ │ ├── Address.ts │ │ │ ├── Timestamp.spec.ts │ │ │ ├── Timestamp.ts │ │ │ ├── Txtype.spec.ts │ │ │ ├── Txtype.ts │ │ │ ├── Uuid.spec.ts │ │ │ └── Uuid.ts │ ├── index.ts │ └── networks │ │ ├── HemiMainnet.ts │ │ └── HemiTestnet.ts │ ├── tsconfig.build.cjs.json │ ├── tsconfig.build.esm.json │ ├── tsconfig.build.json │ └── tsconfig.json └── turbo.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ['apps/**', 'packages/**'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: false, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gabmontes @rafappelt -------------------------------------------------------------------------------- /.github/workflows/js-checks.yml: -------------------------------------------------------------------------------- 1 | name: JS Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | js-checks: 13 | uses: hemilabs/actions/.github/workflows/js-checks.yml@main 14 | with: 15 | node-versions: '["16", "18", "20", "22"]' 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | # Testing 15 | coverage 16 | 17 | # Turbo 18 | .turbo 19 | 20 | # Vercel 21 | .vercel 22 | 23 | # Build Outputs 24 | .next/ 25 | out/ 26 | build 27 | dist 28 | 29 | 30 | # Debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # Misc 36 | .DS_Store 37 | *.pem 38 | .vscode/ 39 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "quoteProps": "consistent", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /CodingChallenge.md: -------------------------------------------------------------------------------- 1 | # Coding Challenge Submission

2 | 3 | ## Description 4 | 5 | Describe your project here 6 | 7 | ## Prerequisites, Dependencies, Versions 8 | 9 | Please add anything here you've built that requires the above specifics 10 | 11 | ## Licensing 12 | 13 | Please state if license is CC, MIT or ISC or other (please no GPL) 14 | 15 | ## Outside Libraries 16 | 17 | Link any outside open source libraries used 18 | 19 | ## Steps 20 | 21 | Tell us how to run it locally 22 | -------------------------------------------------------------------------------- /Dockerfile.api: -------------------------------------------------------------------------------- 1 | ## -*- docker-image-name: "hemilabs/cryptochords-api:1.0.0" -*- 2 | FROM node:20 3 | 4 | COPY package*.json ./ 5 | 6 | RUN mkdir -p /usr/src/app 7 | 8 | WORKDIR /usr/src/app 9 | 10 | COPY . . 11 | 12 | RUN npm install 13 | 14 | RUN npm run build 15 | 16 | EXPOSE 3000 3001 17 | 18 | CMD [ "npm", "run", "start:api" ] 19 | -------------------------------------------------------------------------------- /Dockerfile.web: -------------------------------------------------------------------------------- 1 | ## -*- docker-image-name: "hemilabs/cryptochords-web:1.0.0" -*- 2 | FROM node:20 as build 3 | 4 | COPY package*.json ./ 5 | 6 | RUN mkdir -p /usr/src/app 7 | 8 | WORKDIR /usr/src/app 9 | 10 | COPY . . 11 | 12 | RUN npm install 13 | 14 | RUN npm run build 15 | 16 | FROM nginx:alpine 17 | 18 | COPY --from=build /usr/src/app/apps/web/dist /usr/share/nginx/html 19 | 20 | COPY --from=build /usr/src/app/apps/web/nginx.conf /etc/nginx/conf.d/default.conf 21 | 22 | EXPOSE 80 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hemi Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/api/.env: -------------------------------------------------------------------------------- 1 | # .env file for the API 2 | USE_WEBSOCKET_NODE_L2=true 3 | ENABLE_MAINNET=true 4 | -------------------------------------------------------------------------------- /apps/api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["bloq", "prettier"], 3 | "ignorePatterns": ["dist", "_esm/*", "_cjs/*", "_types/*"], 4 | "overrides": [ 5 | { 6 | "extends": ["bloq/typescript", "prettier"], 7 | "files": ["src/**/*.ts"] 8 | }, 9 | { 10 | "extends": ["bloq/markdown"], 11 | "files": ["*.md"] 12 | }, 13 | { 14 | "extends": ["bloq/vitest", "prettier"], 15 | "files": ["*.spec.{js,ts}"] 16 | } 17 | ], 18 | "parserOptions": { 19 | "sourceType": "module" 20 | }, 21 | "root": true, 22 | "rules": { 23 | "arrow-body-style": "off", 24 | "no-console": "off" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules/ 3 | dist/ 4 | coverage/ 5 | .vscode 6 | -------------------------------------------------------------------------------- /apps/api/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["./**/*.ts"], 3 | "ext": "ts", 4 | "ignore": ["./**/*.spec.ts"], 5 | "exec": "ts-node ./src/server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-chords-api", 3 | "version": "1.2.4", 4 | "description": "CryptoChords API", 5 | "main": "src/server.ts", 6 | "scripts": { 7 | "test": "vitest run src", 8 | "test:cov": "vitest run src --coverage", 9 | "test:watch": "vitest watch src", 10 | "dev": "nodemon", 11 | "build": "tsc -p tsconfig.build.json", 12 | "lint": "eslint . --ext ts", 13 | "start:api": "node dist/server.js" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@cryptochords/shared": "*", 19 | "dotenv": "16.4.7", 20 | "express": "4.21.2", 21 | "web3": "4.16.0" 22 | }, 23 | "devDependencies": { 24 | "@types/dotenv": "8.2.3", 25 | "@types/express": "5.0.0", 26 | "@types/node": "22.10.2", 27 | "@types/ws": "8.5.13", 28 | "@vitest/coverage-v8": "2.1.8", 29 | "@typescript-eslint/eslint-plugin": "8.18.2", 30 | "@typescript-eslint/parser": "8.18.2", 31 | "eslint": "8.57.1", 32 | "eslint-config-bloq": "4.4.1", 33 | "nodemon": "3.1.9", 34 | "prettier": "3.3.3", 35 | "ts-node": "10.9.2", 36 | "typescript": "5.3.3", 37 | "vitest": "2.1.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/api/src/application/helpers/BroadcastToClients.ts: -------------------------------------------------------------------------------- 1 | import WebSocket, { WebSocketServer } from 'ws'; 2 | import { L2Block } from '@cryptochords/shared'; 3 | 4 | const broadcastToClients = (wss: WebSocketServer, l2Block: L2Block): void => { 5 | wss.clients.forEach((client: WebSocket) => { 6 | if (client.readyState === WebSocket.OPEN) { 7 | client.send(JSON.stringify(l2Block.toJSON())); 8 | } 9 | }); 10 | }; 11 | 12 | export default broadcastToClients; 13 | -------------------------------------------------------------------------------- /apps/api/src/application/helpers/broadcastToClients.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { describe, it, expect, vi } from 'vitest'; 3 | import WebSocket, { WebSocketServer } from 'ws'; 4 | import broadcastToClients from './BroadcastToClients'; 5 | 6 | describe('BroadcastToClients', () => { 7 | it('should send a message to all connected clients', () => { 8 | const mockSend = vi.fn(); 9 | const wss: any = new WebSocketServer({ noServer: true }); 10 | wss.clients = new Set([ 11 | { 12 | readyState: WebSocket.OPEN, 13 | send: mockSend, 14 | }, 15 | { 16 | readyState: WebSocket.CLOSED, 17 | send: mockSend, 18 | }, 19 | ]); 20 | 21 | const l2Block = { 22 | toJSON: () => ({ key: 'value' }), 23 | }; 24 | 25 | broadcastToClients(wss, l2Block as any); 26 | 27 | expect(mockSend).toHaveBeenCalledTimes(1); 28 | expect(mockSend).toHaveBeenCalledWith(JSON.stringify({ key: 'value' })); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/api/src/application/polling/pollingService.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { WebSocketServer } from 'ws'; 3 | import { EventEmitter } from 'events'; 4 | import broadcastToClients from '../helpers/BroadcastToClients'; 5 | import { TxTypesEnum } from '@cryptochords/shared'; 6 | import { PollingService } from './pollingService'; 7 | 8 | vi.mock('ws'); 9 | vi.mock('../helpers/BroadcastToClients', () => ({ 10 | __esModule: true, 11 | default: vi.fn(), 12 | })); 13 | 14 | class MockBlockRepository extends EventEmitter { 15 | execute = vi.fn(); 16 | stop = vi.fn(); 17 | } 18 | 19 | describe('PollingService', () => { 20 | let pollingService: PollingService; 21 | let mockBlockRepository: MockBlockRepository; 22 | let mockWss: WebSocketServer; 23 | 24 | beforeEach(() => { 25 | mockBlockRepository = new MockBlockRepository(); 26 | mockWss = new WebSocketServer({ noServer: true }); 27 | pollingService = new PollingService(mockBlockRepository); 28 | }); 29 | 30 | it('should execute and handle Block and Eth events', () => { 31 | const websocketUrl = 'ws://test.url'; 32 | const mockL2Block = { key: 'Value' }; 33 | 34 | pollingService.execute(mockWss, websocketUrl); 35 | 36 | expect(mockBlockRepository.execute).toHaveBeenCalledWith(websocketUrl); 37 | 38 | mockBlockRepository.emit(TxTypesEnum.Block, mockL2Block); 39 | mockBlockRepository.emit(TxTypesEnum.Eth, mockL2Block); 40 | 41 | expect(broadcastToClients).toHaveBeenCalledWith(mockWss, mockL2Block); 42 | expect(broadcastToClients).toHaveBeenCalledTimes(2); 43 | }); 44 | 45 | it('should stop the repository on stop', () => { 46 | pollingService.stop(); 47 | expect(mockBlockRepository.stop).toHaveBeenCalled(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /apps/api/src/application/polling/pollingService.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws'; 2 | import { BlockRepository } from '../../domain/repositories/BlockRepository'; 3 | import broadcastToClients from '../helpers/BroadcastToClients'; 4 | import { TxTypesEnum } from '@cryptochords/shared'; 5 | import { L2Block } from '@cryptochords/shared'; 6 | export class PollingService { 7 | constructor(private blockRepository: BlockRepository) {} 8 | 9 | execute(wss: WebSocketServer, url: string): void { 10 | this.blockRepository.execute(url); 11 | 12 | this.blockRepository.on(TxTypesEnum.Block, (l2Block: L2Block) => 13 | broadcastToClients(wss, l2Block), 14 | ); 15 | this.blockRepository.on(TxTypesEnum.Eth, (l2Block: L2Block) => 16 | broadcastToClients(wss, l2Block), 17 | ); 18 | } 19 | 20 | stop() { 21 | this.blockRepository.stop(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/api/src/domain/base/Entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Entity } from './Entity'; 3 | import { ValueObject } from '@cryptochords/shared'; 4 | 5 | interface TestProps { 6 | value: string; 7 | } 8 | 9 | class TestEntity extends Entity { 10 | static create(props: TestProps): TestEntity { 11 | return new TestEntity(props); 12 | } 13 | 14 | get value(): string { 15 | return this.props.value; 16 | } 17 | } 18 | 19 | describe('src/domain/base/Entity', () => { 20 | it('should be defined', () => { 21 | expect(Entity).toBeDefined(); 22 | }); 23 | 24 | it('should be an instance of ValueObject', () => { 25 | const entity = TestEntity.create({ value: 'test' }); 26 | 27 | expect(entity).toBeInstanceOf(ValueObject); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /apps/api/src/domain/base/Entity.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | 3 | export class Entity extends ValueObject { 4 | protected constructor(props: T) { 5 | super(props); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/domain/enums/BlockTypesEnum.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { BlockTypesEnum } from './BlockTypesEnum'; 3 | 4 | describe('BlockTypesEnum', () => { 5 | it('should have a LEGACY type with value "0"', () => { 6 | expect(BlockTypesEnum.LEGACY).toBe('0'); 7 | }); 8 | 9 | it('should have an EIP2930 type with value "1"', () => { 10 | expect(BlockTypesEnum.EIP2930).toBe('1'); 11 | }); 12 | 13 | it('should have an EIP1559 type with value "2"', () => { 14 | expect(BlockTypesEnum.EIP1559).toBe('2'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /apps/api/src/domain/enums/BlockTypesEnum.ts: -------------------------------------------------------------------------------- 1 | export enum BlockTypesEnum { 2 | LEGACY = '0', 3 | EIP2930 = '1', 4 | EIP1559 = '2', 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/src/domain/errors/InvalidAddressError.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { DomainError } from '@cryptochords/shared'; 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 | -------------------------------------------------------------------------------- /apps/api/src/domain/errors/InvalidAddressError.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from '@cryptochords/shared'; 2 | 3 | export class InvalidAddressError extends DomainError { 4 | constructor() { 5 | super('INVALID_ADDRESS', true); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/domain/repositories/BlockRepository.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export interface BlockRepository extends EventEmitter { 4 | execute(url: string): void; 5 | stop(): void; 6 | } 7 | -------------------------------------------------------------------------------- /apps/api/src/infrastructure/repositories/blockPolling.spec.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'; 3 | import { BlockTypesEnum } from '../../domain/enums/BlockTypesEnum'; 4 | import { BlockPollingRepository } from './blockPolling'; 5 | 6 | vi.mock('@cryptochords/shared', () => ({ 7 | Address: { 8 | create: vi.fn().mockReturnValue({}), 9 | }, 10 | L2Block: { 11 | create: vi.fn().mockReturnValue({}), 12 | }, 13 | TxType: { 14 | create: vi.fn().mockReturnValue({}), 15 | }, 16 | TxTypesEnum: { 17 | Block: 'Block', 18 | Eth: 'Eth', 19 | }, 20 | })); 21 | 22 | describe('BlockPollingRepository', () => { 23 | let blockPollingRepository: BlockPollingRepository; 24 | let mockWeb3: Web3; 25 | 26 | beforeEach(() => { 27 | blockPollingRepository = new BlockPollingRepository(); 28 | mockWeb3 = new Web3(); 29 | mockWeb3.eth.getBlockNumber = vi.fn().mockResolvedValue(100); 30 | mockWeb3.eth.getBlock = vi.fn().mockResolvedValue({ 31 | hash: '0xhash', 32 | transactions: [], 33 | }); 34 | vi.useFakeTimers(); 35 | 36 | vi.spyOn(blockPollingRepository, 'emit'); 37 | }); 38 | 39 | afterEach(() => { 40 | vi.runOnlyPendingTimers(); 41 | vi.useRealTimers(); 42 | vi.clearAllMocks(); 43 | }); 44 | 45 | it('should start and stop polling correctly', () => { 46 | blockPollingRepository.execute('http://localhost:8545', 1000); 47 | expect(vi.getTimerCount()).toBe(1); 48 | blockPollingRepository.stop(); 49 | expect(vi.getTimerCount()).toBe(0); 50 | }); 51 | 52 | it('should emit "Block" event for new blocks with no transactions', async () => { 53 | await blockPollingRepository.checkNewBlocks(mockWeb3); 54 | expect(blockPollingRepository.emit).toHaveBeenCalledWith( 55 | 'Block', 56 | expect.anything(), 57 | ); 58 | }); 59 | 60 | it('should emit "Block" and "Eth" events for new blocks with EIP1559 transactions', async () => { 61 | mockWeb3.eth.getBlock = vi.fn().mockResolvedValue({ 62 | hash: '0xnewhash', 63 | transactions: [{ from: '0xSomeAddress', type: BlockTypesEnum.EIP1559 }], 64 | }); 65 | 66 | await blockPollingRepository.checkNewBlocks(mockWeb3); 67 | expect(blockPollingRepository.emit).toHaveBeenCalledWith( 68 | 'Block', 69 | expect.anything(), 70 | ); 71 | expect(blockPollingRepository.emit).toHaveBeenCalledWith( 72 | 'Eth', 73 | expect.anything(), 74 | ); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /apps/api/src/infrastructure/repositories/blockPolling.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'stream'; 2 | import { BlockRepository } from '../../domain/repositories/BlockRepository'; 3 | import Web3 from 'web3'; 4 | import { TxTypesEnum } from '@cryptochords/shared'; 5 | import { TxType } from '@cryptochords/shared'; 6 | import { Address } from '@cryptochords/shared'; 7 | import { BlockTypesEnum } from '../../domain/enums/BlockTypesEnum'; 8 | import { L2Block } from '@cryptochords/shared'; 9 | 10 | export class BlockPollingRepository 11 | extends EventEmitter 12 | implements BlockRepository 13 | { 14 | private latestBlockNumber = BigInt(0); 15 | private pollingIntervalId: NodeJS.Timeout | null = null; 16 | 17 | execute(rpcUrl: string, pollingInterval = 5000): void { 18 | const web3 = new Web3(rpcUrl); 19 | this.poll(web3, pollingInterval); 20 | } 21 | 22 | stop() { 23 | if (this.pollingIntervalId) { 24 | clearInterval(this.pollingIntervalId); 25 | this.pollingIntervalId = null; 26 | } 27 | } 28 | 29 | async checkNewBlocks(web3: Web3): Promise { 30 | const currentBlockNumber = await web3.eth.getBlockNumber(); 31 | 32 | if (currentBlockNumber > this.latestBlockNumber) { 33 | this.latestBlockNumber = currentBlockNumber; 34 | 35 | const block = await web3.eth.getBlock(currentBlockNumber, true); 36 | if (block) { 37 | this.emit( 38 | TxTypesEnum.Block, 39 | L2Block.create({ 40 | address: Address.create(block.hash ? block.hash.toString() : ''), 41 | txType: TxType.create(TxTypesEnum.Block), 42 | }), 43 | ); 44 | 45 | if (block.transactions) { 46 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 47 | block.transactions.forEach((tx: any) => { 48 | if (tx.type.toString() === BlockTypesEnum.EIP1559) { 49 | this.emit( 50 | TxTypesEnum.Eth, 51 | L2Block.create({ 52 | address: Address.create(tx.from), 53 | txType: TxType.create(TxTypesEnum.Eth), 54 | }), 55 | ); 56 | } 57 | }); 58 | } 59 | } 60 | } 61 | } 62 | 63 | poll(web3: Web3, pollingInterval: number) { 64 | this.pollingIntervalId = setInterval( 65 | () => this.checkNewBlocks(web3), 66 | pollingInterval, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/api/src/presentation/ExpressServer.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 2 | import { ExpressServer } from './ExpressServer'; 3 | import { PollingRoute } from './routes/PollingRoute'; 4 | import express from 'express'; 5 | import { HemiTestnet } from '@cryptochords/shared'; 6 | 7 | vi.mock('express', () => { 8 | const listenMock = vi.fn((_port, callback) => { 9 | callback(); 10 | return { 11 | close: vi.fn(cb => cb && cb()), 12 | }; 13 | }); 14 | return { 15 | __esModule: true, 16 | default: vi.fn(() => ({ 17 | listen: listenMock, 18 | })), 19 | }; 20 | }); 21 | 22 | vi.mock('ws', () => ({ 23 | Server: vi.fn().mockImplementation(() => ({ 24 | on: vi.fn(), 25 | })), 26 | })); 27 | 28 | vi.mock('./routes/PollingRoute', () => ({ 29 | PollingRoute: vi.fn().mockImplementation(() => ({ 30 | initialize: vi.fn(), 31 | stop: vi.fn(), 32 | })), 33 | })); 34 | 35 | describe('ExpressServer', () => { 36 | let expressServer: ExpressServer; 37 | let pollingRouteMock: PollingRoute; 38 | 39 | const MOCK_PORT = 3000; 40 | const USE_WEBSOCKET = true; 41 | const MOCK_WEBSOCKET_URL = HemiTestnet.rpcUrls.default.webSocket[0]; 42 | const MOCK_RPC_URL = HemiTestnet.rpcUrls.default.http[0]; 43 | 44 | beforeEach(() => { 45 | vi.clearAllMocks(); 46 | 47 | pollingRouteMock = new PollingRoute( 48 | USE_WEBSOCKET, 49 | MOCK_WEBSOCKET_URL, 50 | MOCK_RPC_URL, 51 | ); 52 | expressServer = new ExpressServer(pollingRouteMock, MOCK_PORT); 53 | }); 54 | 55 | afterEach(() => { 56 | vi.clearAllMocks(); 57 | }); 58 | 59 | it('should start the server correctly', () => { 60 | expressServer.start(); 61 | 62 | const mockedExpress = express(); 63 | expect(mockedExpress.listen).toHaveBeenCalledWith( 64 | MOCK_PORT, 65 | expect.any(Function), 66 | ); 67 | expect(pollingRouteMock.initialize).toHaveBeenCalled(); 68 | }); 69 | 70 | it('should stop the server correctly', async () => { 71 | expressServer.start(); 72 | await expressServer.stop(); 73 | 74 | expect(pollingRouteMock.stop).toHaveBeenCalled(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /apps/api/src/presentation/ExpressServer.ts: -------------------------------------------------------------------------------- 1 | import express, { Express } from 'express'; 2 | import { Server as HTTPServer } from 'http'; 3 | import { Server as WebSocketServer } from 'ws'; 4 | import 'dotenv/config'; 5 | import { PollingRoute } from './routes/PollingRoute'; 6 | 7 | export class ExpressServer { 8 | private pollingRoute: PollingRoute; 9 | private api: Express; 10 | private httpServer: HTTPServer | null = null; 11 | private wss: WebSocketServer | null = null; 12 | private port: string | number; 13 | 14 | constructor(pollingRoute: PollingRoute, port: number) { 15 | this.pollingRoute = pollingRoute; 16 | this.api = express(); 17 | this.port = port || 3000; 18 | } 19 | 20 | public start(): void { 21 | this.httpServer = this.api.listen(this.port, () => { 22 | console.log(`Express server running on port ${this.port}`); 23 | }); 24 | 25 | this.wss = new WebSocketServer({ server: this.httpServer }); 26 | 27 | this.wss.on('connection', ws => { 28 | console.log('WebSocket client connected'); 29 | ws.on('message', (message: string) => { 30 | console.log('Message received:', message); 31 | }); 32 | }); 33 | 34 | this.pollingRoute.initialize(this.wss); 35 | } 36 | 37 | async stop(): Promise { 38 | console.info('CryptoChords API | Closing HTTP Server'); 39 | 40 | return await new Promise(resolve => { 41 | this.httpServer?.close(error => { 42 | if (error) { 43 | console.error( 44 | `CryptoChords API | Error Closing HTTP Server: ${error.message}`, 45 | ); 46 | } else { 47 | this.wss = null; 48 | console.info('CryptoChords API | WSS Server successfully closed'); 49 | } 50 | this.pollingRoute.stop(); 51 | resolve(); 52 | }); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/api/src/presentation/routes/PollingRoute.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import { WebSocketServer } from 'ws'; 3 | import { PollingRoute } from './PollingRoute'; 4 | import { HemiTestnet } from '@cryptochords/shared'; 5 | 6 | vi.mock('../../application/polling/pollingService', () => { 7 | return { 8 | PollingService: vi.fn().mockImplementation(() => ({ 9 | execute: vi.fn(), 10 | stop: vi.fn(), 11 | })), 12 | }; 13 | }); 14 | 15 | vi.mock('ws'); 16 | vi.mock('../../infrastructure/repositories/blockWebsocket', () => ({ 17 | BlockWebsocketRepository: vi.fn(), 18 | })); 19 | vi.mock('../../infrastructure/repositories/blockPolling', () => ({ 20 | BlockPollingRepository: vi.fn(), 21 | })); 22 | 23 | describe('PollingRoute', () => { 24 | let wss: WebSocketServer; 25 | 26 | const MOCK_WEBSOCKET_URL = HemiTestnet.rpcUrls.default.webSocket[0]; 27 | const MOCK_RPC_URL = HemiTestnet.rpcUrls.default.http[0]; 28 | 29 | beforeEach(() => { 30 | vi.clearAllMocks(); 31 | wss = new WebSocketServer({ noServer: true }); 32 | }); 33 | 34 | it('should initialize with WebSocket when useWebsocket is true', () => { 35 | const pollingRoute = new PollingRoute( 36 | true, 37 | MOCK_WEBSOCKET_URL, 38 | MOCK_RPC_URL, 39 | ); 40 | pollingRoute.initialize(wss); 41 | 42 | const mockPollingService = pollingRoute['pollingService']; 43 | expect(mockPollingService.execute).toHaveBeenCalledWith( 44 | wss, 45 | MOCK_WEBSOCKET_URL, 46 | ); 47 | }); 48 | 49 | it('should initialize with Polling when useWebsocket is false', () => { 50 | const pollingRoute = new PollingRoute( 51 | false, 52 | MOCK_WEBSOCKET_URL, 53 | MOCK_RPC_URL, 54 | ); 55 | pollingRoute.initialize(wss); 56 | 57 | const mockPollingService = pollingRoute['pollingService']; 58 | expect(mockPollingService.execute).toHaveBeenCalledWith(wss, MOCK_RPC_URL); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /apps/api/src/presentation/routes/PollingRoute.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws'; 2 | import { BlockWebsocketRepository } from '../../infrastructure/repositories/blockWebsocket'; 3 | import { BlockPollingRepository } from '../../infrastructure/repositories/blockPolling'; 4 | import { PollingService } from '../../application/polling/pollingService'; 5 | import 'dotenv/config'; 6 | 7 | export class PollingRoute { 8 | private pollingService: PollingService; 9 | private url: string; 10 | 11 | constructor(useWebsocket: boolean, websocketUrl: string, rpcUrl: string) { 12 | if (useWebsocket) { 13 | this.url = websocketUrl; 14 | const blockWebsocketRepository = new BlockWebsocketRepository(); 15 | this.pollingService = new PollingService(blockWebsocketRepository); 16 | } else { 17 | this.url = rpcUrl; 18 | const blockPollingRepository = new BlockPollingRepository(); 19 | this.pollingService = new PollingService(blockPollingRepository); 20 | } 21 | } 22 | 23 | public initialize(wss: WebSocketServer): void { 24 | this.pollingService.execute(wss, this.url); 25 | } 26 | 27 | public stop(): void { 28 | this.pollingService.stop(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/api/src/server.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | describe, 3 | it, 4 | expect, 5 | beforeAll, 6 | afterAll, 7 | vi, 8 | beforeEach, 9 | afterEach, 10 | } from 'vitest'; 11 | import { ExpressServer } from './presentation/ExpressServer'; 12 | 13 | vi.mock('./presentation/ExpressServer'); 14 | 15 | describe('server', () => { 16 | beforeEach(() => { 17 | vi.spyOn(process, 'exit').mockImplementation(() => { 18 | return undefined as never; 19 | }); 20 | }); 21 | 22 | afterEach(() => { 23 | vi.restoreAllMocks(); 24 | }); 25 | 26 | beforeAll(() => { 27 | ExpressServer.prototype.start = vi.fn(); 28 | ExpressServer.prototype.stop = vi.fn(); 29 | }); 30 | 31 | afterAll(() => { 32 | vi.restoreAllMocks(); 33 | }); 34 | 35 | it('should start the server', async () => { 36 | await import('./server'); 37 | expect(ExpressServer.prototype.start).toHaveBeenCalled(); 38 | }); 39 | 40 | it('should stop the server on SIGTERM', async () => { 41 | process.emit('SIGTERM'); 42 | expect(ExpressServer.prototype.stop).toHaveBeenCalled(); 43 | }); 44 | 45 | it('should stop the server on SIGINT', async () => { 46 | process.emit('SIGINT'); 47 | expect(ExpressServer.prototype.stop).toHaveBeenCalled(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /apps/api/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { ExpressServer } from './presentation/ExpressServer'; 3 | import { PollingRoute } from './presentation/routes/PollingRoute'; 4 | import { HemiTestnet, HemiMainnet } from '@cryptochords/shared'; 5 | 6 | const useWebsocket = process.env['USE_WEBSOCKET_NODE_L2'] === 'true'; 7 | const useMainnet = process.env['ENABLE_MAINNET'] === 'true'; 8 | // Testnet 9 | const websocketTestnet = HemiTestnet.rpcUrls.default.webSocket[0]; 10 | const rpcTestnet = HemiTestnet.rpcUrls.default.http[0]; 11 | const pollingRouteTestnet = new PollingRoute( 12 | useWebsocket, 13 | websocketTestnet, 14 | rpcTestnet, 15 | ); 16 | const serverTestnet = new ExpressServer(pollingRouteTestnet, 3000); 17 | // Mainnet 18 | const websocketMainnet = HemiMainnet.rpcUrls.default.webSocket[0]; 19 | const rpcMainnet = HemiMainnet.rpcUrls.default.http[0]; 20 | const pollingRouteMainnet = new PollingRoute( 21 | useWebsocket, 22 | websocketMainnet, 23 | rpcMainnet, 24 | ); 25 | const serverMainnet = new ExpressServer(pollingRouteMainnet, 3001); 26 | 27 | const startServer = async (): Promise => { 28 | await serverTestnet.start(); 29 | if (useMainnet) { 30 | await serverMainnet.start(); 31 | } 32 | }; 33 | 34 | const stopServer = async (): Promise => { 35 | await serverTestnet.stop(); 36 | if (useMainnet) { 37 | await serverMainnet.stop(); 38 | } 39 | }; 40 | 41 | process.on('SIGTERM', async () => { 42 | await stopServer(); 43 | process.exit(0); 44 | }); 45 | 46 | process.on('SIGINT', async () => { 47 | await stopServer(); 48 | process.exit(0); 49 | }); 50 | 51 | startServer(); 52 | -------------------------------------------------------------------------------- /apps/api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/**/*.spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/api/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | // instanbul excludes interfaces from coverage 7 | include: ['src/**/*.ts'], 8 | provider: 'istanbul', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /apps/api/webSocketTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocket Test 6 | 7 | 8 |

Last block: waiting...

9 |

Last eth tx: waiting...

10 |

Last piano note: waiting...

11 | 12 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /apps/web/.env: -------------------------------------------------------------------------------- 1 | VITE_TESTNET_API_WEBSERVICE_URL=ws://localhost:3000 2 | VITE_MAINNET_API_WEBSERVICE_URL=ws://localhost:3001 3 | VITE_USE_API_MOCK=false 4 | VITE_ENABLE_MAINNET=true -------------------------------------------------------------------------------- /apps/web/.env.production: -------------------------------------------------------------------------------- 1 | VITE_TESTNET_API_WEBSERVICE_URL=wss://\${host}/api/testnet 2 | VITE_MAINNET_API_WEBSERVICE_URL=wss://\${host}/api/mainnet 3 | VITE_USE_API_MOCK=false 4 | VITE_ENABLE_MAINNET=true -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["bloq", "prettier"], 3 | "ignorePatterns": ["**/*.d.ts", "dist", "_esm/*", "_cjs/*", "_types/*"], 4 | "overrides": [ 5 | { 6 | "extends": ["bloq/typescript", "prettier"], 7 | "files": ["src/**/*.ts"] 8 | }, 9 | { 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:react-hooks/recommended" 14 | ], 15 | "files": ["src/**/*.tsx"], 16 | "parser": "@typescript-eslint/parser" 17 | }, 18 | { 19 | "extends": ["bloq/markdown"], 20 | "files": ["*.md"] 21 | }, 22 | { 23 | "extends": ["bloq/vitest", "prettier"], 24 | "files": ["*.spec.{js,ts}"] 25 | } 26 | ], 27 | "parserOptions": { 28 | "sourceType": "module" 29 | }, 30 | "plugins": ["sort-keys-fix", "@typescript-eslint"], 31 | "root": true, 32 | "rules": { 33 | "arrow-body-style": "off", 34 | "promise/catch-or-return": "off", 35 | "prefer-arrow/prefer-arrow-functions": "off", 36 | "sort-keys-fix/sort-keys-fix": ["warn"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /apps/web/@types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | Crypto Chords 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri $uri/ /index.html; 9 | 10 | add_header Content-Security-Policy "upgrade-insecure-requests"; 11 | 12 | add_header Permissions-Policy "geolocation=(), microphone=()"; 13 | } 14 | 15 | location /api/mainnet { 16 | proxy_pass http://cryptochords-api-service:3001; 17 | proxy_http_version 1.1; 18 | proxy_set_header Upgrade $http_upgrade; 19 | proxy_set_header Connection "Upgrade"; 20 | proxy_set_header Permissions-Policy "geolocation=(), microphone=()"; 21 | } 22 | 23 | location /api/testnet { 24 | proxy_pass http://cryptochords-api-service:3000; 25 | proxy_http_version 1.1; 26 | proxy_set_header Upgrade $http_upgrade; 27 | proxy_set_header Connection "Upgrade"; 28 | proxy_set_header Permissions-Policy "geolocation=(), microphone=()"; 29 | } 30 | 31 | error_page 500 502 503 504 /50x.html; 32 | 33 | location = /50x.html { 34 | root /usr/share/nginx/html; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-chords-web", 3 | "private": true, 4 | "version": "1.2.4", 5 | "description": "CryptoChords Web", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx", 11 | "preview": "vite preview", 12 | "test": "vitest run", 13 | "test:cov": "vitest run --coverage", 14 | "test:watch": "vitest watch" 15 | }, 16 | "dependencies": { 17 | "@cryptochords/shared": "*", 18 | "hemi-socials": "1.0.0", 19 | "qs": "6.13.1", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-select": "5.9.0", 23 | "tone": "15.0.4", 24 | "tonejs-instrument-bass-electric-mp3": "^1.1.2", 25 | "tonejs-instrument-bassoon-mp3": "1.1.2", 26 | "tonejs-instrument-cello-mp3": "1.1.1", 27 | "tonejs-instrument-clarinet-mp3": "1.1.2", 28 | "tonejs-instrument-contrabass-mp3": "1.1.2", 29 | "tonejs-instrument-flute-mp3": "1.1.2", 30 | "tonejs-instrument-french-horn-mp3": "1.1.2", 31 | "tonejs-instrument-guitar-acoustic-mp3": "1.1.2", 32 | "tonejs-instrument-guitar-electric-mp3": "1.1.1", 33 | "tonejs-instrument-guitar-nylon-mp3": "1.1.1", 34 | "tonejs-instrument-harmonium-mp3": "1.1.1", 35 | "tonejs-instrument-harp-mp3": "1.1.1", 36 | "tonejs-instrument-organ-mp3": "1.1.1", 37 | "tonejs-instrument-piano-mp3": "1.1.2", 38 | "tonejs-instrument-saxophone-mp3": "1.1.2", 39 | "tonejs-instrument-trombone-mp3": "1.1.2", 40 | "tonejs-instrument-trumpet-mp3": "1.1.2", 41 | "tonejs-instrument-tuba-mp3": "1.1.2", 42 | "tonejs-instrument-violin-mp3": "1.1.1", 43 | "tonejs-instrument-xylophone-mp3": "1.1.2", 44 | "uuid": "11.0.3" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/BVM-priv/CryptoChords/webapp" 49 | }, 50 | "devDependencies": { 51 | "@types/react": "18.2.43", 52 | "@types/react-dom": "18.2.17", 53 | "@types/uuid": "10.0.0", 54 | "@typescript-eslint/eslint-plugin": "8.18.2", 55 | "@typescript-eslint/parser": "8.18.2", 56 | "@vitejs/plugin-react": "4.3.4", 57 | "@vitest/coverage-istanbul": "1.2.2", 58 | "autoprefixer": "10.4.20", 59 | "eslint": "8.57.1", 60 | "eslint-config-bloq": "4.4.1", 61 | "eslint-plugin-react-hooks": "5.1.0", 62 | "eslint-plugin-react-refresh": "0.4.16", 63 | "eslint-plugin-sort-keys-fix": "^1.1.2", 64 | "postcss": "8.4.49", 65 | "prettier": "3.3.3", 66 | "tailwindcss": "3.4.16", 67 | "typescript": "5.2.2", 68 | "vite": "5.0.8", 69 | "vitest": "1.2.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {}, 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/public/image/close-button.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/image/crypto-chords-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/web/public/image/cube/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemilabs/CryptoChords/52e003c8c28ddfa99b3e363a439f73a1895722e3/apps/web/public/image/cube/blue.png -------------------------------------------------------------------------------- /apps/web/public/image/cube/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemilabs/CryptoChords/52e003c8c28ddfa99b3e363a439f73a1895722e3/apps/web/public/image/cube/green.png -------------------------------------------------------------------------------- /apps/web/public/image/cube/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemilabs/CryptoChords/52e003c8c28ddfa99b3e363a439f73a1895722e3/apps/web/public/image/cube/orange.png -------------------------------------------------------------------------------- /apps/web/public/image/cube/purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemilabs/CryptoChords/52e003c8c28ddfa99b3e363a439f73a1895722e3/apps/web/public/image/cube/purple.png -------------------------------------------------------------------------------- /apps/web/public/image/keyboard/base.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/image/nav-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/web/public/image/social/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/web/public/image/social/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/web/public/image/social/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/src/application/ObservableService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Observable, 3 | EventBusInstance, 4 | EventSubscription, 5 | Event, 6 | } from '@cryptochords/shared'; 7 | import { Service } from './Service'; 8 | import { ServiceResponseEvent } from './events/ServiceResponseEvent'; 9 | 10 | export abstract class ObservableService 11 | extends Service 12 | implements Observable> 13 | { 14 | private eventBus = new EventBusInstance(); 15 | 16 | public async execute(request: Request): Promise { 17 | const response = this.process(request); 18 | response.then((responseValue: Response) => { 19 | const event = new ServiceResponseEvent(request, responseValue); 20 | this.eventBus.publish(event); 21 | }); 22 | return response; 23 | } 24 | 25 | public async listen( 26 | listener: EventSubscription>, 27 | ) { 28 | this.eventBus.subscribe( 29 | ServiceResponseEvent.eventKey, 30 | listener as EventSubscription, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/application/Service.ts: -------------------------------------------------------------------------------- 1 | export abstract class Service { 2 | protected abstract process(request: Request): Promise; 3 | 4 | public async execute(request: Request): Promise { 5 | return this.process(request); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/application/events/ServiceResponseEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@cryptochords/shared'; 2 | 3 | export class ServiceResponseEvent extends Event { 4 | static eventKey = Symbol('ServiceResponseEvent'); 5 | constructor( 6 | public readonly request: Request, 7 | public readonly response: Response, 8 | ) { 9 | super(ServiceResponseEvent.eventKey); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateCube/CreateCubeDtos.ts: -------------------------------------------------------------------------------- 1 | export interface CreateCubeResponseDto { 2 | id: string; 3 | x: number; 4 | y: number; 5 | color: string; 6 | mirrored: boolean; 7 | } 8 | 9 | export interface CreateCubeRequestDto { 10 | x: number; 11 | color: string; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateCube/CreateCubeService.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 3 | import { CreateCubeService } from './CreateCubeService'; 4 | 5 | const cubeRepositoryMock: CubeRepository = { 6 | clear: vi.fn(), 7 | create: vi.fn().mockResolvedValue({ 8 | color: { value: 'blue' }, 9 | mirrored: false, 10 | uuid: { value: 'some-id' }, 11 | x: { value: 0.5 }, 12 | y: { value: 0 }, 13 | }), 14 | delete: vi.fn(), 15 | list: vi.fn(), 16 | update: vi.fn(), 17 | }; 18 | 19 | const create: CreateCubeService = new CreateCubeService(cubeRepositoryMock); 20 | 21 | describe('src/application/CreateCube/CreateCubeService', () => { 22 | describe('execute', () => { 23 | it('should return', async () => { 24 | const response = await create.execute({ color: 'blue', x: 0.5 }); 25 | expect(response).toEqual({ 26 | color: 'blue', 27 | id: 'some-id', 28 | mirrored: false, 29 | x: 0.5, 30 | y: 0, 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateCube/CreateCubeService.ts: -------------------------------------------------------------------------------- 1 | import { Cube } from '../../../domain/entities/Cube'; 2 | import { TransactionColorEnum } from '../../../domain/enum/TransactionColorEnum'; 3 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 4 | import { TransactionColor } from '../../../domain/valueObjects/TransactionColor'; 5 | import { UnitInterval } from '../../../domain/valueObjects/UnitInterval'; 6 | import { ObservableService } from '../../ObservableService'; 7 | import { CreateCubeRequestDto, CreateCubeResponseDto } from './CreateCubeDtos'; 8 | 9 | export class CreateCubeService extends ObservableService< 10 | CreateCubeRequestDto, 11 | CreateCubeResponseDto 12 | > { 13 | private readonly cubeRepository: CubeRepository; 14 | 15 | constructor(cubeRepository: CubeRepository) { 16 | super(); 17 | this.cubeRepository = cubeRepository; 18 | } 19 | 20 | protected async process( 21 | request: CreateCubeRequestDto, 22 | ): Promise { 23 | const cube = Cube.create( 24 | TransactionColor.create(request.color as TransactionColorEnum), 25 | UnitInterval.create(request.x), 26 | ); 27 | const createdCube = await this.cubeRepository.create(cube); 28 | return { 29 | color: createdCube.color.value, 30 | id: createdCube.uuid.value, 31 | mirrored: createdCube.mirrored, 32 | x: createdCube.x.value, 33 | y: createdCube.y.value, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateKeyboard/CreateKeyboardDtos.ts: -------------------------------------------------------------------------------- 1 | export interface CreateKeyboardRequestDto { 2 | numberOfKeys: number; 3 | initialPitchClass: string; 4 | initialOctave: number; 5 | } 6 | 7 | export interface CreateKeyboardResponseDto { 8 | keys: { 9 | pitch: { 10 | class: string; 11 | octave: number; 12 | }; 13 | keyShape: string; 14 | x: number; 15 | color?: string; 16 | }[]; 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateKeyboard/CreateKeyboardService.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it, vi } from 'vitest'; 2 | import { CreateKeyboardService } from './CreateKeyboardService'; 3 | import { KeyboardRepository } from '../../../domain/repositories/KeyboardRepository'; 4 | 5 | describe('src/application/CreateKeyboard/CreateKeyboardService', () => { 6 | let createKeyboardService: CreateKeyboardService; 7 | let keyboardRepository: KeyboardRepository; 8 | 9 | beforeAll(() => { 10 | keyboardRepository = { 11 | getKeyboard: vi.fn(), 12 | setKeyboard: vi.fn(), 13 | }; 14 | createKeyboardService = new CreateKeyboardService(keyboardRepository); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(createKeyboardService).toBeDefined(); 19 | }); 20 | 21 | it('should return a keyboard with 88 keys', async () => { 22 | const response = await createKeyboardService.execute({ 23 | initialOctave: 0, 24 | initialPitchClass: 'A', 25 | numberOfKeys: 88, 26 | }); 27 | expect(response.keys.length).toBe(88); 28 | }); 29 | 30 | it('should call setKeyboard', async () => { 31 | await createKeyboardService.execute({ 32 | initialOctave: 0, 33 | initialPitchClass: 'A', 34 | numberOfKeys: 88, 35 | }); 36 | expect(keyboardRepository.setKeyboard).toHaveBeenCalled(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateKeyboard/CreateKeyboardService.ts: -------------------------------------------------------------------------------- 1 | import { PitchClassEnum } from '../../../domain/enum/PitchClassEnum'; 2 | import { KeyboardFactory } from '../../../domain/factories/KeyboardFactory'; 3 | import { KeyboardRepository } from '../../../domain/repositories/KeyboardRepository'; 4 | import { ObservableService } from '../../ObservableService'; 5 | import { 6 | CreateKeyboardRequestDto, 7 | CreateKeyboardResponseDto, 8 | } from './CreateKeyboardDtos'; 9 | 10 | export class CreateKeyboardService extends ObservableService< 11 | CreateKeyboardRequestDto, 12 | CreateKeyboardResponseDto 13 | > { 14 | private keyboardRepository: KeyboardRepository; 15 | 16 | constructor(keyboardRepository: KeyboardRepository) { 17 | super(); 18 | this.keyboardRepository = keyboardRepository; 19 | } 20 | 21 | protected async process( 22 | request: CreateKeyboardRequestDto, 23 | ): Promise { 24 | const keyboard = KeyboardFactory.create({ 25 | initialOctave: request.initialOctave, 26 | initialPitchClass: request.initialPitchClass as PitchClassEnum, 27 | numberOfKeys: request.numberOfKeys, 28 | }); 29 | 30 | this.keyboardRepository.setKeyboard(keyboard); 31 | 32 | return { 33 | keys: keyboard.keys.map(key => ({ 34 | color: key.color, 35 | keyShape: key.keyShape.value, 36 | pitch: { 37 | class: key.pitch.pitchClass.value, 38 | octave: key.pitch.octave, 39 | }, 40 | x: key.x.value, 41 | })), 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/application/services/CreateTransaction/CreateTransactionDtos.ts: -------------------------------------------------------------------------------- 1 | export interface CreateTransactionRequest { 2 | txType: string; 3 | address: string; 4 | network: string; 5 | timestamp: number; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetCubes/GetCubesDtos.ts: -------------------------------------------------------------------------------- 1 | interface CubeDto { 2 | id: string; 3 | x: number; 4 | y: number; 5 | color: string; 6 | mirrored: boolean; 7 | age: number; 8 | } 9 | 10 | export interface GetCubesResponseDto { 11 | cubes: CubeDto[]; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetCubes/GetCubesService.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, expectTypeOf, it, vi } from 'vitest'; 2 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 3 | import { GetCubesService } from './GetCubesService'; 4 | import { GetCubesResponseDto } from './GetCubesDtos'; 5 | 6 | const cubeRepositoryMock: CubeRepository = { 7 | clear: vi.fn(), 8 | create: vi.fn(), 9 | delete: vi.fn(), 10 | list: vi.fn().mockResolvedValue([ 11 | { 12 | color: { value: 'blue' }, 13 | mirrored: false, 14 | uuid: { value: 'some-id' }, 15 | x: { value: 0.5 }, 16 | y: { value: 0 }, 17 | }, 18 | { 19 | color: { value: 'orange' }, 20 | mirrored: false, 21 | uuid: { value: 'another-id' }, 22 | x: { value: 0.1 }, 23 | y: { value: 0 }, 24 | }, 25 | ]), 26 | update: vi.fn(), 27 | }; 28 | 29 | const getCubes = new GetCubesService(cubeRepositoryMock); 30 | 31 | describe('src/application/CreateCube/GetCubesService', () => { 32 | describe('execute', async () => { 33 | const response: GetCubesResponseDto = await getCubes.execute(); 34 | 35 | it('should call list once', () => { 36 | expect(cubeRepositoryMock.list).toHaveBeenCalledOnce(); 37 | }); 38 | 39 | it('should return a GetCubesResponseDto', () => { 40 | expectTypeOf(response).toMatchTypeOf(); 41 | expect(response).toBeDefined(); 42 | }); 43 | 44 | it('should return the correct number of items', () => { 45 | expect(response.cubes.length).toEqual(2); 46 | }); 47 | 48 | it('should return the correct values', async () => { 49 | expect(response).toEqual({ 50 | cubes: [ 51 | { 52 | color: 'blue', 53 | id: 'some-id', 54 | mirrored: false, 55 | x: 0.5, 56 | y: 0, 57 | }, 58 | { 59 | color: 'orange', 60 | id: 'another-id', 61 | mirrored: false, 62 | x: 0.1, 63 | y: 0, 64 | }, 65 | ], 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetCubes/GetCubesService.ts: -------------------------------------------------------------------------------- 1 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { GetCubesResponseDto } from './GetCubesDtos'; 4 | 5 | export class GetCubesService extends ObservableService< 6 | void, 7 | GetCubesResponseDto 8 | > { 9 | private readonly cubeRepository: CubeRepository; 10 | 11 | constructor(cubeRepository: CubeRepository) { 12 | super(); 13 | this.cubeRepository = cubeRepository; 14 | } 15 | 16 | protected async process(): Promise { 17 | const cubes = await this.cubeRepository.list(); 18 | 19 | return { 20 | cubes: cubes.map(cube => ({ 21 | age: cube.age, 22 | color: cube.color.value, 23 | id: cube.uuid.value, 24 | mirrored: cube.mirrored, 25 | x: cube.x.value, 26 | y: cube.y.value, 27 | })), 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetKeyboard/GetKeyboardDtos.ts: -------------------------------------------------------------------------------- 1 | export interface GetKeyboardResponseDto { 2 | keys: { 3 | pitch: { 4 | class: string; 5 | octave: number; 6 | }; 7 | keyShape: string; 8 | x: number; 9 | color: string; 10 | pressed: boolean; 11 | }[]; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetKeyboard/GetKeyboardService.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardRepository } from '../../../domain/repositories/KeyboardRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { GetKeyboardResponseDto } from './GetKeyboardDtos'; 4 | 5 | export class GetKeyboardService extends ObservableService< 6 | void, 7 | GetKeyboardResponseDto 8 | > { 9 | private readonly keyboardRepository: KeyboardRepository; 10 | 11 | constructor(keyboardRepository: KeyboardRepository) { 12 | super(); 13 | this.keyboardRepository = keyboardRepository; 14 | } 15 | 16 | protected async process(): Promise { 17 | const keyboard = this.keyboardRepository.getKeyboard(); 18 | 19 | if (!keyboard) { 20 | return { 21 | keys: [], 22 | }; 23 | } 24 | 25 | return { 26 | keys: keyboard.keys.map(key => ({ 27 | color: key.color, 28 | keyShape: key.keyShape.value, 29 | pitch: { 30 | class: key.pitch.pitchClass.value, 31 | octave: key.pitch.octave, 32 | }, 33 | pressed: key.pressed, 34 | x: key.x.value, 35 | })), 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetOptions/GetOptionsDtos.ts: -------------------------------------------------------------------------------- 1 | export interface GetOptionsResponseDto { 2 | muted: boolean; 3 | instrument: string; 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetOptions/GetOptionsService.ts: -------------------------------------------------------------------------------- 1 | import { OptionsRepository } from '../../../domain/repositories/OptionsRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { GetOptionsResponseDto } from './GetOptionsDtos'; 4 | 5 | export class GetOptionsService extends ObservableService< 6 | void, 7 | GetOptionsResponseDto 8 | > { 9 | private readonly optionsRepository: OptionsRepository; 10 | 11 | constructor(optionsRepository: OptionsRepository) { 12 | super(); 13 | this.optionsRepository = optionsRepository; 14 | } 15 | 16 | protected async process(): Promise { 17 | const options = await this.optionsRepository.getOptions(); 18 | 19 | return { 20 | instrument: options.instrument.name, 21 | muted: options.muted, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetSelectedNetwork/GetSelectedNetworkResponseDto.ts: -------------------------------------------------------------------------------- 1 | export interface GetSelectedNetworkResponseDto { 2 | networkName: string; 3 | networkWsUrl: string; 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/src/application/services/GetSelectedNetwork/GetSelectedNetworkService.ts: -------------------------------------------------------------------------------- 1 | import { NetworkRepository } from '../../../domain/repositories/NetworkRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { GetSelectedNetworkResponseDto } from './GetSelectedNetworkResponseDto'; 4 | 5 | export class GetSelectedNetworkService extends ObservableService< 6 | void, 7 | GetSelectedNetworkResponseDto 8 | > { 9 | private readonly netwtorkRepository: NetworkRepository; 10 | 11 | constructor(networkRepository: NetworkRepository) { 12 | super(); 13 | this.netwtorkRepository = networkRepository; 14 | } 15 | 16 | protected async process(): Promise { 17 | const network = await this.netwtorkRepository.getSelected(); 18 | return { 19 | networkName: network.name, 20 | networkWsUrl: network.wsUrl, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ListNetworks/ListNetworksResponseDto.ts: -------------------------------------------------------------------------------- 1 | import { NetworkDto } from './NetworkDto'; 2 | 3 | export interface ListNetworksResponseDto { 4 | networks: NetworkDto[]; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ListNetworks/ListNetworksService.ts: -------------------------------------------------------------------------------- 1 | import { NetworkRepository } from '../../../domain/repositories/NetworkRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { ListNetworksResponseDto } from './ListNetworksResponseDto'; 4 | 5 | export class ListNetworksService extends ObservableService< 6 | void, 7 | ListNetworksResponseDto 8 | > { 9 | private readonly networkRepository: NetworkRepository; 10 | 11 | constructor(networkRepository: NetworkRepository) { 12 | super(); 13 | this.networkRepository = networkRepository; 14 | } 15 | 16 | protected async process(): Promise { 17 | const [networks, selectedNetwork] = await Promise.all([ 18 | this.networkRepository.list(), 19 | this.networkRepository.getSelected(), 20 | ]); 21 | 22 | return { 23 | networks: networks.map(network => ({ 24 | name: network.name, 25 | selected: network.name === selectedNetwork.name, 26 | wsUrl: network.wsUrl, 27 | })), 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ListNetworks/NetworkDto.ts: -------------------------------------------------------------------------------- 1 | export interface NetworkDto { 2 | name: string; 3 | wsUrl: string; 4 | selected: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ListTransactions/ListTransactionsDtos.ts: -------------------------------------------------------------------------------- 1 | interface TransactionDto { 2 | txType: string; 3 | address: string; 4 | timestamp: number; 5 | url: string; 6 | } 7 | 8 | export interface ListTransactionsResponseDto { 9 | transactions: TransactionDto[]; 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ListTransactions/ListTransactionsService.ts: -------------------------------------------------------------------------------- 1 | import { TransactionRepository } from '../../../domain/repositories/TransactionRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { ListTransactionsResponseDto } from './ListTransactionsDtos'; 4 | 5 | export class ListTransactionsService extends ObservableService< 6 | void, 7 | ListTransactionsResponseDto 8 | > { 9 | private readonly transactionRepository: TransactionRepository; 10 | 11 | constructor(transactionRepository: TransactionRepository) { 12 | super(); 13 | this.transactionRepository = transactionRepository; 14 | } 15 | 16 | protected async process(): Promise { 17 | const transactions = await this.transactionRepository.list(); 18 | 19 | return { 20 | transactions: transactions.map(transaction => ({ 21 | address: transaction.address.value, 22 | timestamp: transaction.timestamp.value, 23 | txType: transaction.txType.value as string, 24 | url: transaction.url, 25 | })), 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/application/services/LoadInstrument/LoadInstrumentDtos.ts: -------------------------------------------------------------------------------- 1 | export interface LoadInstrumentRequest { 2 | instrument: string; 3 | } 4 | 5 | export type LoadInstrumentResponse = void; 6 | -------------------------------------------------------------------------------- /apps/web/src/application/services/LoadInstrument/LoadInstrumentService.ts: -------------------------------------------------------------------------------- 1 | import { SoundService } from '../../../domain/services/SoundService'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { 4 | LoadInstrumentRequest, 5 | LoadInstrumentResponse, 6 | } from './LoadInstrumentDtos'; 7 | 8 | export class LoadInstrumentService extends ObservableService< 9 | LoadInstrumentRequest, 10 | LoadInstrumentResponse 11 | > { 12 | private soundService: SoundService; 13 | 14 | constructor(soundService: SoundService) { 15 | super(); 16 | this.soundService = soundService; 17 | } 18 | 19 | protected async process({ 20 | instrument, 21 | }: LoadInstrumentRequest): Promise { 22 | await this.soundService.loadInstrument(instrument); 23 | return; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/src/application/services/PlaySound/PlaySoundDtos.ts: -------------------------------------------------------------------------------- 1 | export interface PlaySoundRequest { 2 | pitchClass: string; 3 | octave: number; 4 | } 5 | 6 | export interface PlaySoundResponse { 7 | instrument?: string; 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/application/services/PlaySound/PlaySoundService.ts: -------------------------------------------------------------------------------- 1 | import { PitchClassEnum } from '../../../domain/enum/PitchClassEnum'; 2 | import { OptionsRepository } from '../../../domain/repositories/OptionsRepository'; 3 | import { SoundService } from '../../../domain/services/SoundService'; 4 | import { PitchClass } from '../../../domain/valueObjects/PitchClass'; 5 | import { ObservableService } from '../../ObservableService'; 6 | import { PlaySoundRequest, PlaySoundResponse } from './PlaySoundDtos'; 7 | 8 | export class PlaySoundService extends ObservableService< 9 | PlaySoundRequest, 10 | PlaySoundResponse 11 | > { 12 | private soundService: SoundService; 13 | private optionsRepository: OptionsRepository; 14 | 15 | constructor( 16 | soundService: SoundService, 17 | optionsRepository: OptionsRepository, 18 | ) { 19 | super(); 20 | this.soundService = soundService; 21 | this.optionsRepository = optionsRepository; 22 | } 23 | 24 | protected async process( 25 | request: PlaySoundRequest, 26 | ): Promise { 27 | const options = await this.optionsRepository.getOptions(); 28 | 29 | if (options.muted) { 30 | return {}; 31 | } 32 | 33 | const pitchClass = PitchClass.create(request.pitchClass as PitchClassEnum); 34 | await this.soundService.playSound( 35 | pitchClass.value, 36 | request.octave, 37 | options.instrument.name, 38 | ); 39 | 40 | return { instrument: options.instrument.name }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/src/application/services/PressKey/PressKeyDtos.ts: -------------------------------------------------------------------------------- 1 | export interface PressKeyRequest { 2 | pitchClass: string; 3 | octave: number; 4 | } 5 | 6 | export interface PressKeyResponse { 7 | instrument?: string; 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/application/services/PressKey/PressKeyService.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardRepository } from '../../../domain/repositories/KeyboardRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { PlaySoundService } from '../PlaySound/PlaySoundService'; 4 | import { PressKeyRequest, PressKeyResponse } from './PressKeyDtos'; 5 | 6 | export class PressKeyService extends ObservableService< 7 | PressKeyRequest, 8 | PressKeyResponse 9 | > { 10 | private keyboardRepository: KeyboardRepository; 11 | private playSound: PlaySoundService; 12 | 13 | constructor( 14 | keyboardRepository: KeyboardRepository, 15 | playSound: PlaySoundService, 16 | ) { 17 | super(); 18 | this.keyboardRepository = keyboardRepository; 19 | this.playSound = playSound; 20 | } 21 | 22 | protected async process(request: PressKeyRequest): Promise { 23 | const keyboard = this.keyboardRepository.getKeyboard(); 24 | if (!keyboard) return {}; 25 | 26 | const key = keyboard.findKey(request.pitchClass, request.octave); 27 | if (!key) return {}; 28 | 29 | key.press(); 30 | 31 | const { instrument } = await this.playSound.execute({ 32 | octave: request.octave, 33 | pitchClass: request.pitchClass, 34 | }); 35 | 36 | return { instrument }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/src/application/services/RecalculateCubePositions/RecalculateCubePositionsDtos.ts: -------------------------------------------------------------------------------- 1 | export interface RecalculateCubePositionsRequestDto { 2 | maxAge: number; 3 | } 4 | 5 | export interface RecalculateCubePositionsResponseDto { 6 | deletedCubes: number; 7 | updatedCubes: number; 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/application/services/RecalculateCubePositions/RecalculateCubePositionsService.ts: -------------------------------------------------------------------------------- 1 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 2 | import { ObservableService } from '../../ObservableService'; 3 | import { 4 | RecalculateCubePositionsRequestDto, 5 | RecalculateCubePositionsResponseDto, 6 | } from './RecalculateCubePositionsDtos'; 7 | 8 | export class RecalculateCubePositionsService extends ObservableService< 9 | RecalculateCubePositionsRequestDto, 10 | RecalculateCubePositionsResponseDto 11 | > { 12 | private readonly cubeRepository: CubeRepository; 13 | 14 | constructor(cubeRepository: CubeRepository) { 15 | super(); 16 | this.cubeRepository = cubeRepository; 17 | } 18 | 19 | protected async process( 20 | request: RecalculateCubePositionsRequestDto, 21 | ): Promise { 22 | const cubes = await this.cubeRepository.list(); 23 | const response: RecalculateCubePositionsResponseDto = { 24 | deletedCubes: 0, 25 | updatedCubes: 0, 26 | }; 27 | 28 | for await (const cube of cubes) { 29 | cube.recalculateYByAge(request.maxAge); 30 | if (cube.isOnTop) { 31 | await this.cubeRepository.delete(cube.uuid); 32 | response.deletedCubes++; 33 | } else { 34 | await this.cubeRepository.update(cube); 35 | response.updatedCubes++; 36 | } 37 | } 38 | 39 | return response; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ReleaseKey/ReleaseKeyDtos.ts: -------------------------------------------------------------------------------- 1 | export interface ReleaseKeyRequest { 2 | pitchClass: string; 3 | octave: number; 4 | instrument?: string; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/application/services/ReleaseKey/ReleaseKeyService.ts: -------------------------------------------------------------------------------- 1 | import { InstrumentEnum } from '../../../domain/enum/InstrumentEnum'; 2 | import { KeyboardRepository } from '../../../domain/repositories/KeyboardRepository'; 3 | import { ObservableService } from '../../ObservableService'; 4 | import { StopSoundService } from '../StopSound/StopSoundService'; 5 | import { ReleaseKeyRequest as ReleaseKeyRequest } from './ReleaseKeyDtos'; 6 | 7 | export class ReleaseKeyService extends ObservableService< 8 | ReleaseKeyRequest, 9 | void 10 | > { 11 | private keyboardRepository: KeyboardRepository; 12 | private stopSound: StopSoundService; 13 | 14 | constructor( 15 | keyboardRepository: KeyboardRepository, 16 | stopSound: StopSoundService, 17 | ) { 18 | super(); 19 | this.keyboardRepository = keyboardRepository; 20 | this.stopSound = stopSound; 21 | } 22 | 23 | protected async process(request: ReleaseKeyRequest): Promise { 24 | const keyboard = this.keyboardRepository.getKeyboard(); 25 | if (!keyboard) return; 26 | 27 | const key = keyboard.findKey(request.pitchClass, request.octave); 28 | if (!key) return; 29 | 30 | key.release(); 31 | 32 | if (!request.instrument) { 33 | return; 34 | } 35 | 36 | this.stopSound.execute({ 37 | instrument: request.instrument as InstrumentEnum, 38 | octave: request.octave, 39 | pitchClass: request.pitchClass, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SetInstrument/SetInstrumentDtos.ts: -------------------------------------------------------------------------------- 1 | export interface SetInstrumentRequest { 2 | instrument: string; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SetInstrument/SetInstrumentService.ts: -------------------------------------------------------------------------------- 1 | import { InstrumentEnum } from '../../../domain/enum/InstrumentEnum'; 2 | import { OptionsRepository } from '../../../domain/repositories/OptionsRepository'; 3 | import { Instrument } from '../../../domain/valueObjects/Instrument'; 4 | import { Options } from '../../../domain/valueObjects/Options'; 5 | import { ObservableService } from '../../ObservableService'; 6 | import { SetInstrumentRequest } from './SetInstrumentDtos'; 7 | 8 | export class SetInstrumentService extends ObservableService< 9 | SetInstrumentRequest, 10 | void 11 | > { 12 | private optionsRepository: OptionsRepository; 13 | 14 | constructor(optionsRepository: OptionsRepository) { 15 | super(); 16 | this.optionsRepository = optionsRepository; 17 | } 18 | 19 | protected async process(request: SetInstrumentRequest): Promise { 20 | const options = await this.optionsRepository.getOptions(); 21 | this.optionsRepository.setOptions( 22 | Options.create({ 23 | instrument: Instrument.create({ 24 | name: request.instrument as InstrumentEnum, 25 | }), 26 | muted: options.muted, 27 | }), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SetMuted/SetMutedDtos.ts: -------------------------------------------------------------------------------- 1 | export interface SetMutedRequest { 2 | muted: boolean; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SetMuted/SetMutedService.ts: -------------------------------------------------------------------------------- 1 | import { OptionsRepository } from '../../../domain/repositories/OptionsRepository'; 2 | import { Options } from '../../../domain/valueObjects/Options'; 3 | import { ObservableService } from '../../ObservableService'; 4 | import { SetMutedRequest } from './SetMutedDtos'; 5 | 6 | export class SetMutedService extends ObservableService { 7 | private optionsRepository: OptionsRepository; 8 | 9 | constructor(optionsRepository: OptionsRepository) { 10 | super(); 11 | this.optionsRepository = optionsRepository; 12 | } 13 | 14 | protected async process(request: SetMutedRequest): Promise { 15 | const options = await this.optionsRepository.getOptions(); 16 | this.optionsRepository.setOptions( 17 | Options.create({ 18 | instrument: options.instrument, 19 | muted: request.muted, 20 | }), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/application/services/StopSound/StopSoundDtos.ts: -------------------------------------------------------------------------------- 1 | export interface StopSoundRequest { 2 | pitchClass: string; 3 | octave: number; 4 | instrument: string; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/application/services/StopSound/StopSoundService.ts: -------------------------------------------------------------------------------- 1 | import { PitchClassEnum } from '../../../domain/enum/PitchClassEnum'; 2 | import { SoundService } from '../../../domain/services/SoundService'; 3 | import { PitchClass } from '../../../domain/valueObjects/PitchClass'; 4 | import { ObservableService } from '../../ObservableService'; 5 | import { StopSoundRequest } from './StopSoundDtos'; 6 | 7 | export class StopSoundService extends ObservableService< 8 | StopSoundRequest, 9 | void 10 | > { 11 | private soundService: SoundService; 12 | 13 | constructor(soundService: SoundService) { 14 | super(); 15 | this.soundService = soundService; 16 | } 17 | 18 | protected async process(request: StopSoundRequest): Promise { 19 | const pitchClass = PitchClass.create(request.pitchClass as PitchClassEnum); 20 | await this.soundService.stopSound( 21 | pitchClass.value, 22 | request.octave, 23 | request.instrument, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SwitchNetwork/SwitchNetworkRequestDto.ts: -------------------------------------------------------------------------------- 1 | export interface SwitchNetworkRequestDto { 2 | networkName: string; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SwitchNetwork/SwitchNetworkResponseDto.ts: -------------------------------------------------------------------------------- 1 | export interface SwitchNetworkResponseDto { 2 | networkName: string; 3 | networkWsUrl: string; 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/src/application/services/SwitchNetwork/SwitchNetworkService.ts: -------------------------------------------------------------------------------- 1 | import { NetworkEnum } from '@cryptochords/shared'; 2 | import { Network } from '../../../domain/entities/Network'; 3 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 4 | import { NetworkRepository } from '../../../domain/repositories/NetworkRepository'; 5 | import { TransactionRepository } from '../../../domain/repositories/TransactionRepository'; 6 | import { ObservableService } from '../../ObservableService'; 7 | import { SwitchNetworkRequestDto } from './SwitchNetworkRequestDto'; 8 | import { SwitchNetworkResponseDto } from './SwitchNetworkResponseDto'; 9 | 10 | export class SwitchNetworkService extends ObservableService< 11 | SwitchNetworkRequestDto, 12 | SwitchNetworkResponseDto 13 | > { 14 | private readonly transactionRepository: TransactionRepository; 15 | private readonly networkRepository: NetworkRepository; 16 | private readonly cubeRepository: CubeRepository; 17 | 18 | constructor( 19 | transactionRepository: TransactionRepository, 20 | networkRepository: NetworkRepository, 21 | cubeRepository: CubeRepository, 22 | ) { 23 | super(); 24 | this.transactionRepository = transactionRepository; 25 | this.networkRepository = networkRepository; 26 | this.cubeRepository = cubeRepository; 27 | } 28 | 29 | protected async process( 30 | request: SwitchNetworkRequestDto, 31 | ): Promise { 32 | const network = await this.validateNetwork(request.networkName); 33 | await Promise.all([ 34 | this.transactionRepository.clear(), 35 | this.cubeRepository.clear(), 36 | this.networkRepository.select(network.name), 37 | ]); 38 | 39 | return { 40 | networkName: network.name, 41 | networkWsUrl: network.wsUrl, 42 | }; 43 | } 44 | 45 | private async validateNetwork(networkName: string): Promise { 46 | if (!networkName) { 47 | throw new Error('Network not found'); 48 | } 49 | 50 | const network = await this.networkRepository.find( 51 | networkName as NetworkEnum, 52 | ); 53 | 54 | if (!network) { 55 | throw new Error('Network not found'); 56 | } 57 | 58 | return network; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/src/domain/entities/Network.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Network } from './Network'; 3 | import { NetworkEnum } from '../../../../../packages/shared/src/domain/enums/NetworkEnum'; 4 | import { Uuid } from '../../../../../packages/shared/src/domain/valueObjects/Uuid'; 5 | 6 | describe('Network', () => { 7 | const mockProps = { 8 | explorerUrl: 'https://explorer.mainnet.com', 9 | name: NetworkEnum.MAINNET, 10 | wsUrl: 'wss://mainnet.ws.com', 11 | }; 12 | 13 | it('should create a Network instance with default UUID', () => { 14 | const network = Network.create(mockProps); 15 | expect(network).toBeInstanceOf(Network); 16 | expect(network.name).toBe(NetworkEnum.MAINNET); 17 | expect(network.explorerUrl).toBe(mockProps.explorerUrl); 18 | expect(network.wsUrl).toBe(mockProps.wsUrl); 19 | expect(network.transactionUrl).toBe(`${mockProps.explorerUrl}/tx`); 20 | expect(network.blockUrl).toBe(`${mockProps.explorerUrl}/block`); 21 | }); 22 | 23 | it('should create a Network instance with a provided UUID', () => { 24 | const uuid = Uuid.create(); 25 | const network = Network.create(mockProps, uuid); 26 | expect(network).toBeInstanceOf(Network); 27 | expect(network.name).toBe(NetworkEnum.MAINNET); 28 | expect(network.explorerUrl).toBe(mockProps.explorerUrl); 29 | expect(network.wsUrl).toBe(mockProps.wsUrl); 30 | expect(network.transactionUrl).toBe(`${mockProps.explorerUrl}/tx`); 31 | expect(network.blockUrl).toBe(`${mockProps.explorerUrl}/block`); 32 | }); 33 | 34 | it('should return correct URLs for transactions and blocks', () => { 35 | const network = Network.create(mockProps); 36 | expect(network.transactionUrl).toBe(`${mockProps.explorerUrl}/tx`); 37 | expect(network.blockUrl).toBe(`${mockProps.explorerUrl}/block`); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /apps/web/src/domain/entities/Network.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../../../../packages/shared/src/domain/base/Entity'; 2 | import { NetworkEnum } from '../../../../../packages/shared/src/domain/enums/NetworkEnum'; 3 | import { Uuid } from '../../../../../packages/shared/src/domain/valueObjects/Uuid'; 4 | 5 | export interface NetworkProps { 6 | name: NetworkEnum; 7 | explorerUrl: string; 8 | wsUrl: string; 9 | } 10 | 11 | export class Network extends Entity { 12 | private constructor(props: NetworkProps, uuid: Uuid) { 13 | super(props, uuid); 14 | } 15 | 16 | static create(props: NetworkProps, id?: Uuid) { 17 | const uuid = id ?? Uuid.create(); 18 | return new Network(props, uuid); 19 | } 20 | 21 | get name() { 22 | return this.props.name; 23 | } 24 | 25 | get explorerUrl() { 26 | return this.props.explorerUrl; 27 | } 28 | 29 | get transactionUrl() { 30 | return `${this.explorerUrl}/tx`; 31 | } 32 | 33 | get blockUrl() { 34 | return `${this.explorerUrl}/block`; 35 | } 36 | 37 | get addressUrl() { 38 | return `${this.explorerUrl}/address`; 39 | } 40 | 41 | get wsUrl() { 42 | return this.props.wsUrl; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/domain/entities/Transaction.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Transaction } from './Transaction'; 3 | import { NetworkEnum, TxType } from '@cryptochords/shared'; 4 | import { Network } from './Network'; 5 | import { Address } from '@cryptochords/shared'; 6 | import { Timestamp } from '@cryptochords/shared'; 7 | 8 | describe('Transaction', () => { 9 | const mockNetwork = Network.create({ 10 | explorerUrl: 'https://explorer.mainnet.com', 11 | name: NetworkEnum.MAINNET, 12 | wsUrl: 'wss://mainnet.ws.com', 13 | }); 14 | 15 | const mockAddress = { value: '0x1234567890abcdef' } as Address; 16 | const mockTimestamp = Timestamp.create(new Date().getTime()); 17 | 18 | it('should create a Transaction instance', () => { 19 | const txType = { isBlock: false } as TxType; 20 | 21 | const transaction = Transaction.create({ 22 | address: mockAddress, 23 | network: mockNetwork, 24 | timestamp: mockTimestamp, 25 | txType, 26 | }); 27 | 28 | expect(transaction).toBeInstanceOf(Transaction); 29 | expect(transaction.txType).toEqual(txType); 30 | expect(transaction.address).toEqual(mockAddress); 31 | expect(transaction.network).toEqual(mockNetwork); 32 | expect(transaction.timestamp).toEqual(mockTimestamp); 33 | }); 34 | 35 | it('should return correct block URL when txType.isBlock is true', () => { 36 | const txType = { isBlock: true } as TxType; 37 | 38 | const transaction = Transaction.create({ 39 | address: mockAddress, 40 | network: mockNetwork, 41 | timestamp: mockTimestamp, 42 | txType, 43 | }); 44 | 45 | const expectedUrl = `${mockNetwork.blockUrl}/${mockAddress.value}`; 46 | expect(transaction.url).toBe(expectedUrl); 47 | }); 48 | 49 | it('should return correct transaction URL when txType.isBlock is false', () => { 50 | const txType = { isBlock: false } as TxType; 51 | 52 | const transaction = Transaction.create({ 53 | address: mockAddress, 54 | network: mockNetwork, 55 | timestamp: mockTimestamp, 56 | txType, 57 | }); 58 | 59 | const expectedUrl = `${mockNetwork.addressUrl}/${mockAddress.value}`; 60 | expect(transaction.url).toBe(expectedUrl); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /apps/web/src/domain/entities/Transaction.ts: -------------------------------------------------------------------------------- 1 | import { Address, Timestamp, TxType } from '@cryptochords/shared'; 2 | import { Network } from './Network'; 3 | 4 | export interface TransactionProps { 5 | txType: TxType; 6 | address: Address; 7 | network: Network; 8 | timestamp: Timestamp; 9 | } 10 | 11 | export class Transaction { 12 | private props: TransactionProps; 13 | 14 | private constructor(props: TransactionProps) { 15 | this.props = props; 16 | } 17 | 18 | static create(props: TransactionProps) { 19 | return new Transaction(props); 20 | } 21 | 22 | get txType() { 23 | return this.props.txType; 24 | } 25 | 26 | get address() { 27 | return this.props.address; 28 | } 29 | 30 | get network() { 31 | return this.props.network; 32 | } 33 | 34 | get timestamp() { 35 | return this.props.timestamp; 36 | } 37 | 38 | get url() { 39 | if (this.props.txType.isBlock) { 40 | return `${this.props.network.blockUrl}/${this.props.address.value}`; 41 | } 42 | 43 | return `${this.props.network.addressUrl}/${this.props.address.value}`; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/src/domain/enum/InstrumentEnum.ts: -------------------------------------------------------------------------------- 1 | export enum InstrumentEnum { 2 | BassElectric = 'bass-electric', 3 | Bassoon = 'bassoon', 4 | Clarinet = 'clarinet', 5 | Contrabass = 'contrabass', 6 | Flute = 'flute', 7 | FrenchHorn = 'french-horn', 8 | GuitarAcoustic = 'guitar-acoustic', 9 | GuitarElectric = 'guitar-electric', 10 | GuitarNylon = 'guitar-nylon', 11 | Harmonium = 'harmonium', 12 | Harp = 'harp', 13 | Organ = 'organ', 14 | Piano = 'piano', 15 | Saxophone = 'saxophone', 16 | Trombone = 'trombone', 17 | Trumpet = 'trumpet', 18 | Tuba = 'tuba', 19 | Violin = 'violin', 20 | Xylophone = 'xylophone', 21 | } 22 | 23 | export const instrumentLabels: Map = new Map([ 24 | [InstrumentEnum.BassElectric, 'Bass Electric'], 25 | [InstrumentEnum.Bassoon, 'Bassoon'], 26 | [InstrumentEnum.Clarinet, 'Clarinet'], 27 | [InstrumentEnum.Contrabass, 'Contrabass'], 28 | [InstrumentEnum.Flute, 'Flute'], 29 | [InstrumentEnum.FrenchHorn, 'French Horn'], 30 | [InstrumentEnum.GuitarAcoustic, 'Guitar Acoustic'], 31 | [InstrumentEnum.GuitarElectric, 'Guitar Electric'], 32 | [InstrumentEnum.GuitarNylon, 'Guitar Nylon'], 33 | [InstrumentEnum.Harmonium, 'Harmonium'], 34 | [InstrumentEnum.Harp, 'Harp'], 35 | [InstrumentEnum.Organ, 'Organ'], 36 | [InstrumentEnum.Piano, 'Piano'], 37 | [InstrumentEnum.Saxophone, 'Saxophone'], 38 | [InstrumentEnum.Trombone, 'Trombone'], 39 | [InstrumentEnum.Trumpet, 'Trumpet'], 40 | [InstrumentEnum.Tuba, 'Tuba'], 41 | [InstrumentEnum.Violin, 'Violin'], 42 | [InstrumentEnum.Xylophone, 'Xylophone'], 43 | ]); 44 | -------------------------------------------------------------------------------- /apps/web/src/domain/enum/KeyShapeEnum.ts: -------------------------------------------------------------------------------- 1 | export enum KeyShapeEnum { 2 | Black = 'black', 3 | White = 'white', 4 | WhiteMiddle = 'white-middle', 5 | WhiteLeft = 'white-left', 6 | WhiteRight = 'white-right', 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/domain/enum/PitchClassEnum.ts: -------------------------------------------------------------------------------- 1 | export enum PitchClassEnum { 2 | C = 'C', 3 | C_SHARP = 'C#', 4 | D = 'D', 5 | D_SHARP = 'D#', 6 | E = 'E', 7 | F = 'F', 8 | F_SHARP = 'F#', 9 | G = 'G', 10 | G_SHARP = 'G#', 11 | A = 'A', 12 | A_SHARP = 'A#', 13 | B = 'B', 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/domain/enum/TransactionColorEnum.ts: -------------------------------------------------------------------------------- 1 | export enum TransactionColorEnum { 2 | Orange = 'orange', 3 | Blue = 'blue', 4 | Purple = 'purple', 5 | Green = 'green', 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/domain/errors/InvalidCubePositionError.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from '@cryptochords/shared'; 2 | 3 | export class InvalidCubePositionError extends DomainError { 4 | constructor() { 5 | super('INVALID_CUBE_POSITION', true); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/domain/errors/InvalidIdPitchClassError.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from '@cryptochords/shared'; 2 | 3 | export class InvalidPitchClassError extends DomainError { 4 | constructor() { 5 | super('INVALID_PITCH_CLASS', false); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/domain/errors/InvalidUnitIntervalError.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from '@cryptochords/shared'; 2 | 3 | export class InvalidUnitIntervalError extends DomainError { 4 | constructor() { 5 | super('INVALID_UNIT_INTERVAL', true); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/domain/events/TransactionCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@cryptochords/shared'; 2 | import { Transaction } from '../entities/Transaction'; 3 | 4 | export class TransactionCreatedEvent extends Event { 5 | static eventKey: symbol = Symbol('TransactionCreatedEvent'); 6 | 7 | private readonly transaction: Transaction; 8 | 9 | constructor(transaction: Transaction) { 10 | super(TransactionCreatedEvent.eventKey); 11 | this.transaction = transaction; 12 | } 13 | 14 | static create(transaction: Transaction): TransactionCreatedEvent { 15 | return new TransactionCreatedEvent(transaction); 16 | } 17 | 18 | getTransaction(): Transaction { 19 | return this.transaction; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/domain/repositories/CubeRepository.ts: -------------------------------------------------------------------------------- 1 | import { Uuid } from '@cryptochords/shared'; 2 | import { Cube } from '../entities/Cube'; 3 | 4 | export interface CubeRepository { 5 | list: () => Promise; 6 | create: (cube: Cube) => Promise; 7 | update: (cube: Cube) => Promise; 8 | delete: (id: Uuid) => Promise; 9 | clear: () => Promise; 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/domain/repositories/KeyboardRepository.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard } from '../valueObjects/Keyboard'; 2 | 3 | export interface KeyboardRepository { 4 | setKeyboard: (keyboard: Keyboard) => void; 5 | getKeyboard: () => Keyboard | undefined; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/domain/repositories/NetworkRepository.ts: -------------------------------------------------------------------------------- 1 | import { NetworkEnum } from '@cryptochords/shared'; 2 | import { Network } from '../entities/Network'; 3 | 4 | export interface NetworkRepository { 5 | list: () => Promise; 6 | find: (name: NetworkEnum) => Promise; 7 | select: (name: NetworkEnum) => Promise; 8 | getSelected: () => Promise; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/domain/repositories/OptionsRepository.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../valueObjects/Options'; 2 | 3 | export interface OptionsRepository { 4 | setOptions: (options: Options) => Promise; 5 | getOptions: () => Promise; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/domain/repositories/TransactionRepository.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from '../entities/Transaction'; 2 | 3 | export interface TransactionRepository { 4 | create(block: Transaction): Promise; 5 | list(): Promise; 6 | clear(): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/domain/services/SoundService.ts: -------------------------------------------------------------------------------- 1 | import { PitchClassEnum } from '../enum/PitchClassEnum'; 2 | 3 | export interface SoundService { 4 | loadInstrument(instrument: string): Promise; 5 | playSound( 6 | pitch: PitchClassEnum, 7 | octave: number, 8 | instrument: string, 9 | ): Promise; 10 | stopSound( 11 | pitch: PitchClassEnum, 12 | octave: number, 13 | instrument: string, 14 | ): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/Instrument.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { InstrumentEnum } from '../enum/InstrumentEnum'; 3 | 4 | export interface InstrumentProps { 5 | name: InstrumentEnum; 6 | } 7 | 8 | export class Instrument extends ValueObject { 9 | private constructor(props: InstrumentProps) { 10 | super(props); 11 | } 12 | 13 | static create(props: InstrumentProps) { 14 | return new Instrument(props); 15 | } 16 | 17 | get name() { 18 | return this.props.name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/Key.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { KeyShape } from './KeyShape'; 3 | import { Pitch } from './Pitch'; 4 | import { UnitInterval } from './UnitInterval'; 5 | 6 | export interface KeyProps { 7 | keyShape: KeyShape; 8 | pitch: Pitch; 9 | x: UnitInterval; 10 | color: string; 11 | pressed: boolean; 12 | } 13 | 14 | export class Key extends ValueObject { 15 | private constructor(props: KeyProps) { 16 | super(props); 17 | } 18 | 19 | static create(props: KeyProps) { 20 | return new Key(props); 21 | } 22 | 23 | press() { 24 | this.props.pressed = true; 25 | } 26 | 27 | release() { 28 | this.props.pressed = false; 29 | } 30 | 31 | get keyShape() { 32 | return this.props.keyShape; 33 | } 34 | 35 | get pitch() { 36 | return this.props.pitch; 37 | } 38 | 39 | get x() { 40 | return this.props.x; 41 | } 42 | 43 | get color() { 44 | return this.props.color; 45 | } 46 | 47 | get pressed() { 48 | return this.props.pressed; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/KeyShape.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { KeyShapeEnum } from '../enum/KeyShapeEnum'; 3 | 4 | interface KeyShapeProps { 5 | value: KeyShapeEnum; 6 | } 7 | 8 | export class KeyShape extends ValueObject { 9 | private constructor(shape: KeyShapeEnum) { 10 | super({ value: shape }); 11 | } 12 | 13 | static create(shape: KeyShapeEnum) { 14 | return new KeyShape(shape); 15 | } 16 | 17 | get value() { 18 | return this.props.value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/Keyboard.ts: -------------------------------------------------------------------------------- 1 | import { TxTypesEnum, ValueObject } from '@cryptochords/shared'; 2 | import { Key } from './Key'; 3 | import { KeyShapeEnum } from '../enum/KeyShapeEnum'; 4 | 5 | export interface KeyboardProps { 6 | keys: Key[]; 7 | } 8 | 9 | export class Keyboard extends ValueObject { 10 | private constructor(props: KeyboardProps) { 11 | super(props); 12 | } 13 | 14 | static create(props: KeyboardProps) { 15 | return new Keyboard(props); 16 | } 17 | 18 | get keys() { 19 | return this.props.keys; 20 | } 21 | 22 | findKey(pitchClass: string, octave: number): Key | undefined { 23 | return this.props.keys.find( 24 | key => 25 | key.pitch.pitchClass.value === pitchClass && 26 | key.pitch.octave === octave, 27 | ); 28 | } 29 | 30 | getRandomWhiteKeyByTxType(txType: TxTypesEnum): Key { 31 | if (txType === TxTypesEnum.Btc) 32 | return this.getRandomWhiteKey(0, Math.floor(this.props.keys.length / 4)); 33 | if (txType === TxTypesEnum.Eth) 34 | return this.getRandomWhiteKey( 35 | Math.floor(this.props.keys.length / 4), 36 | Math.floor(this.props.keys.length / 2), 37 | ); 38 | if (txType === TxTypesEnum.Pop) 39 | return this.getRandomWhiteKey( 40 | Math.floor(this.props.keys.length / 2), 41 | Math.floor((this.props.keys.length * 3) / 4), 42 | ); 43 | return this.getRandomWhiteKey( 44 | Math.floor((this.props.keys.length * 3) / 4), 45 | this.props.keys.length, 46 | ); 47 | } 48 | 49 | getRandomKey(min: number, max: number): Key { 50 | const randomIndex = Math.floor(Math.random() * (max - min)) + min; 51 | return this.props.keys[randomIndex]; 52 | } 53 | 54 | getRandomWhiteKey(min: number, max: number): Key { 55 | const keysInTheRange = this.props.keys.slice(min, max); 56 | const whiteKeys = keysInTheRange.filter( 57 | key => key.keyShape.value !== KeyShapeEnum.Black, 58 | ); 59 | const randomIndex = Math.floor(Math.random() * whiteKeys.length); 60 | return whiteKeys[randomIndex]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/Options.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { Instrument } from './Instrument'; 3 | import { InstrumentEnum } from '../enum/InstrumentEnum'; 4 | 5 | export interface OptionsProps { 6 | muted: boolean; 7 | instrument: Instrument; 8 | } 9 | 10 | export interface OptionsJSON { 11 | muted: boolean; 12 | instrument: string; 13 | } 14 | 15 | export class Options extends ValueObject { 16 | private constructor(props: OptionsProps) { 17 | super(props); 18 | } 19 | 20 | static create(props: OptionsProps) { 21 | return new Options(props); 22 | } 23 | 24 | get muted() { 25 | return this.props.muted; 26 | } 27 | 28 | get instrument() { 29 | return this.props.instrument; 30 | } 31 | 32 | toJSON(): OptionsJSON { 33 | return { 34 | instrument: this.props.instrument.name, 35 | muted: this.props.muted, 36 | }; 37 | } 38 | 39 | static fromJSON(json: OptionsJSON) { 40 | return Options.create({ 41 | instrument: Instrument.create({ 42 | name: json.instrument as InstrumentEnum, 43 | }), 44 | muted: json.muted, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/Pitch.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { PitchClass } from './PitchClass'; 3 | 4 | export interface PitchProps { 5 | pitchClass: PitchClass; 6 | octave: number; 7 | } 8 | 9 | export class Pitch extends ValueObject { 10 | private constructor(props: PitchProps) { 11 | super(props); 12 | } 13 | 14 | static create(props: PitchProps) { 15 | return new Pitch(props); 16 | } 17 | 18 | get pitchClass() { 19 | return this.props.pitchClass; 20 | } 21 | 22 | get octave() { 23 | return this.props.octave; 24 | } 25 | 26 | get next() { 27 | const nextClass = this.props.pitchClass.next; 28 | return Pitch.create({ 29 | octave: 30 | nextClass.value === 'C' ? this.props.octave + 1 : this.props.octave, 31 | pitchClass: nextClass, 32 | }); 33 | } 34 | 35 | get previous() { 36 | const previousClass = this.props.pitchClass.previous; 37 | return Pitch.create({ 38 | octave: 39 | previousClass.value === 'B' ? this.props.octave - 1 : this.props.octave, 40 | pitchClass: previousClass, 41 | }); 42 | } 43 | 44 | isBefore(pitch: Pitch) { 45 | return ( 46 | this.props.octave < pitch.octave || 47 | (this.props.octave === pitch.octave && 48 | this.props.pitchClass.value < pitch.pitchClass.value) 49 | ); 50 | } 51 | 52 | isAfter(pitch: Pitch) { 53 | return ( 54 | this.props.octave > pitch.octave || 55 | (this.props.octave === pitch.octave && 56 | this.props.pitchClass.value > pitch.pitchClass.value) 57 | ); 58 | } 59 | 60 | equals(pitch: Pitch) { 61 | return ( 62 | this.props.octave === pitch.octave && 63 | this.props.pitchClass.value === pitch.pitchClass.value 64 | ); 65 | } 66 | 67 | toString() { 68 | return `${this.props.pitchClass.value}${this.props.octave}`; 69 | } 70 | 71 | static random(min: Pitch, max: Pitch) { 72 | const range = Pitch.range(min, max); 73 | return range[Math.floor(Math.random() * range.length)]; 74 | } 75 | 76 | static range(min: Pitch, max: Pitch): Pitch[] { 77 | if (min.isAfter(max)) { 78 | throw new Error('Min pitch must be before max pitch'); 79 | } 80 | 81 | const possibleValues: Pitch[] = []; 82 | let current = min; 83 | while (!current.isAfter(max)) { 84 | possibleValues.push(current); 85 | current = current.next; 86 | } 87 | 88 | return possibleValues; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/PitchClass.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { PitchClass } from './PitchClass'; 3 | import { InvalidPitchClassError } from '../errors/InvalidIdPitchClassError'; 4 | import { PitchClassEnum } from '../enum/PitchClassEnum'; 5 | 6 | describe('src/domain/valueObjects/PitchClass', () => { 7 | it('should be defined', () => { 8 | expect(PitchClass).toBeDefined(); 9 | }); 10 | 11 | it('should return the correct value', () => { 12 | const pitchClass = PitchClass.create(PitchClassEnum.A); 13 | expect(pitchClass.value).toBe(PitchClassEnum.A); 14 | }); 15 | 16 | it('should throw an error when the value is invalid', () => { 17 | expect(() => { 18 | PitchClass.create('Z' as PitchClassEnum); 19 | }).toThrow(InvalidPitchClassError); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/PitchClass.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { PitchClassEnum } from '../enum/PitchClassEnum'; 3 | import { InvalidPitchClassError } from '../errors/InvalidIdPitchClassError'; 4 | 5 | export interface PitchClassProps { 6 | value: PitchClassEnum; 7 | } 8 | 9 | export class PitchClass extends ValueObject { 10 | private constructor(props: PitchClassProps) { 11 | super(props); 12 | } 13 | 14 | static create(value: PitchClassEnum) { 15 | if (!PitchClass.isValidValue(value)) { 16 | throw new InvalidPitchClassError(); 17 | } 18 | return new PitchClass({ value }); 19 | } 20 | 21 | private static isValidValue(value: PitchClassEnum) { 22 | return Object.values(PitchClassEnum).includes(value); 23 | } 24 | 25 | get value() { 26 | return this.props.value; 27 | } 28 | 29 | get next() { 30 | const nextIndex = 31 | (Object.values(PitchClassEnum).indexOf(this.props.value) + 1) % 32 | Object.values(PitchClassEnum).length; 33 | return PitchClass.create(Object.values(PitchClassEnum)[nextIndex]); 34 | } 35 | 36 | get previous() { 37 | const previousIndex = 38 | (Object.values(PitchClassEnum).indexOf(this.props.value) - 39 | 1 + 40 | Object.values(PitchClassEnum).length) % 41 | Object.values(PitchClassEnum).length; 42 | return PitchClass.create(Object.values(PitchClassEnum)[previousIndex]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/TransactionColor.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { TransactionColor } from './TransactionColor'; 3 | import { TransactionColorEnum } from '../enum/TransactionColorEnum'; 4 | 5 | describe('src/domain/valueObjects/CubeColor', () => { 6 | it('should be defined', () => { 7 | expect(TransactionColor).toBeDefined(); 8 | }); 9 | 10 | describe('create', () => { 11 | describe('when a valid color is provided as parameter', () => { 12 | it('should set the valid color in the value props', () => { 13 | const color = TransactionColor.create(TransactionColorEnum.Orange); 14 | expect(color.value).toBe('orange'); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('random', () => { 20 | it('should return a random color', () => { 21 | const color = TransactionColor.random(); 22 | expect(color.value).toBeDefined(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/TransactionColor.ts: -------------------------------------------------------------------------------- 1 | import { TxTypesEnum, ValueObject } from '@cryptochords/shared'; 2 | import { TransactionColorEnum } from '../enum/TransactionColorEnum'; 3 | 4 | interface TransactionColorProps { 5 | value: TransactionColorEnum; 6 | } 7 | 8 | export class TransactionColor extends ValueObject { 9 | private constructor(color: TransactionColorEnum) { 10 | super({ value: color }); 11 | } 12 | 13 | static createByTxType(txType: string) { 14 | if (txType === TxTypesEnum.Btc) 15 | return this.create(TransactionColorEnum.Orange); 16 | 17 | if (txType === TxTypesEnum.Eth) 18 | return this.create(TransactionColorEnum.Blue); 19 | 20 | if (txType === TxTypesEnum.Pop) 21 | return this.create(TransactionColorEnum.Purple); 22 | 23 | return this.create(TransactionColorEnum.Green); 24 | } 25 | 26 | static create(color: TransactionColorEnum) { 27 | return new TransactionColor(color); 28 | } 29 | 30 | static random() { 31 | return this.create(TransactionColor.randomColorValue()); 32 | } 33 | 34 | private static randomColorValue(): TransactionColorEnum { 35 | const enumValues = Object.values(TransactionColorEnum); 36 | const randomIndex = Math.floor(Math.random() * enumValues.length); 37 | const randomEnumValue = enumValues[randomIndex]; 38 | return randomEnumValue as TransactionColorEnum; 39 | } 40 | 41 | get value() { 42 | return this.props.value; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/UnitInterval.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { UnitInterval } from './UnitInterval'; 3 | 4 | describe('src/domain/valueObjects/UnitInterval', () => { 5 | it('should be defined', () => { 6 | expect(UnitInterval).toBeDefined(); 7 | }); 8 | 9 | it('should throw an error when a value greater than 1 is provided', () => { 10 | const test = () => { 11 | UnitInterval.create(1.1); 12 | }; 13 | 14 | expect(test).toThrowError(); 15 | }); 16 | 17 | it('should throw an error when a value less than 0 is provided', () => { 18 | const test = () => { 19 | UnitInterval.create(-0.1); 20 | }; 21 | 22 | expect(test).toThrowError(); 23 | }); 24 | 25 | it('Should return a value between 0 and 1', () => { 26 | const unitInterval = UnitInterval.random(); 27 | 28 | expect(unitInterval.value).toBeGreaterThanOrEqual(0); 29 | expect(unitInterval.value).toBeLessThanOrEqual(1); 30 | }); 31 | 32 | it('Should return a value between 0 and 0.25', () => { 33 | const unitInterval = UnitInterval.random(0, 0.25); 34 | 35 | expect(unitInterval.value).toBeGreaterThanOrEqual(0); 36 | expect(unitInterval.value).toBeLessThanOrEqual(0.25); 37 | }); 38 | 39 | it('Should return a value between 0.25 and 0.26', () => { 40 | const unitInterval = UnitInterval.random(0.25, 0.26); 41 | 42 | expect(unitInterval.value).toBeGreaterThanOrEqual(0.25); 43 | expect(unitInterval.value).toBeLessThanOrEqual(0.26); 44 | }); 45 | 46 | it('Should return a value less than than 0.01 or equal', () => { 47 | const unitInterval = UnitInterval.random(undefined, 0.01); 48 | 49 | expect(unitInterval.value).toBeGreaterThanOrEqual(0); 50 | expect(unitInterval.value).toBeLessThanOrEqual(0.01); 51 | }); 52 | 53 | it('Should return a value greater than than 0.9 or equal', () => { 54 | const unitInterval = UnitInterval.random(0.9); 55 | 56 | expect(unitInterval.value).toBeGreaterThanOrEqual(0.9); 57 | expect(unitInterval.value).toBeLessThanOrEqual(1); 58 | }); 59 | 60 | it('Should increment the value correctly', () => { 61 | const unitInterval = UnitInterval.create(0.5); 62 | 63 | const incremented = unitInterval.increment(0.1); 64 | 65 | expect(incremented.value).toBe(0.6); 66 | }); 67 | 68 | it('Should not increment the value greater than 1', () => { 69 | const unitInterval = UnitInterval.create(0.9); 70 | 71 | const incremented = unitInterval.increment(0.2); 72 | 73 | expect(incremented.value).toBe(1); 74 | }); 75 | 76 | it('Should return true when the value is 1', () => { 77 | const unitInterval = UnitInterval.create(1); 78 | 79 | expect(unitInterval.isMaxReached()).toBe(true); 80 | }); 81 | 82 | it('Should return false when the value is less than 1', () => { 83 | const unitInterval = UnitInterval.create(0.9); 84 | 85 | expect(unitInterval.isMaxReached()).toBe(false); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/UnitInterval.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { InvalidUnitIntervalError } from '../errors/InvalidUnitIntervalError'; 3 | 4 | interface UnitIntevalProps { 5 | value: number; 6 | } 7 | 8 | export class UnitInterval extends ValueObject { 9 | private constructor(value: number) { 10 | super({ value }); 11 | } 12 | 13 | static create(value: number) { 14 | if (typeof value !== 'number' || value < 0 || value > 1) { 15 | throw new InvalidUnitIntervalError(); 16 | } 17 | 18 | return new UnitInterval(value); 19 | } 20 | 21 | static random(min?: number, max?: number): UnitInterval { 22 | if (min === undefined) { 23 | return this.random(0, max); 24 | } 25 | 26 | if (max == undefined) { 27 | return this.random(min, 1); 28 | } 29 | 30 | const minSanitized = Math.max(0, min); 31 | const maxSanitized = Math.min(1, max); 32 | 33 | return this.create( 34 | Math.random() * (maxSanitized - minSanitized) + minSanitized, 35 | ); 36 | } 37 | 38 | increment(value: number) { 39 | return UnitInterval.create(Math.min(this.props.value + value, 1)); 40 | } 41 | 42 | isMaxReached() { 43 | return this.props.value === 1; 44 | } 45 | 46 | get value() { 47 | return this.props.value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/UnitIntervalRange.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { UnitIntervalRange } from './UnitIntervalRange'; 3 | 4 | describe('src/domain/valueObjects/UnitIntervalRange', () => { 5 | it('should be defined', () => { 6 | expect(UnitIntervalRange).toBeDefined(); 7 | }); 8 | 9 | describe('create', () => { 10 | it('should return a UnitIntervalRange', () => { 11 | const unitIntervalRange = UnitIntervalRange.create(0, 1); 12 | expect(unitIntervalRange).toBeInstanceOf(UnitIntervalRange); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/web/src/domain/valueObjects/UnitIntervalRange.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@cryptochords/shared'; 2 | import { UnitInterval } from './UnitInterval'; 3 | 4 | interface UnitIntevalRangeProps { 5 | min: UnitInterval; 6 | max: UnitInterval; 7 | } 8 | 9 | export class UnitIntervalRange extends ValueObject { 10 | private constructor(min: UnitInterval, max: UnitInterval) { 11 | super({ max, min }); 12 | } 13 | 14 | static create(min: number, max: number) { 15 | return new UnitIntervalRange( 16 | UnitInterval.create(min), 17 | UnitInterval.create(max), 18 | ); 19 | } 20 | 21 | get min() { 22 | return this.props.min.value; 23 | } 24 | 25 | get max() { 26 | return this.props.max.value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/infrastructure/repositories/InMemoryCubeRepository.spec.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryCubeRepository } from './InMemoryCubeRepository'; 2 | import { Cube } from '../../domain/entities/Cube'; 3 | import { beforeEach, describe, expect, it } from 'vitest'; 4 | import { Uuid } from '@cryptochords/shared'; 5 | 6 | describe('src/infrastructure/repositories/InMemoryCubeRepository', () => { 7 | let repository: InMemoryCubeRepository; 8 | 9 | beforeEach(() => { 10 | repository = new InMemoryCubeRepository(); 11 | }); 12 | 13 | describe('list', () => { 14 | it('should return an empty array when no cubes are stored', async () => { 15 | const cubes = await repository.list(); 16 | expect(cubes).toEqual([]); 17 | }); 18 | }); 19 | 20 | describe('create', () => { 21 | it('should store the cube and return it', async () => { 22 | const cube: Cube = Cube.random(); 23 | const createdCube = await repository.create(cube); 24 | expect(createdCube).toEqual(cube); 25 | }); 26 | }); 27 | 28 | describe('delete', () => { 29 | it('should remove the cube with the given id', async () => { 30 | const cube: Cube = Cube.random(); 31 | await repository.create(cube); 32 | await repository.delete(cube.uuid); 33 | const cubes = await repository.list(); 34 | expect(cubes).toEqual([]); 35 | }); 36 | }); 37 | 38 | describe('update', () => { 39 | it('should update the cube and return it', async () => { 40 | const id = Uuid.create(); 41 | const cube: Cube = { 42 | color: { value: 'blue' }, 43 | mirrored: false, 44 | uuid: id, 45 | x: { value: 0.5 }, 46 | y: { value: 0 }, 47 | } as Cube; 48 | await repository.create(cube); 49 | 50 | const updatedCube = { 51 | color: { value: 'orange' }, 52 | mirrored: true, 53 | uuid: id, 54 | x: { value: 0.7 }, 55 | y: { value: 0.1 }, 56 | } as Cube; 57 | 58 | const result = await repository.update(updatedCube); 59 | expect(result).toEqual(updatedCube); 60 | expect(await repository.list()).toEqual([updatedCube]); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /apps/web/src/infrastructure/repositories/InMemoryCubeRepository.ts: -------------------------------------------------------------------------------- 1 | import { Uuid } from '@cryptochords/shared'; 2 | import { Cube } from '../../domain/entities/Cube'; 3 | import { CubeRepository } from '../../domain/repositories/CubeRepository'; 4 | 5 | export class InMemoryCubeRepository implements CubeRepository { 6 | private cubes: Map = new Map(); 7 | 8 | async list(): Promise { 9 | return Array.from(this.cubes.values()); 10 | } 11 | 12 | async create(cube: Cube): Promise { 13 | this.cubes.set(cube.uuid.value, cube); 14 | return cube; 15 | } 16 | 17 | async delete(id: Uuid): Promise { 18 | this.cubes.delete(id.value); 19 | } 20 | 21 | async update(cube: Cube): Promise { 22 | this.cubes.set(cube.uuid.value, cube); 23 | return cube; 24 | } 25 | 26 | async clear(): Promise { 27 | this.cubes.clear(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/src/infrastructure/repositories/InMemoryKeyboardRepository.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard } from '../../domain/valueObjects/Keyboard'; 2 | 3 | export class InMemoryKeyboardRepository { 4 | private keyboard: Keyboard | undefined; 5 | 6 | public setKeyboard(keyboard: Keyboard) { 7 | this.keyboard = keyboard; 8 | } 9 | 10 | public getKeyboard() { 11 | return this.keyboard; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/infrastructure/repositories/LimitedInMemoryTransactionRepository.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from '../../domain/entities/Transaction'; 2 | import { TransactionRepository } from '../../domain/repositories/TransactionRepository'; 3 | 4 | export class LimitedInMemoryTransactionRepository 5 | implements TransactionRepository 6 | { 7 | private transactions: Transaction[] = []; 8 | 9 | constructor(private max = 99) { 10 | // 11 | } 12 | 13 | async create(transaction: Transaction): Promise { 14 | this.transactions.push(transaction); 15 | this.enforceMaxSize(); 16 | } 17 | 18 | private enforceMaxSize() { 19 | if (this.transactions.length > this.max) { 20 | this.transactions.shift(); 21 | } 22 | } 23 | 24 | async list(): Promise { 25 | return this.transactions; 26 | } 27 | 28 | async clear(): Promise { 29 | this.transactions = []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/infrastructure/repositories/LocalStorageOptionsRepository.ts: -------------------------------------------------------------------------------- 1 | import { InstrumentEnum } from '../../domain/enum/InstrumentEnum'; 2 | import { OptionsRepository } from '../../domain/repositories/OptionsRepository'; 3 | import { Instrument } from '../../domain/valueObjects/Instrument'; 4 | import { Options } from '../../domain/valueObjects/Options'; 5 | 6 | export class LocalStorageOptionsRepository implements OptionsRepository { 7 | public async setOptions(options: Options) { 8 | localStorage.setItem('options', JSON.stringify(options.toJSON())); 9 | } 10 | 11 | public async getOptions() { 12 | const options = localStorage.getItem('options'); 13 | if (options) { 14 | return Options.fromJSON(JSON.parse(options)); 15 | } 16 | 17 | return Options.create({ 18 | instrument: Instrument.create({ name: InstrumentEnum.Piano }), 19 | muted: true, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/infrastructure/repositories/StaticNetworksRepository.ts: -------------------------------------------------------------------------------- 1 | import { HemiMainnet, HemiTestnet, NetworkEnum } from '@cryptochords/shared'; 2 | import { NetworkRepository } from '../../domain/repositories/NetworkRepository'; 3 | import { Network } from '../../domain/entities/Network'; 4 | 5 | export class StaticNetworksRepository implements NetworkRepository { 6 | private readonly testnet = Network.create({ 7 | explorerUrl: HemiTestnet.blockExplorers.default.url, 8 | name: NetworkEnum.TESTNET, 9 | wsUrl: StaticNetworksRepository.buildWebserviceUrl(NetworkEnum.TESTNET), 10 | }); 11 | 12 | private readonly mainnet = Network.create({ 13 | explorerUrl: HemiMainnet.blockExplorers.default.url, 14 | name: NetworkEnum.MAINNET, 15 | wsUrl: StaticNetworksRepository.buildWebserviceUrl(NetworkEnum.MAINNET), 16 | }); 17 | 18 | private networks = [this.mainnet, this.testnet]; 19 | 20 | private selectedNetwork: Network = this.testnet; 21 | 22 | async list() { 23 | return this.networks.slice(); 24 | } 25 | 26 | async find(name: NetworkEnum) { 27 | if (!name) return undefined; 28 | 29 | return this.networks.find(network => network.name === name); 30 | } 31 | 32 | private static replaceHostWithCurrentLocation(url: string) { 33 | // replaces ${host} with the host address ignoring spaces and tabs within the brackets 34 | const regex = /\$\{[ \t]*host[ \t]*\}/i; 35 | return url.replace(regex, document.location.host); 36 | } 37 | 38 | private static getWebserviceUrlFromEnvironment(network: NetworkEnum) { 39 | return network === NetworkEnum.MAINNET 40 | ? import.meta.env.VITE_MAINNET_API_WEBSERVICE_URL 41 | : import.meta.env.VITE_TESTNET_API_WEBSERVICE_URL; 42 | } 43 | 44 | private static buildWebserviceUrl(network: NetworkEnum) { 45 | const url = this.getWebserviceUrlFromEnvironment(network); 46 | return this.replaceHostWithCurrentLocation(url!); 47 | } 48 | 49 | async select(name: NetworkEnum) { 50 | const network = await this.find(name); 51 | if (!network) throw new Error('Network not found'); 52 | 53 | this.selectedNetwork = network; 54 | } 55 | 56 | async getSelected() { 57 | return this.selectedNetwork; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/base/Presenter.ts: -------------------------------------------------------------------------------- 1 | import { StateController } from './StateController'; 2 | 3 | export abstract class Presenter extends StateController {} 4 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/base/StateController.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 2 | import { StateController } from './StateController'; 3 | 4 | type State = { 5 | count: number; 6 | }; 7 | 8 | const initalState: State = { 9 | count: 0, 10 | }; 11 | 12 | class SimpleStateController extends StateController { 13 | public increment(): void { 14 | this.changeState({ count: this.state.count + 1 }); 15 | } 16 | } 17 | 18 | describe('src/presentation/common/base/StateController', () => { 19 | let stateController: SimpleStateController; 20 | 21 | beforeEach(() => { 22 | stateController = new SimpleStateController(initalState); 23 | }); 24 | 25 | it('should initialize with initial state', () => { 26 | expect(stateController.state).toEqual(initalState); 27 | }); 28 | 29 | it('should update state correctly', () => { 30 | stateController.increment(); 31 | expect(stateController.state).toEqual({ count: 1 }); 32 | }); 33 | 34 | describe('subscribe', () => { 35 | it('should notify subscribers when state changes', () => { 36 | const subscriber1 = vi.fn(); 37 | const subscriber2 = vi.fn(); 38 | 39 | stateController.subscribe(subscriber1); 40 | stateController.subscribe(subscriber2); 41 | 42 | stateController.increment(); 43 | 44 | expect(subscriber1).toHaveBeenCalledWith({ count: 1 }); 45 | expect(subscriber2).toHaveBeenCalledWith({ count: 1 }); 46 | }); 47 | 48 | it('should send current status to subscriber when sendCurrentStatus is true', () => { 49 | const subscriber = vi.fn(); 50 | 51 | stateController.subscribe(subscriber, true); 52 | 53 | expect(subscriber).toHaveBeenCalledWith(initalState); 54 | }); 55 | }); 56 | 57 | describe('unsubscribe', () => { 58 | it('should unsubscribe subscribers correctly', () => { 59 | const subscriber1 = vi.fn(); 60 | const subscriber2 = vi.fn(); 61 | 62 | stateController.subscribe(subscriber1); 63 | stateController.subscribe(subscriber2); 64 | 65 | stateController.increment(); 66 | 67 | stateController.unsubscribe(subscriber1); 68 | 69 | stateController.increment(); 70 | 71 | expect(subscriber1).toHaveBeenCalledTimes(1); 72 | expect(subscriber2).toHaveBeenCalledTimes(2); 73 | }); 74 | 75 | it('should not throw an error or affect the listeners if unsubscribe an unsubscribed listener', () => { 76 | const subscriber1 = vi.fn(); 77 | const subscriber2 = vi.fn(); 78 | 79 | stateController.subscribe(subscriber1); 80 | stateController.unsubscribe(subscriber2); 81 | 82 | stateController.increment(); 83 | expect(subscriber1).toHaveBeenCalledTimes(1); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/base/StateController.ts: -------------------------------------------------------------------------------- 1 | type Subscription = (state: State) => void; 2 | 3 | export abstract class StateController { 4 | private internalState: State; 5 | private listeners: Subscription[] = []; 6 | 7 | constructor(initalState: State) { 8 | this.internalState = initalState; 9 | } 10 | 11 | public get state(): State { 12 | return this.internalState; 13 | } 14 | 15 | protected changeState(state: Partial): void { 16 | this.setState({ 17 | ...this.internalState, 18 | ...state, 19 | }); 20 | } 21 | 22 | protected setState(state: State): void { 23 | this.internalState = state; 24 | this.notifyStateChangeToListeners(); 25 | } 26 | 27 | protected notifyStateChangeToListeners() { 28 | if (this.listeners.length > 0) { 29 | this.listeners.forEach(this.notifyStateChangeToListener.bind(this)); 30 | } 31 | } 32 | 33 | protected notifyStateChangeToListener(listener: Subscription) { 34 | listener(this.state); 35 | } 36 | 37 | public subscribe( 38 | listener: Subscription, 39 | sendCurrentStatus = false, 40 | ): void { 41 | this.listeners.push(listener); 42 | if (sendCurrentStatus) { 43 | this.notifyStateChangeToListener(listener); 44 | } 45 | } 46 | 47 | public unsubscribe(listener: Subscription): void { 48 | const index = this.listeners.indexOf(listener); 49 | if (index < 0) { 50 | return; 51 | } 52 | 53 | this.listeners.splice(index, 1); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/context/domainServices.ts: -------------------------------------------------------------------------------- 1 | import { SoundService } from '../../../domain/services/SoundService'; 2 | import { ToneJS } from '../../../infrastructure/services/ToneJs'; 3 | 4 | export interface DomainServices { 5 | soundService: SoundService; 6 | } 7 | 8 | export const domainServices: DomainServices = { 9 | soundService: new ToneJS(), 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/context/presenters.ts: -------------------------------------------------------------------------------- 1 | import { Event, ObservableSet } from '@cryptochords/shared'; 2 | import { AppPresenter } from '../presenter/app/AppPresenter'; 3 | import { CubesPresenter } from '../presenter/cubes/CubesPresenter'; 4 | import { KeyboardPresenter } from '../presenter/keyboard/KeyboardPresenter'; 5 | import { TransactionsPresenter } from '../presenter/transactions/TransactionsPresenter'; 6 | import { services } from './services'; 7 | import { OptionsPresenter } from '../presenter/options/OptionsPresenter'; 8 | 9 | export interface Presenters { 10 | cubesPresenter: CubesPresenter; 11 | appPresenter: AppPresenter; 12 | keyboardPresenter: KeyboardPresenter; 13 | transactionsPresenter: TransactionsPresenter; 14 | optionsPresenter: OptionsPresenter; 15 | } 16 | 17 | const cubesPresenter = new CubesPresenter( 18 | services.getCubes, 19 | services.recalculateCubePosition, 20 | ); 21 | 22 | const appPresenter = new AppPresenter( 23 | services.createTransaction, 24 | services.switchNetwork, 25 | services.getSelectedNetwork, 26 | services.listNetworks, 27 | ); 28 | 29 | const keyboardPresenter = new KeyboardPresenter( 30 | services.createKeyboard, 31 | services.getKeyboard, 32 | new ObservableSet(services.pressKey, services.releaseKey), 33 | ); 34 | 35 | const transactionsPresenter = new TransactionsPresenter( 36 | services.listTransactions, 37 | new ObservableSet(services.createTransaction, services.switchNetwork), 38 | ); 39 | 40 | const optionsPresenter = new OptionsPresenter( 41 | services.getOptions, 42 | services.setMuted, 43 | services.setInstrument, 44 | services.loadInstrument, 45 | ); 46 | 47 | export const presenters: Presenters = { 48 | appPresenter, 49 | cubesPresenter, 50 | keyboardPresenter, 51 | optionsPresenter, 52 | transactionsPresenter, 53 | }; 54 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/context/repositories.ts: -------------------------------------------------------------------------------- 1 | import { CubeRepository } from '../../../domain/repositories/CubeRepository'; 2 | import { KeyboardRepository } from '../../../domain/repositories/KeyboardRepository'; 3 | import { NetworkRepository } from '../../../domain/repositories/NetworkRepository'; 4 | import { OptionsRepository } from '../../../domain/repositories/OptionsRepository'; 5 | import { TransactionRepository } from '../../../domain/repositories/TransactionRepository'; 6 | import { InMemoryCubeRepository } from '../../../infrastructure/repositories/InMemoryCubeRepository'; 7 | import { InMemoryKeyboardRepository } from '../../../infrastructure/repositories/InMemoryKeyboardRepository'; 8 | import { LimitedInMemoryTransactionRepository } from '../../../infrastructure/repositories/LimitedInMemoryTransactionRepository'; 9 | import { LocalStorageOptionsRepository } from '../../../infrastructure/repositories/LocalStorageOptionsRepository'; 10 | import { StaticNetworksRepository } from '../../../infrastructure/repositories/StaticNetworksRepository'; 11 | 12 | export interface Repositories { 13 | cubeRepository: CubeRepository; 14 | transactionRepository: TransactionRepository; 15 | keyboardRepository: KeyboardRepository; 16 | optionsRepository: OptionsRepository; 17 | networkRepository: NetworkRepository; 18 | } 19 | 20 | export const repositories: Repositories = { 21 | cubeRepository: new InMemoryCubeRepository(), 22 | keyboardRepository: new InMemoryKeyboardRepository(), 23 | networkRepository: new StaticNetworksRepository(), 24 | optionsRepository: new LocalStorageOptionsRepository(), 25 | transactionRepository: new LimitedInMemoryTransactionRepository(), 26 | }; 27 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/app/AppPresenterState.ts: -------------------------------------------------------------------------------- 1 | export type AppPresenterState = { 2 | networkNames: string[]; 3 | selectedNetworkName: string | null; 4 | navMenuVisible: boolean; 5 | selectedNetworkWsUrl: string | null; 6 | enableMainnet: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/cubes/CubesPresenter.ts: -------------------------------------------------------------------------------- 1 | import { GetCubesService } from '../../../../application/services/GetCubes/GetCubesService'; 2 | import { RecalculateCubePositionsService } from '../../../../application/services/RecalculateCubePositions/RecalculateCubePositionsService'; 3 | import { Presenter } from '../../base/Presenter'; 4 | import { CubesPresenterState } from './CubesPresenterState'; 5 | 6 | const initalState: CubesPresenterState = { 7 | cubes: [], 8 | }; 9 | 10 | interface CubesPresenterOptions { 11 | maxCubeCreationInterval: number; 12 | tickInterval: number; 13 | maxCubeAge: number; 14 | } 15 | 16 | const defaultOptions: CubesPresenterOptions = { 17 | maxCubeAge: 17_000, 18 | maxCubeCreationInterval: 300, 19 | tickInterval: 20, 20 | }; 21 | 22 | export class CubesPresenter extends Presenter { 23 | private readonly getCubesService: GetCubesService; 24 | private readonly recalculateCubePositions: RecalculateCubePositionsService; 25 | 26 | private options: CubesPresenterOptions; 27 | 28 | private tickLoop: NodeJS.Timeout | undefined; 29 | 30 | constructor( 31 | getCubesService: GetCubesService, 32 | recalculateCubePositions: RecalculateCubePositionsService, 33 | options?: Partial, 34 | ) { 35 | super(initalState); 36 | this.getCubesService = getCubesService; 37 | this.recalculateCubePositions = recalculateCubePositions; 38 | this.options = { 39 | ...defaultOptions, 40 | ...options, 41 | }; 42 | 43 | this.run(); 44 | } 45 | 46 | async run() { 47 | if (this.isRunning()) { 48 | return; 49 | } 50 | this.runTickLoop(); 51 | } 52 | 53 | stop() { 54 | this.stopTickLoop(); 55 | } 56 | 57 | private isRunning() { 58 | return !!this.tickLoop; 59 | } 60 | 61 | private stopTickLoop() { 62 | if (this.tickLoop) { 63 | clearInterval(this.tickLoop); 64 | this.tickLoop = undefined; 65 | } 66 | } 67 | 68 | private runTickLoop() { 69 | this.tickLoop = setInterval(async () => { 70 | await this.moveAllCubesUp(); 71 | await this.syncState(); 72 | }, this.options.tickInterval); 73 | } 74 | 75 | private async moveAllCubesUp() { 76 | await this.recalculateCubePositions.execute({ 77 | maxAge: this.options.maxCubeAge, 78 | }); 79 | } 80 | 81 | private async syncState() { 82 | const getCubesResponse = await this.getCubesService.execute(); 83 | this.changeState({ 84 | cubes: getCubesResponse.cubes, 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/cubes/CubesPresenterState.ts: -------------------------------------------------------------------------------- 1 | export type CubesPresenterState = { 2 | cubes: { 3 | id: string; 4 | x: number; 5 | y: number; 6 | color: string; 7 | mirrored: boolean; 8 | }[]; 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/keyboard/KeyboardPresenter.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { KeyboardPresenter } from './KeyboardPresenter'; 3 | import { InMemoryKeyboardRepository } from '../../../../infrastructure/repositories/InMemoryKeyboardRepository'; 4 | import { CreateKeyboardService } from '../../../../application/services/CreateKeyboard/CreateKeyboardService'; 5 | import { GetKeyboardService } from '../../../../application/services/GetKeyboard/GetKeyboardService'; 6 | 7 | const keyboardRepository = new InMemoryKeyboardRepository(); 8 | const createKeyboardService = new CreateKeyboardService(keyboardRepository); 9 | await createKeyboardService.execute({ 10 | initialOctave: 1, 11 | initialPitchClass: 'A', 12 | numberOfKeys: 88, 13 | }); 14 | const getKeyboardService = new GetKeyboardService(keyboardRepository); 15 | const keyboardPresenter = new KeyboardPresenter( 16 | createKeyboardService, 17 | getKeyboardService, 18 | ); 19 | 20 | describe('src/presentation/common/presenter/keyboard/KeyboardPresenter', () => { 21 | it('should be defined', () => { 22 | expect(keyboardPresenter).toBeDefined(); 23 | }); 24 | 25 | it('should have 88 keys', () => { 26 | expect(keyboardPresenter.state.keys.length).toBe(88); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/keyboard/KeyboardPresenter.ts: -------------------------------------------------------------------------------- 1 | import { Event, Observable } from '@cryptochords/shared'; 2 | import { CreateKeyboardService } from '../../../../application/services/CreateKeyboard/CreateKeyboardService'; 3 | import { GetKeyboardService } from '../../../../application/services/GetKeyboard/GetKeyboardService'; 4 | import { Presenter } from '../../base/Presenter'; 5 | import { KeyboardPresenterState } from './KeyboardPresenterState'; 6 | 7 | const initalState: KeyboardPresenterState = { 8 | keys: [], 9 | }; 10 | 11 | interface KeyboardPresenterOptions { 12 | numberOfKeys: number; 13 | initialPitchClass: string; 14 | initialOctave: number; 15 | } 16 | 17 | const defaultOptions: KeyboardPresenterOptions = { 18 | initialOctave: 0, 19 | initialPitchClass: 'A', 20 | numberOfKeys: 88, 21 | }; 22 | 23 | export class KeyboardPresenter extends Presenter { 24 | private options: KeyboardPresenterOptions; 25 | private createKeyboardService: CreateKeyboardService; 26 | private getKeyboardService: GetKeyboardService; 27 | 28 | constructor( 29 | createKeyboardService: CreateKeyboardService, 30 | getKeyboardService: GetKeyboardService, 31 | keyboardChangesObserver?: Observable, 32 | options?: Partial, 33 | ) { 34 | super(initalState); 35 | 36 | this.createKeyboardService = createKeyboardService; 37 | this.getKeyboardService = getKeyboardService; 38 | 39 | this.options = { 40 | ...defaultOptions, 41 | ...options, 42 | }; 43 | 44 | this.createKeyboard(); 45 | this.refresh(); 46 | 47 | if (keyboardChangesObserver) { 48 | keyboardChangesObserver.listen(this.refresh.bind(this)); 49 | } 50 | } 51 | 52 | async refresh() { 53 | const keyboard = await this.getKeyboardService.execute(); 54 | 55 | if (!keyboard) { 56 | return; 57 | } 58 | 59 | this.changeState({ 60 | keys: keyboard.keys.map(key => ({ 61 | color: key.pressed ? key.color : undefined, 62 | keyShape: key.keyShape, 63 | pitch: { 64 | class: key.pitch.class, 65 | octave: key.pitch.octave, 66 | }, 67 | x: key.x, 68 | })), 69 | }); 70 | } 71 | 72 | private async createKeyboard() { 73 | await this.createKeyboardService.execute({ 74 | initialOctave: this.options.initialOctave, 75 | initialPitchClass: this.options.initialPitchClass, 76 | numberOfKeys: this.options.numberOfKeys, 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/keyboard/KeyboardPresenterState.ts: -------------------------------------------------------------------------------- 1 | export type KeyboardPresenterState = { 2 | keys: { 3 | pitch: { 4 | class: string; 5 | octave: number; 6 | }; 7 | keyShape: string; 8 | x: number; 9 | color?: string; 10 | }[]; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/options/OptionsPresenter.ts: -------------------------------------------------------------------------------- 1 | import { GetOptionsService } from '../../../../application/services/GetOptions/GetOptionsService'; 2 | import { LoadInstrumentService } from '../../../../application/services/LoadInstrument/LoadInstrumentService'; 3 | import { SetInstrumentService } from '../../../../application/services/SetInstrument/SetInstrumentService'; 4 | import { SetMutedService } from '../../../../application/services/SetMuted/SetMutedService'; 5 | import { 6 | InstrumentEnum, 7 | instrumentLabels, 8 | } from '../../../../domain/enum/InstrumentEnum'; 9 | import { Presenter } from '../../base/Presenter'; 10 | import { OptionsPresenterState } from './OptionsPresenterState'; 11 | 12 | const instruments: { label: string; value: string }[] = Object.values( 13 | InstrumentEnum, 14 | ).map(instrument => ({ 15 | label: instrumentLabels.get(instrument) ?? instrument, 16 | value: instrument, 17 | })); 18 | 19 | const initalState: OptionsPresenterState = { 20 | displayInstrumentPicker: false, 21 | displayLoadingMessage: false, 22 | instruments, 23 | muted: true, 24 | selectedInstrument: '', 25 | }; 26 | 27 | export class OptionsPresenter extends Presenter { 28 | private getOptionsService: GetOptionsService; 29 | private setMutedService: SetMutedService; 30 | private setInstrumentService: SetInstrumentService; 31 | private loadInstrumentService: LoadInstrumentService; 32 | 33 | constructor( 34 | getOptionsService: GetOptionsService, 35 | setMutedService: SetMutedService, 36 | setInstrumentService: SetInstrumentService, 37 | loadInstrumentService: LoadInstrumentService, 38 | ) { 39 | super(initalState); 40 | 41 | this.getOptionsService = getOptionsService; 42 | this.setMutedService = setMutedService; 43 | this.setInstrumentService = setInstrumentService; 44 | this.loadInstrumentService = loadInstrumentService; 45 | 46 | this.init(); 47 | } 48 | 49 | async init() { 50 | const options = await this.getOptionsService.execute(); 51 | this.setState({ 52 | displayInstrumentPicker: true, 53 | displayLoadingMessage: false, 54 | instruments, 55 | muted: options.muted, 56 | selectedInstrument: options.instrument, 57 | }); 58 | } 59 | 60 | async setMuted(muted: boolean) { 61 | if (!muted) { 62 | await this.loadInstrument(this.state.selectedInstrument); 63 | } 64 | await this.setMutedService.execute({ muted }); 65 | this.changeState({ muted }); 66 | } 67 | 68 | async setInstrument(instrument: string) { 69 | await this.loadInstrument(instrument); 70 | await this.setInstrumentService.execute({ instrument }); 71 | this.changeState({ selectedInstrument: instrument }); 72 | } 73 | 74 | private async loadInstrument(instrument: string) { 75 | this.changeState({ 76 | displayInstrumentPicker: false, 77 | displayLoadingMessage: true, 78 | }); 79 | await this.loadInstrumentService.execute({ instrument }); 80 | this.changeState({ 81 | displayInstrumentPicker: true, 82 | displayLoadingMessage: false, 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/options/OptionsPresenterState.ts: -------------------------------------------------------------------------------- 1 | export type InstrumentState = { 2 | value: string; 3 | label: string; 4 | }; 5 | 6 | export type OptionsPresenterState = { 7 | muted: boolean; 8 | selectedInstrument: string; 9 | instruments: InstrumentState[]; 10 | displayLoadingMessage: boolean; 11 | displayInstrumentPicker: boolean; 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/transactions/TransactionsPresenter.ts: -------------------------------------------------------------------------------- 1 | import { Event, Observable, TxTypesEnum } from '@cryptochords/shared'; 2 | import { ListTransactionsService } from '../../../../application/services/ListTransactions/ListTransactionsService'; 3 | import { Presenter } from '../../base/Presenter'; 4 | import { TransactionsPresenterState } from './TransactionsPresenterState'; 5 | 6 | const initalState: TransactionsPresenterState = { 7 | transactions: [], 8 | }; 9 | 10 | const titleMap: Map = new Map([ 11 | [TxTypesEnum.Block, 'New Block'], 12 | [TxTypesEnum.Eth, 'ETH'], 13 | [TxTypesEnum.Pop, 'PoP'], 14 | [TxTypesEnum.Btc, 'BTC'], 15 | ]); 16 | 17 | const rgbMap: Map = new Map([ 18 | [TxTypesEnum.Block, '#10FF2A'], 19 | [TxTypesEnum.Eth, '#00D3FF'], 20 | [TxTypesEnum.Pop, '#DC53FF'], 21 | [TxTypesEnum.Btc, '#FFB200'], 22 | ]); 23 | 24 | const messageMap: Map = new Map([ 25 | [TxTypesEnum.Block, 'created by'], 26 | [TxTypesEnum.Eth, 'transaction by'], 27 | [TxTypesEnum.Pop, 'transaction by'], 28 | [TxTypesEnum.Btc, 'transaction by'], 29 | ]); 30 | 31 | export class TransactionsPresenter extends Presenter { 32 | private listTransactions: ListTransactionsService; 33 | 34 | constructor( 35 | listTransactions: ListTransactionsService, 36 | transactionsChangeObserver?: Observable, 37 | ) { 38 | super(initalState); 39 | this.listTransactions = listTransactions; 40 | if (transactionsChangeObserver) { 41 | transactionsChangeObserver.listen(this.refresh.bind(this)); 42 | } 43 | } 44 | 45 | async refresh() { 46 | const response = await this.listTransactions.execute(); 47 | this.changeState({ 48 | transactions: response.transactions.map(transaction => ({ 49 | at: this.formatDate(transaction.timestamp), 50 | color: rgbMap.get(transaction.txType as TxTypesEnum) ?? '#fff', 51 | id: transaction.address, 52 | message: messageMap.get(transaction.txType as TxTypesEnum) ?? ' ', 53 | type: titleMap.get(transaction.txType as TxTypesEnum) ?? 'Unknown', 54 | url: transaction.url, 55 | })), 56 | }); 57 | } 58 | 59 | private formatDate(date: number) { 60 | const formattedDate = new Date(date).toLocaleString('en-US', { 61 | day: 'numeric', 62 | hour: 'numeric', 63 | hour12: true, 64 | minute: 'numeric', 65 | month: 'short', 66 | second: 'numeric', 67 | year: 'numeric', 68 | }); 69 | 70 | return formattedDate.replace(/,/g, ''); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /apps/web/src/presentation/common/presenter/transactions/TransactionsPresenterState.ts: -------------------------------------------------------------------------------- 1 | export type TransactionsPresenterState = { 2 | transactions: { 3 | type: string; 4 | color: string; 5 | message: string; 6 | id: string; 7 | at: string; 8 | url: string; 9 | }[]; 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/App.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AppPresenter } from '../common/presenter/app/AppPresenter'; 3 | import { AppPresenterState } from '../common/presenter/app/AppPresenterState'; 4 | import { Header } from './components/Header'; 5 | import { MainContent } from './components/MainContent'; 6 | import { NavMenu } from './components/NavMenu'; 7 | import { presenters } from './context'; 8 | import { usePresenter } from './hooks/usePresenter'; 9 | 10 | function App() { 11 | const { appPresenter } = useContext(presenters); 12 | 13 | const { navMenuVisible } = usePresenter( 14 | appPresenter, 15 | ); 16 | 17 | return ( 18 | <> 19 |
appPresenter.navButtonClicked()} 22 | networks={appPresenter.state.networkNames} 23 | selectedNetwork={appPresenter.state.selectedNetworkName} 24 | selectNetwork={networkName => appPresenter.selectNetwork(networkName)} 25 | enableMainnet={appPresenter.state.enableMainnet} 26 | /> 27 | appPresenter.closeButtonClicked()} 29 | className={`${navMenuVisible ? '' : 'hidden'} md:hidden`} 30 | networks={appPresenter.state.networkNames} 31 | selectedNetwork={appPresenter.state.selectedNetworkName} 32 | selectNetwork={networkName => appPresenter.selectNetwork(networkName)} 33 | enableMainnet={appPresenter.state.enableMainnet} 34 | /> 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Card.tsx: -------------------------------------------------------------------------------- 1 | export const Card = function (props: { 2 | className?: string; 3 | children?: React.ReactNode; 4 | }) { 5 | return ( 6 |
9 | {props.children} 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | export const Checkbox = function (props: { 2 | value: boolean; 3 | className?: string; 4 | onClick?: () => void; 5 | }) { 6 | return ( 7 | <> 8 | {}} 13 | /> 14 |
49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Cube.tsx: -------------------------------------------------------------------------------- 1 | export const Cube = function (props: { 2 | className?: string; 3 | style?: React.CSSProperties; 4 | centerPositioning?: boolean; 5 | color: string; 6 | mirrored?: boolean; 7 | }) { 8 | return ( 9 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Cubes.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { CubesPresenter } from '../../common/presenter/cubes/CubesPresenter'; 3 | import { CubesPresenterState } from '../../common/presenter/cubes/CubesPresenterState'; 4 | import { presenters } from '../context'; 5 | import { usePresenter } from '../hooks/usePresenter'; 6 | import { Cube } from './Cube'; 7 | 8 | export const Cubes = function (props: { 9 | className?: string; 10 | centerPositioning?: boolean; 11 | yMultiplier?: number; 12 | bottomOffset?: number; 13 | }) { 14 | const { cubesPresenter } = useContext(presenters); 15 | const { cubes } = usePresenter( 16 | cubesPresenter, 17 | ); 18 | 19 | return ( 20 |
21 | {cubes.map(cube => { 22 | return ( 23 | 33 | ); 34 | })} 35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { JoinCommunityButton } from './JoinCommunityButton'; 2 | import { Logo } from './Logo'; 3 | import { NavButton } from './NavButton'; 4 | import { NavItems } from './NavItems'; 5 | import { NetworkSwitch } from './NetworkSwitch'; 6 | 7 | export const Header = (props: { 8 | className?: string; 9 | onNavButtonClick?: () => void; 10 | networks: string[]; 11 | selectedNetwork: string | null; 12 | selectNetwork: (networkName: string) => void; 13 | enableMainnet: boolean; 14 | }) => { 15 | return ( 16 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | {props.enableMainnet && ( 37 | 42 | )} 43 | 44 |
45 | 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/JoinCommunityButton.tsx: -------------------------------------------------------------------------------- 1 | import { discordUrl } from 'hemi-socials'; 2 | 3 | export const JoinCommunityButton = (props: { className?: string }) => { 4 | return ( 5 | 23 | Join Community 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Key.tsx: -------------------------------------------------------------------------------- 1 | export interface KeyProps { 2 | keyShape: string; 3 | x: number; 4 | index: number; 5 | color?: string; 6 | } 7 | export const Key = function ({ keyShape, x, index, color }: KeyProps) { 8 | const isBlack = keyShape === 'black'; 9 | const zIndex = isBlack ? 1 : 0; 10 | const left = `${x * 100}%`; 11 | const top = 0; 12 | const width = isBlack ? '5.4vw' : '8.4vw'; 13 | 14 | return ( 15 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Keyboard.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { KeyboardPresenter } from '../../common/presenter/keyboard/KeyboardPresenter'; 3 | import { KeyboardPresenterState } from '../../common/presenter/keyboard/KeyboardPresenterState'; 4 | import { presenters } from '../context'; 5 | import { usePresenter } from '../hooks/usePresenter'; 6 | import { Key } from './Key'; 7 | import keyboard from '/image/keyboard/base.svg'; 8 | 9 | export const Keyboard = function (props: { className?: string }) { 10 | const { keyboardPresenter } = useContext(presenters); 11 | const { keys } = usePresenter( 12 | keyboardPresenter, 13 | ); 14 | 15 | return ( 16 |
17 | 18 |
19 | {keys.map(({ keyShape, x, color, pitch }, index) => ( 20 | 27 | ))} 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import logo from '/image/crypto-chords.svg'; 2 | import { twitterUrl } from 'hemi-socials'; 3 | 4 | export const Logo = function () { 5 | const url = twitterUrl; 6 | return ( 7 | 15 | Crypto Chords 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/MainContent.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from './Card'; 2 | import { Cubes } from './Cubes'; 3 | import { Keyboard } from './Keyboard'; 4 | import { Social } from './Social'; 5 | import { Options } from './Options'; 6 | import { Transactions } from './Transactions'; 7 | import desktopBackground from '/image/background/desktop.svg'; 8 | import mobileBackground from '/image/background/mobile.svg'; 9 | 10 | // This constant is used to multiply the y position of the cubes 11 | // to allow the cubes extrapolate the top of the component 12 | const CUBES_Y_MULTIPLIER = 2; 13 | // This constant is used to offset the cubes from the bottom of the component 14 | // to hide the creation of the cubes avoiding a visual glitch 15 | const CUBES_BOTTOM_OFFSET = 0.15; 16 | 17 | export function MainContent(props: { className: string }) { 18 | return ( 19 |
20 | 24 | 28 |
29 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Menu.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | items: { id: string; content: React.ReactNode }[]; 3 | }; 4 | 5 | export const Menu = ({ items }: Props) => ( 6 |
7 |
    8 | {items.map(({ content, id }) => ( 9 |
  • 13 | {content} 14 |
  • 15 | ))} 16 |
17 |
18 | ); 19 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/NavButton.tsx: -------------------------------------------------------------------------------- 1 | import navButton from '/image/nav-button.svg'; 2 | export const NavButton = function (props: { 3 | className?: string; 4 | onClick?: () => void; 5 | }) { 6 | return ( 7 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/NavItem.tsx: -------------------------------------------------------------------------------- 1 | export const NavItem = function (options: { 2 | href: string; 3 | children: React.ReactNode; 4 | }) { 5 | return ( 6 | 11 | {options.children} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/NavItems.tsx: -------------------------------------------------------------------------------- 1 | import { NavItem } from './NavItem'; 2 | import { githubUrl } from 'hemi-socials'; 3 | 4 | export const NavItems = function (props: { className?: string }) { 5 | return ( 6 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/NavMenu.tsx: -------------------------------------------------------------------------------- 1 | import { JoinCommunityButton } from './JoinCommunityButton'; 2 | import { Logo } from './Logo'; 3 | import { NavItems } from './NavItems'; 4 | import { NetworkSwitch } from './NetworkSwitch'; 5 | import { Social } from './Social'; 6 | 7 | import closeButton from '/image/close-button.svg'; 8 | 9 | export const NavMenu = function (props: { 10 | className?: string; 11 | onCloseButtonClick?: () => void; 12 | networks: string[]; 13 | selectedNetwork: string | null; 14 | selectNetwork: (networkName: string) => void; 15 | enableMainnet: boolean; 16 | }) { 17 | return ( 18 |
30 |
31 | 32 | 37 |
38 | 39 |
40 | {props.enableMainnet && ( 41 |
42 | 48 |
49 | )} 50 |
51 | 52 |
53 |
54 | 55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /apps/web/src/presentation/react/components/Options.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import { usePresenter } from '../hooks/usePresenter'; 3 | import { presenters } from '../context'; 4 | import { OptionsPresenter } from '../../common/presenter/options/OptionsPresenter'; 5 | import { OptionsPresenterState } from '../../common/presenter/options/OptionsPresenterState'; 6 | import { Checkbox } from './Checkbox'; 7 | import { Select } from './Select'; 8 | 9 | export const Options = function (props: { className?: string }) { 10 | const { optionsPresenter } = useContext(presenters); 11 | const { 12 | muted, 13 | instruments, 14 | selectedInstrument, 15 | displayLoadingMessage, 16 | displayInstrumentPicker, 17 | } = usePresenter(optionsPresenter); 18 | 19 | /** 20 | * Since the browser will not allow sound to be played without user interaction, 21 | * we will mute the sound by default. 22 | */ 23 | useEffect(() => { 24 | optionsPresenter.setMuted(true); 25 | }, [optionsPresenter]); 26 | 27 | return ( 28 |
36 | 37 | Sound 38 | 39 | optionsPresenter.setMuted(!muted)} 43 | /> 44 | {displayLoadingMessage && Loading instrument sounds...} 45 | {displayInstrumentPicker && ( 46 |