;
12 | }
13 | export default function CountdownTimer({
14 | targetDate,
15 | expiredMessage,
16 | onExpire,
17 | }: ICountdownTimerProps) {
18 | const [days, hours, minutes, seconds] = useCountdown(targetDate, onExpire);
19 |
20 | if (days + hours + minutes + seconds <= 0) {
21 | return {expiredMessage || 'Expired'}
;
22 | }
23 |
24 | return (
25 |
26 | {days > 0 ? `${days} days, ` : ''}
27 | {hours > 0 ? `${hours} hours, ` : ''}
28 | {minutes > 0 ? `${minutes} minutes, ` : ''}
29 | {seconds > 0 ? `${seconds} seconds` : ''}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/api/src/app/auth/dto/forgot-password.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { ApiProperty } from "@nestjs/swagger";
7 | import {
8 | IsEmail,
9 | IsNotEmpty,
10 | Matches,
11 | MaxLength,
12 | MinLength,
13 | } from "class-validator";
14 |
15 | export class CreateForgotPasswordResetTokenDto {
16 | @ApiProperty({ description: "Email address" })
17 | @IsNotEmpty()
18 | @IsEmail()
19 | email: string;
20 | }
21 |
22 | export class ValidateForgotPasswordResetTokenDto {
23 | @ApiProperty({ description: "Password reset token" })
24 | @IsNotEmpty()
25 | token: string;
26 |
27 | @ApiProperty({ description: "New password for the user" })
28 | @IsNotEmpty()
29 | @MinLength(8)
30 | @MaxLength(150)
31 | @Matches(/^(?=.*[A-Za-z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/, {
32 | message: "Password must contain at least one letter and one number",
33 | })
34 | newPassword: string;
35 | }
36 |
--------------------------------------------------------------------------------
/packages/frontend/src/services/Order/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | export enum OrderType {
7 | BUY = 'BUY',
8 | SELL = 'SELL',
9 | }
10 |
11 | export interface CreateOrder {
12 | quantity?: number;
13 | notional?: number;
14 | stockId?: string; // Just need to pass in one or the other (either stockId or ticker, if stockId is passed in it will be prioritized)
15 | ticker?: string;
16 | }
17 |
18 | export interface Order {
19 | name: string;
20 | ticker: string;
21 | image: string;
22 | quantity: number;
23 | boughtAt: number;
24 | currentBuyingPower: number;
25 | id: string;
26 | createdAt: string;
27 | type: OrderType;
28 | status: string;
29 | notional: number;
30 | }
31 |
32 | export interface OrderHistory {
33 | cursor: string;
34 | data: Order[];
35 | }
36 |
37 | export interface GetOrdersQueryParams {
38 | ticker?: string;
39 | limit?: number; // defaults to 30
40 | }
41 |
--------------------------------------------------------------------------------
/packages/api/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Description
6 |
7 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
8 |
9 | ## Installation
10 |
11 | ```bash
12 | $ yarn install
13 | ```
14 |
15 | ## Hacking
16 |
17 | You need to start two things:
18 |
19 | The api:
20 |
21 | ```bash
22 | yarn run start:dev
23 | ```
24 |
25 | You can also use `.user.env` as a gitignored .env file that won't
26 | be checked into VCS.
27 |
28 | ## Test
29 |
30 | ```bash
31 | # unit tests
32 | $ yarn run test
33 |
34 | # e2e tests
35 | $ yarn run test:e2e
36 |
37 | # test coverage
38 | $ yarn run test:cov
39 | ```
40 |
41 | ## To Test the Production Dockerfile
42 |
43 | If you're in the root directory:
44 |
45 | ```shell
46 | docker build -t bloom/api:latest -f packages/api/Dockerfile .
47 | docker run -p 80:80 -t bloom/api:latest
48 | ```
49 |
--------------------------------------------------------------------------------
/packages/postgresql/src/db/migrations/1658173270596-fixquantity-position.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { MigrationInterface, QueryRunner } from "typeorm";
7 |
8 | export class fixquantityPosition1658173270596 implements MigrationInterface {
9 | name = "fixquantityPosition1658173270596";
10 |
11 | public async up(queryRunner: QueryRunner): Promise {
12 | await queryRunner.query(
13 | `ALTER TABLE "CurrentPosition" DROP COLUMN "quantity"`
14 | );
15 | await queryRunner.query(
16 | `ALTER TABLE "CurrentPosition" ADD "quantity" bigint NOT NULL`
17 | );
18 | }
19 |
20 | public async down(queryRunner: QueryRunner): Promise {
21 | await queryRunner.query(
22 | `ALTER TABLE "CurrentPosition" DROP COLUMN "quantity"`
23 | );
24 | await queryRunner.query(
25 | `ALTER TABLE "CurrentPosition" ADD "quantity" double precision NOT NULL`
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/api/src/app/market/dto/get-trading-days.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { ApiProperty } from "@nestjs/swagger";
7 | import { Transform, Type } from "class-transformer";
8 | import * as moment from "moment";
9 | import { Moment } from "moment";
10 | import { IsNotEmpty } from "class-validator";
11 |
12 | export class GetTradingDaysDto {
13 | @ApiProperty({
14 | description: "Date formatted",
15 | type: String,
16 | default: new Date(new Date().getTime() - 100000).toISOString(),
17 | })
18 | @IsNotEmpty()
19 | @Type(() => Date)
20 | @Transform(({ value }) => moment(value), { toClassOnly: true })
21 | start: Moment;
22 |
23 | @ApiProperty({
24 | description: "Date formatted",
25 | type: String,
26 | default: new Date().toISOString(),
27 | })
28 | @IsNotEmpty()
29 | @Type(() => Date)
30 | @Transform(({ value }) => moment(value), { toClassOnly: true })
31 | end: Moment;
32 | }
33 |
--------------------------------------------------------------------------------
/packages/api/src/system/OrmConfig.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";
7 | import AllEntities from "@bloom-smg/postgresql";
8 | import DbQueryLogger from "./DbQueryLogger";
9 |
10 | const isCI = process.env.CI === "true";
11 | const isLocalTest = process.env.NODE_ENV === "test" && !isCI;
12 |
13 | const ormConfig: PostgresConnectionOptions = {
14 | type: "postgres",
15 | username: process.env.PG_USERNAME,
16 | host: process.env.PG_HOST,
17 | database: isLocalTest
18 | ? `test_${process.env.PG_DATABASE}`
19 | : process.env.PG_DATABASE,
20 | password: process.env.PG_PASSWORD,
21 | port: parseInt(process.env.PG_PORT),
22 | synchronize: false,
23 | entities: [...AllEntities],
24 | logger: new DbQueryLogger(),
25 | maxQueryExecutionTime: 300, //it will log all queries that take more than Xms
26 | };
27 |
28 | export default ormConfig;
29 |
--------------------------------------------------------------------------------
/packages/real-time-collector/src/app/minutes/crypto/crypto.service.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { Injectable, Logger } from "@nestjs/common";
7 | import { AbstractMinutesService } from "../abstract-minutes.service";
8 | import { MinuteBarData } from "./dto/minute-bar-data.dto";
9 |
10 | @Injectable()
11 | export class CryptoMinutesService extends AbstractMinutesService {
12 | logger = new Logger(CryptoMinutesService.name);
13 | url = "wss://delayed.polygon.io/crypto";
14 |
15 | async onConnect(): Promise {
16 | this.socket.send(
17 | JSON.stringify({
18 | action: "subscribe",
19 | params: "XA.*",
20 | })
21 | );
22 | }
23 |
24 | async receive(data) {
25 | for (const x of data) {
26 | if (x.ev === "XA") {
27 | await this.handleNewMinute(x);
28 | }
29 | }
30 | }
31 |
32 | async handleNewMinute(data: MinuteBarData) {
33 | return data;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/api/src/app/orders/dto/order-history.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { ApiProperty } from "@nestjs/swagger";
7 | import { OrderHistoryEntity } from "@bloom-smg/postgresql";
8 | import { OrderDto } from "./order.dto";
9 | import { StockPriceEntity } from "@bloom-smg/postgresql";
10 |
11 | export class OrderHistoryPageDto {
12 | @ApiProperty({
13 | description:
14 | "Pass in `next` or `before` in query parameter to get next or previous page",
15 | })
16 | cursor: string;
17 |
18 | data: OrderDto[];
19 |
20 | constructor(
21 | data: OrderHistoryEntity[],
22 | cursor: string,
23 | stockPrices: StockPriceEntity[]
24 | ) {
25 | this.data = data.map((x) => {
26 | const stockPrice = stockPrices.find(
27 | (price) => price.stockId === x.stockId
28 | );
29 | return OrderDto.Init(x, x.stock, stockPrice.price, x.value, 0);
30 | });
31 | this.cursor = cursor;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/api/src/app/search/dto/asset-search.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { StockEntity } from "@bloom-smg/postgresql";
7 |
8 | class SearchDto {
9 | ticker: string;
10 | name: string;
11 | description: string;
12 | image: string;
13 | latestPrice?: number;
14 |
15 | constructor(stock: StockEntity, prices: { [key: string]: number }) {
16 | this.ticker = stock.ticker;
17 | this.name = stock.name;
18 | this.description = stock.description;
19 | this.image = stock.image;
20 | this.latestPrice = prices[stock.id] || null; // possibility it might not exist yet
21 | }
22 | }
23 |
24 | export class SearchPageDto {
25 | assets: SearchDto[];
26 | count: number;
27 |
28 | constructor(
29 | stocks: StockEntity[],
30 | prices: { [key: string]: number },
31 | count: number
32 | ) {
33 | this.assets = stocks.map((stock) => new SearchDto(stock, prices));
34 | this.count = count;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/real-time-collector/src/app/minutes/crypto/dto/minute-bar-data.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | export interface MinuteBarData {
7 | // https://polygon.io/docs/crypto/ws_crypto_xa
8 | ev: "XA";
9 | // The crypto pair.
10 | pair: string;
11 | // The open price for this aggregate window.
12 | o: number;
13 | // The close price for this aggregate window.
14 | c: number;
15 | // The high price for this aggregate window.
16 | h: number;
17 | // The low price for this aggregate window.
18 | l: number;
19 | // The volume of trades for this aggregate window.
20 | v: number;
21 | // The timestamp of the starting tick for this aggregate window in Unix Milliseconds.
22 | s: number;
23 | // The timestamp of the ending tick for this aggregate window in Unix Milliseconds.
24 | e: number;
25 | // The volume weighted average price for this aggregate window.
26 | vw: number;
27 | // The average trade size for this aggregate window.
28 | z: number;
29 | }
30 |
--------------------------------------------------------------------------------
/packages/frontend/src/hooks/useDashboardNavigate.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { useContext } from 'react';
7 | import { NavigationItem } from '../modules/Dashboard/Navigation/utils';
8 | import { useEffect, useState } from 'react';
9 | import { DashboardContext } from '../modules/Dashboard';
10 | import { getNavigationItems } from '../modules/Dashboard/Navigation/utils';
11 |
12 | export default function useDashboardNavigate(): [number, (val: number) => void, NavigationItem[]] {
13 | const { game } = useContext(DashboardContext);
14 | const [active, setActive] = useState(0);
15 | const items = getNavigationItems(game?.isGameAdmin as boolean);
16 |
17 | useEffect(() => {
18 | let navPath = location.pathname.substring(location.pathname.lastIndexOf('/') + 1);
19 | const idx = items.findIndex((item) => {
20 | return item.to === navPath;
21 | });
22 | setActive(idx);
23 | }, [location.pathname]);
24 |
25 | return [active, setActive, items];
26 | }
27 |
--------------------------------------------------------------------------------
/packages/frontend/src/modules/PasswordReset/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { Navigate, Route, Routes, useMatch } from 'react-router-dom';
7 | import Header from '../Header';
8 | import ForgotPassword from './ForgotPassword';
9 | import ResetPassword from './ResetPassword';
10 |
11 | export default function PasswordReset() {
12 | const empty = useMatch('/password');
13 |
14 | if (empty) {
15 | return ;
16 | }
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | } />
24 | } />
25 | } />
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/packages/api/src/app/orders/dto/order-history-query.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import { ApiPropertyOptional } from "@nestjs/swagger";
7 | import { IsInt, IsOptional, Max, Min } from "class-validator";
8 | import { Type } from "class-transformer";
9 |
10 | export class OrderHistoryQueryDto {
11 | @ApiPropertyOptional({
12 | description: "Pass in cursor in query parameter to get next page",
13 | })
14 | @IsOptional()
15 | after?: string;
16 |
17 | @ApiPropertyOptional({
18 | description: "Pass in cursor in query parameter to get next page",
19 | })
20 | @IsOptional()
21 | before?: string;
22 |
23 | @ApiPropertyOptional({
24 | description: "Limit the number of results",
25 | default: 30,
26 | })
27 | @IsOptional()
28 | @Type(() => Number)
29 | @IsInt()
30 | @Max(80)
31 | @Min(1)
32 | limit: number = 30;
33 |
34 | @IsOptional()
35 | @ApiPropertyOptional({
36 | description: "Pass in ticker to search order history by",
37 | })
38 | ticker?: string;
39 | }
40 |
--------------------------------------------------------------------------------
/packages/scripts/src/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2022 Contour Labs, Inc.
3 | * SPDX-License-Identifier: AGPL-3.0-only
4 | */
5 |
6 | import * as core from "@actions/core";
7 | import CITest from "./actions/ci-test";
8 | import PreDeploy from "./actions/pre-deploy";
9 | import PreDeployFrontend from "./actions/pre-deploy-frontend";
10 | import Deploy from "./actions/deploy";
11 | import PostDeploy from "./actions/post-deploy";
12 | import CopyParameterStoreValues from "./aws/copy.parameterstore";
13 |
14 | async function main() {
15 | const args = process.argv.slice(2);
16 | if (args.length === 0) {
17 | return;
18 | }
19 | const actions = {
20 | "ci-test": CITest,
21 | "pre-deploy": PreDeploy,
22 | "pre-deploy-frontend": PreDeployFrontend,
23 | deploy: Deploy,
24 | "post-deploy": PostDeploy,
25 | "aws-parameter-store-copy": CopyParameterStoreValues,
26 | };
27 | const fn = actions[args[0]];
28 | if (fn === null || fn === undefined) {
29 | core.setFailed("Invalid script name");
30 | return;
31 | }
32 | await fn(...args.slice(1));
33 | }
34 |
35 | main();
36 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | 1. Environment Setup
4 |
5 | This project requires node version `17.6.0` (our legacy repo needs `14.x.x`) + `yarn`. If you are using `nvm`:
6 |
7 | ```shell
8 | nvm install
9 | nvm use
10 | # close and reopen shell
11 | npm install --global yarn
12 | ```
13 |
14 | 2. Lerna Setup
15 |
16 | ```shell
17 | yarn global add lerna
18 | lerna bootstrap
19 | lerna run build
20 | ```
21 |
22 | The command `lerna run