diff --git a/Dockerfile b/Dockerfile index 2a014e3..703d96b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,8 @@ # syntax = docker/dockerfile:1 - ARG NODE_VERSION=20 FROM node:${NODE_VERSION}-slim AS base-node WORKDIR /app ENV NODE_ENV="production" - RUN apt-get update -qq && \ apt-get install --no-install-recommends -y \ build-essential \ @@ -18,7 +16,6 @@ RUN apt-get update -qq && \ && rm -rf /var/lib/apt/lists/* FROM base-node AS build-main - # Copy package files COPY website/package.json website/package-lock.json* ./ @@ -32,58 +29,31 @@ COPY website/. . RUN mkdir -p .svelte-kit # Generate SvelteKit types and build + RUN npm run build FROM base-node AS build-websocket WORKDIR /websocket - RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/root/.bun/bin:${PATH}" - COPY website/websocket/package.json website/websocket/bun.lock* ./ COPY website/websocket/tsconfig.json ./ - RUN bun install - COPY website/websocket/src ./src/ - RUN bun build src/main.ts --outdir dist --target bun FROM base-node AS production-main - COPY --from=build-main --chown=node:node /app/build ./build COPY --from=build-main --chown=node:node /app/node_modules ./node_modules COPY --from=build-main --chown=node:node /app/package.json ./package.json - -RUN npm install -g pm2 - -RUN echo 'module.exports = {\ - apps: [{\ - name: "rugplay-app",\ - script: "./build/index.js",\ - instances: "max",\ - exec_mode: "cluster",\ - env: {\ - NODE_ENV: "production",\ - PORT: 3000,\ - BODY_SIZE_LIMIT: "1.1M"\ - }\ - }]\ -};' > ecosystem.config.cjs - USER node EXPOSE 3000 +CMD ["node", "build"] -CMD ["pm2-runtime", "start", "ecosystem.config.cjs"] - -FROM base-node AS production-websocket +FROM oven/bun:1 AS production-websocket WORKDIR /websocket - -RUN curl -fsSL https://bun.sh/install | bash -ENV PATH="/root/.bun/bin:${PATH}" - -COPY --from=build-websocket /websocket/dist ./dist -COPY --from=build-websocket /websocket/package.json ./package.json - +COPY --from=build-websocket --chown=bun:bun /websocket/dist ./dist +COPY --from=build-websocket --chown=bun:bun /websocket/package.json ./package.json +USER bun EXPOSE 8080 CMD ["bun", "run", "dist/main.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index dbb5493..d577469 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,14 +5,16 @@ services: target: production-main dockerfile: Dockerfile ports: - - "3002:3000" + - "5900-5907:3000" env_file: - website/.env depends_on: - websocket - - redis - - postgres restart: unless-stopped + networks: + - shared_backend + deploy: + replicas: 8 websocket: build: @@ -20,33 +22,13 @@ services: target: production-websocket dockerfile: Dockerfile ports: - - "8081:8080" + - "8082:8080" env_file: - website/.env - depends_on: - - redis restart: unless-stopped + networks: + - shared_backend - redis: - image: redis:8-alpine - volumes: - - rugplay_redisdata:/data - command: "redis-server --save 60 1" - restart: unless-stopped - - postgres: - image: pgvector/pgvector:pg16 - container_name: rugplay-postgres - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB:-rugplay} - ports: - - "5432:5432" - volumes: - - rugplay_pgdata:/var/lib/postgresql/data - restart: unless-stopped - -volumes: - rugplay_pgdata: - rugplay_redisdata: +networks: + shared_backend: + external: true \ No newline at end of file diff --git a/website/drizzle/0003_complete_runaways.sql b/website/drizzle/0003_complete_runaways.sql new file mode 100644 index 0000000..a7b2e52 --- /dev/null +++ b/website/drizzle/0003_complete_runaways.sql @@ -0,0 +1,19 @@ +CREATE INDEX IF NOT EXISTS "coin_symbol_idx" ON "coin" USING btree ("symbol");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_creator_id_idx" ON "coin" USING btree ("creator_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_is_listed_idx" ON "coin" USING btree ("is_listed");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_market_cap_idx" ON "coin" USING btree ("market_cap");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_current_price_idx" ON "coin" USING btree ("current_price");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_change24h_idx" ON "coin" USING btree ("change_24h");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_volume24h_idx" ON "coin" USING btree ("volume_24h");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "coin_created_at_idx" ON "coin" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transaction_user_id_idx" ON "transaction" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transaction_coin_id_idx" ON "transaction" USING btree ("coin_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transaction_type_idx" ON "transaction" USING btree ("type");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transaction_timestamp_idx" ON "transaction" USING btree ("timestamp");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transaction_user_coin_idx" ON "transaction" USING btree ("user_id","coin_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "transaction_coin_type_idx" ON "transaction" USING btree ("coin_id","type");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "user_username_idx" ON "user" USING btree ("username");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "user_is_banned_idx" ON "user" USING btree ("is_banned");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "user_is_admin_idx" ON "user" USING btree ("is_admin");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "user_created_at_idx" ON "user" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "user_updated_at_idx" ON "user" USING btree ("updated_at"); \ No newline at end of file diff --git a/website/drizzle/meta/0003_snapshot.json b/website/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..7abf363 --- /dev/null +++ b/website/drizzle/meta/0003_snapshot.json @@ -0,0 +1,2027 @@ +{ + "id": "496c23a1-1fd7-4116-b72d-5b56db3e7059", + "prevId": "223d9abc-f0d3-4c71-9cda-8a069fc13205", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.account_deletion_request": { + "name": "account_deletion_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scheduled_deletion_at": { + "name": "scheduled_deletion_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "account_deletion_request_user_id_idx": { + "name": "account_deletion_request_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_request_scheduled_deletion_idx": { + "name": "account_deletion_request_scheduled_deletion_idx", + "columns": [ + { + "expression": "scheduled_deletion_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_request_open_idx": { + "name": "account_deletion_request_open_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "is_processed = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_deletion_request_user_id_user_id_fk": { + "name": "account_deletion_request_user_id_user_id_fk", + "tableFrom": "account_deletion_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_deletion_request_user_id_unique": { + "name": "account_deletion_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + } + }, + "public.coin": { + "name": "coin", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "initial_supply": { + "name": "initial_supply", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "circulating_supply": { + "name": "circulating_supply", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "current_price": { + "name": "current_price", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "market_cap": { + "name": "market_cap", + "type": "numeric(30, 2)", + "primaryKey": false, + "notNull": true + }, + "volume_24h": { + "name": "volume_24h", + "type": "numeric(30, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.00'" + }, + "change_24h": { + "name": "change_24h", + "type": "numeric(30, 4)", + "primaryKey": false, + "notNull": false, + "default": "'0.0000'" + }, + "pool_coin_amount": { + "name": "pool_coin_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "pool_base_currency_amount": { + "name": "pool_base_currency_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "coin_symbol_idx": { + "name": "coin_symbol_idx", + "columns": [ + { + "expression": "symbol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_creator_id_idx": { + "name": "coin_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_is_listed_idx": { + "name": "coin_is_listed_idx", + "columns": [ + { + "expression": "is_listed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_market_cap_idx": { + "name": "coin_market_cap_idx", + "columns": [ + { + "expression": "market_cap", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_current_price_idx": { + "name": "coin_current_price_idx", + "columns": [ + { + "expression": "current_price", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_change24h_idx": { + "name": "coin_change24h_idx", + "columns": [ + { + "expression": "change_24h", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_volume24h_idx": { + "name": "coin_volume24h_idx", + "columns": [ + { + "expression": "volume_24h", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "coin_created_at_idx": { + "name": "coin_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coin_creator_id_user_id_fk": { + "name": "coin_creator_id_user_id_fk", + "tableFrom": "coin", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "coin_symbol_unique": { + "name": "coin_symbol_unique", + "nullsNotDistinct": false, + "columns": [ + "symbol" + ] + } + } + }, + "public.comment": { + "name": "comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_deleted": { + "name": "is_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "comment_user_id_idx": { + "name": "comment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_coin_id_idx": { + "name": "comment_coin_id_idx", + "columns": [ + { + "expression": "coin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_user_id_user_id_fk": { + "name": "comment_user_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comment_coin_id_coin_id_fk": { + "name": "comment_coin_id_coin_id_fk", + "tableFrom": "comment", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.comment_like": { + "name": "comment_like", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_like_user_id_user_id_fk": { + "name": "comment_like_user_id_user_id_fk", + "tableFrom": "comment_like", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_like_comment_id_comment_id_fk": { + "name": "comment_like_comment_id_comment_id_fk", + "tableFrom": "comment_like", + "tableTo": "comment", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "comment_like_user_id_comment_id_pk": { + "name": "comment_like_user_id_comment_id_pk", + "columns": [ + "user_id", + "comment_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notification_user_id_idx": { + "name": "notification_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_type_idx": { + "name": "notification_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_is_read_idx": { + "name": "notification_is_read_idx", + "columns": [ + { + "expression": "is_read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_created_at_idx": { + "name": "notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_user_id_user_id_fk": { + "name": "notification_user_id_user_id_fk", + "tableFrom": "notification", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.prediction_bet": { + "name": "prediction_bet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "question_id": { + "name": "question_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "side": { + "name": "side", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "actual_winnings": { + "name": "actual_winnings", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "prediction_bet_user_id_idx": { + "name": "prediction_bet_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_question_id_idx": { + "name": "prediction_bet_question_id_idx", + "columns": [ + { + "expression": "question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_user_question_idx": { + "name": "prediction_bet_user_question_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "question_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_bet_created_at_idx": { + "name": "prediction_bet_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prediction_bet_user_id_user_id_fk": { + "name": "prediction_bet_user_id_user_id_fk", + "tableFrom": "prediction_bet", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "prediction_bet_question_id_prediction_question_id_fk": { + "name": "prediction_bet_question_id_prediction_question_id_fk", + "tableFrom": "prediction_bet", + "tableTo": "prediction_question", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.prediction_question": { + "name": "prediction_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "question": { + "name": "question", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "prediction_market_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "resolution_date": { + "name": "resolution_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ai_resolution": { + "name": "ai_resolution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_yes_amount": { + "name": "total_yes_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "total_no_amount": { + "name": "total_no_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "requires_web_search": { + "name": "requires_web_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "validation_reason": { + "name": "validation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "prediction_question_creator_id_idx": { + "name": "prediction_question_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_status_idx": { + "name": "prediction_question_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_resolution_date_idx": { + "name": "prediction_question_resolution_date_idx", + "columns": [ + { + "expression": "resolution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "prediction_question_status_resolution_idx": { + "name": "prediction_question_status_resolution_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resolution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prediction_question_creator_id_user_id_fk": { + "name": "prediction_question_creator_id_user_id_fk", + "tableFrom": "prediction_question", + "tableTo": "user", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.price_history": { + "name": "price_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "price_history_coin_id_coin_id_fk": { + "name": "price_history_coin_id_coin_id_fk", + "tableFrom": "price_history", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.promo_code": { + "name": "promo_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount": { + "name": "reward_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "promo_code_created_by_user_id_fk": { + "name": "promo_code_created_by_user_id_fk", + "tableFrom": "promo_code", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "promo_code_code_unique": { + "name": "promo_code_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + } + }, + "public.promo_code_redemption": { + "name": "promo_code_redemption", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promo_code_id": { + "name": "promo_code_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reward_amount": { + "name": "reward_amount", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "promo_code_redemption_user_id_user_id_fk": { + "name": "promo_code_redemption_user_id_user_id_fk", + "tableFrom": "promo_code_redemption", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "promo_code_redemption_promo_code_id_promo_code_id_fk": { + "name": "promo_code_redemption_promo_code_id_promo_code_id_fk", + "tableFrom": "promo_code_redemption", + "tableTo": "promo_code", + "columnsFrom": [ + "promo_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "promo_code_redemption_user_id_promo_code_id_unique": { + "name": "promo_code_redemption_user_id_promo_code_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "promo_code_id" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "price_per_coin": { + "name": "price_per_coin", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true + }, + "total_base_currency_amount": { + "name": "total_base_currency_amount", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sender_user_id": { + "name": "sender_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "transaction_user_id_idx": { + "name": "transaction_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transaction_coin_id_idx": { + "name": "transaction_coin_id_idx", + "columns": [ + { + "expression": "coin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transaction_type_idx": { + "name": "transaction_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transaction_timestamp_idx": { + "name": "transaction_timestamp_idx", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transaction_user_coin_idx": { + "name": "transaction_user_coin_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transaction_coin_type_idx": { + "name": "transaction_coin_type_idx", + "columns": [ + { + "expression": "coin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transaction_user_id_user_id_fk": { + "name": "transaction_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "transaction_coin_id_coin_id_fk": { + "name": "transaction_coin_id_coin_id_fk", + "tableFrom": "transaction", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transaction_recipient_user_id_user_id_fk": { + "name": "transaction_recipient_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "transaction_sender_user_id_user_id_fk": { + "name": "transaction_sender_user_id_user_id_fk", + "tableFrom": "transaction", + "tableTo": "user", + "columnsFrom": [ + "sender_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_banned": { + "name": "is_banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_currency_balance": { + "name": "base_currency_balance", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'100.00000000'" + }, + "bio": { + "name": "bio", + "type": "varchar(160)", + "primaryKey": false, + "notNull": false, + "default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'" + }, + "username": { + "name": "username", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "volume_master": { + "name": "volume_master", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0.70'" + }, + "volume_muted": { + "name": "volume_muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_reward_claim": { + "name": "last_reward_claim", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_rewards_claimed": { + "name": "total_rewards_claimed", + "type": "numeric(20, 8)", + "primaryKey": false, + "notNull": true, + "default": "'0.00000000'" + }, + "login_streak": { + "name": "login_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prestige_level": { + "name": "prestige_level", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "user_username_idx": { + "name": "user_username_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_is_banned_idx": { + "name": "user_is_banned_idx", + "columns": [ + { + "expression": "is_banned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_is_admin_idx": { + "name": "user_is_admin_idx", + "columns": [ + { + "expression": "is_admin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_created_at_idx": { + "name": "user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_updated_at_idx": { + "name": "user_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + } + }, + "public.user_portfolio": { + "name": "user_portfolio", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "coin_id": { + "name": "coin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(30, 8)", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_portfolio_user_id_user_id_fk": { + "name": "user_portfolio_user_id_user_id_fk", + "tableFrom": "user_portfolio", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_portfolio_coin_id_coin_id_fk": { + "name": "user_portfolio_coin_id_coin_id_fk", + "tableFrom": "user_portfolio", + "tableTo": "coin", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_portfolio_user_id_coin_id_pk": { + "name": "user_portfolio_user_id_coin_id_pk", + "columns": [ + "user_id", + "coin_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "HOPIUM", + "SYSTEM", + "TRANSFER", + "RUG_PULL" + ] + }, + "public.prediction_market_status": { + "name": "prediction_market_status", + "schema": "public", + "values": [ + "ACTIVE", + "RESOLVED", + "CANCELLED" + ] + }, + "public.transaction_type": { + "name": "transaction_type", + "schema": "public", + "values": [ + "BUY", + "SELL", + "TRANSFER_IN", + "TRANSFER_OUT" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/website/drizzle/meta/_journal.json b/website/drizzle/meta/_journal.json index aede010..6f60140 100644 --- a/website/drizzle/meta/_journal.json +++ b/website/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1749916220202, "tag": "0002_small_micromacro", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1750707307426, + "tag": "0003_complete_runaways", + "breakpoints": true } ] } \ No newline at end of file diff --git a/website/src/hooks.server.ts b/website/src/hooks.server.ts index abd3d72..8a37675 100644 --- a/website/src/hooks.server.ts +++ b/website/src/hooks.server.ts @@ -7,6 +7,7 @@ import { redirect, type Handle } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { user } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; +import { minesCleanupInactiveGames, minesAutoCashout } from '$lib/server/games/mines'; async function initializeScheduler() { if (building) return; @@ -49,10 +50,16 @@ async function initializeScheduler() { processAccountDeletions().catch(console.error); }, 5 * 60 * 1000); + const minesCleanupInterval = setInterval(() => { + minesCleanupInactiveGames().catch(console.error); + minesAutoCashout().catch(console.error); + }, 60 * 1000); + // Cleanup on process exit const cleanup = async () => { clearInterval(renewInterval); clearInterval(schedulerInterval); + clearInterval(minesCleanupInterval); const currentValue = await redis.get(lockKey); if (currentValue === lockValue) { await redis.del(lockKey); @@ -172,6 +179,13 @@ export const handle: Handle = async ({ event, resolve }) => { event.locals.userSession = userData; + if (event.url.pathname.startsWith('/api/')) { + const response = await svelteKitHandler({ event, resolve, auth }); + response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); + + return response; + } + return svelteKitHandler({ event, resolve, auth }); }; diff --git a/website/src/lib/components/self/TradeModal.svelte b/website/src/lib/components/self/TradeModal.svelte index 9c323dd..f554103 100644 --- a/website/src/lib/components/self/TradeModal.svelte +++ b/website/src/lib/components/self/TradeModal.svelte @@ -212,4 +212,4 @@ - + \ No newline at end of file diff --git a/website/src/lib/components/self/games/Coinflip.svelte b/website/src/lib/components/self/games/Coinflip.svelte index daa940d..e4aac9a 100644 --- a/website/src/lib/components/self/games/Coinflip.svelte +++ b/website/src/lib/components/self/games/Coinflip.svelte @@ -164,6 +164,7 @@ import { formatValue, playSound, showConfetti } from '$lib/utils'; import { volumeSettings } from '$lib/stores/volume-settings'; import { onMount } from 'svelte'; + import { fetchPortfolioSummary } from '$lib/stores/portfolio-data'; interface CoinflipResult { won: boolean; @@ -314,8 +315,18 @@ } } - onMount(() => { + onMount(async () => { volumeSettings.load(); + + try { + const data = await fetchPortfolioSummary(); + if (data) { + balance = data.baseCurrencyBalance; + onBalanceUpdate?.(data.baseCurrencyBalance); + } + } catch (error) { + console.error('Failed to fetch balance:', error); + } }); diff --git a/website/src/lib/components/self/games/Dice.svelte b/website/src/lib/components/self/games/Dice.svelte new file mode 100644 index 0000000..ea4430c --- /dev/null +++ b/website/src/lib/components/self/games/Dice.svelte @@ -0,0 +1,419 @@ + + + + + Dice + Choose a number and roll the dice to win 3x your bet! + + +
+
+
+

Balance

+

{formatValue(balance)}

+
+ +
+
+
+ {#each Array(6) as _, i} +
+
+ {#each Array(i + 1) as _} +
+ {/each} +
+
+ {/each} +
+
+
+ +
+ {#if lastResult && !isRolling} +
+ {#if lastResult.won} +

WIN

+

+ Won {formatValue(lastResult.payout)} on {lastResult.result} +

+ {:else} +

LOSS

+

+ Lost {formatValue(lastResult.amountWagered)} on {lastResult.result} +

+ {/if} +
+ {/if} +
+
+ +
+
+
Choose Number
+
+ {#each Array(6) as _, i} + + {/each} +
+
+ +
+ + +

+ Max bet: {MAX_BET_AMOUNT.toLocaleString()} +

+
+ +
+
+ + + + +
+
+ + +
+
+
+
+ + diff --git a/website/src/lib/components/self/games/Mines.svelte b/website/src/lib/components/self/games/Mines.svelte new file mode 100644 index 0000000..17b0aaa --- /dev/null +++ b/website/src/lib/components/self/games/Mines.svelte @@ -0,0 +1,528 @@ + + + + + Mines + + Navigate through the minefield and cash out before hitting a mine! + + + +
+ +
+ +
+

Balance

+

{formatValue(balance)}

+
+ + +
= 7}> + {#each Array(TOTAL_TILES) as _, index} + + + {/each} +
+
+ +
+
+ +
+ + { + const target = e.target as HTMLInputElement | null; + const val = Math.max( + MIN_MINES, + Math.min(24, parseInt(target?.value ?? '') || MIN_MINES) + ); + mineCount = val; + }} + disabled={isPlaying} + class="w-12 text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + /> + +
+

+ You will get + + {calculateMinesMultiplier(isPlaying ? revealedTiles.length + 1 : 1, mineCount, betAmount).toFixed(2)}x + + per tile, probability of winning: + + {calculateProbability(isPlaying ? 1 : 1, mineCount)}% + +

+ + Note: Maximum payout per game is capped at $2,000,000. + +
+
+ + +

+ Max bet: {MAX_BET_AMOUNT.toLocaleString()} +

+
+
+
+ + + + +
+
+
+ {#if !isPlaying} + + {:else} + {#if hasRevealedTile} +
+
+
+ Auto Cashout in {Math.ceil(AUTO_CASHOUT_TIME - autoCashoutTimer)}s +
+
+
= 7} + style="width: {autoCashoutProgress}%" + >
+
+
+
+ {/if} + + + {#if hasRevealedTile} +
+
+ Current Profit: + + +{formatValue(betAmount * (currentMultiplier - 1))} + +
+
+ Next Tile: + + +{formatValue( + betAmount * (calculateMinesMultiplier(revealedTiles.length + 1, mineCount, betAmount) - 1) + )} + +
+
+ Current Multiplier: + {currentMultiplier.toFixed(2)}x +
+
+ {/if} + {/if} +
+
+
+
+
+ + diff --git a/website/src/lib/components/self/games/Slots.svelte b/website/src/lib/components/self/games/Slots.svelte index 6ed0c6c..240339e 100644 --- a/website/src/lib/components/self/games/Slots.svelte +++ b/website/src/lib/components/self/games/Slots.svelte @@ -13,6 +13,7 @@ import { formatValue, playSound, showConfetti, showSchoolPrideCannons } from '$lib/utils'; import { volumeSettings } from '$lib/stores/volume-settings'; import { onMount } from 'svelte'; + import { fetchPortfolioSummary } from '$lib/stores/portfolio-data'; interface SlotsResult { won: boolean; @@ -211,8 +212,19 @@ } }); - onMount(() => { + // Dynmaically fetch the correct balance. + onMount(async () => { volumeSettings.load(); + + try { + const data = await fetchPortfolioSummary(); + if (data) { + balance = data.baseCurrencyBalance; + onBalanceUpdate?.(data.baseCurrencyBalance); + } + } catch (error) { + console.error('Failed to fetch balance:', error); + } }); diff --git a/website/src/lib/server/db/schema.ts b/website/src/lib/server/db/schema.ts index b0b0015..e6b6420 100644 --- a/website/src/lib/server/db/schema.ts +++ b/website/src/lib/server/db/schema.ts @@ -33,6 +33,14 @@ export const user = pgTable("user", { }).notNull().default("0.00000000"), loginStreak: integer("login_streak").notNull().default(0), prestigeLevel: integer("prestige_level").default(0), +}, (table) => { + return { + usernameIdx: index("user_username_idx").on(table.username), + isBannedIdx: index("user_is_banned_idx").on(table.isBanned), + isAdminIdx: index("user_is_admin_idx").on(table.isAdmin), + createdAtIdx: index("user_created_at_idx").on(table.createdAt), + updatedAtIdx: index("user_updated_at_idx").on(table.updatedAt), + }; }); export const session = pgTable("session", { @@ -88,6 +96,17 @@ export const coin = pgTable("coin", { createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), isListed: boolean("is_listed").default(true).notNull(), +}, (table) => { + return { + symbolIdx: index("coin_symbol_idx").on(table.symbol), + creatorIdIdx: index("coin_creator_id_idx").on(table.creatorId), + isListedIdx: index("coin_is_listed_idx").on(table.isListed), + marketCapIdx: index("coin_market_cap_idx").on(table.marketCap), + currentPriceIdx: index("coin_current_price_idx").on(table.currentPrice), + change24hIdx: index("coin_change24h_idx").on(table.change24h), + volume24hIdx: index("coin_volume24h_idx").on(table.volume24h), + createdAtIdx: index("coin_created_at_idx").on(table.createdAt), + }; }); export const userPortfolio = pgTable("user_portfolio", { @@ -114,6 +133,15 @@ export const transaction = pgTable("transaction", { timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow(), recipientUserId: integer('recipient_user_id').references(() => user.id, { onDelete: 'set null' }), senderUserId: integer('sender_user_id').references(() => user.id, { onDelete: 'set null' }), +}, (table) => { + return { + userIdIdx: index("transaction_user_id_idx").on(table.userId), + coinIdIdx: index("transaction_coin_id_idx").on(table.coinId), + typeIdx: index("transaction_type_idx").on(table.type), + timestampIdx: index("transaction_timestamp_idx").on(table.timestamp), + userCoinIdx: index("transaction_user_coin_idx").on(table.userId, table.coinId), + coinTypeIdx: index("transaction_coin_type_idx").on(table.coinId, table.type), + }; }); export const priceHistory = pgTable("price_history", { diff --git a/website/src/lib/server/games/mines.ts b/website/src/lib/server/games/mines.ts new file mode 100644 index 0000000..2acbae4 --- /dev/null +++ b/website/src/lib/server/games/mines.ts @@ -0,0 +1,117 @@ +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis } from '$lib/server/redis'; +import { calculateMinesMultiplier } from '$lib/utils'; + +interface MinesSession { + sessionToken: string; + betAmount: number; + mineCount: number; + minePositions: number[]; + revealedTiles: number[]; + startTime: number; + currentMultiplier: number; + status: 'active' | 'won' | 'lost'; + lastActivity: number; + userId: number; +} + +const MINES_SESSION_PREFIX = 'mines:session:'; +export const getSessionKey = (token: string) => `${MINES_SESSION_PREFIX}${token}`; + +// --- Mines cleanup logic for scheduler --- +export async function minesCleanupInactiveGames() { + const now = Date.now(); + const keys: string[] = []; + let cursor = '0'; + do { + const scanResult = await redis.scan(cursor, { MATCH: `${MINES_SESSION_PREFIX}*` }); + cursor = scanResult.cursor; + keys.push(...scanResult.keys); + } while (cursor !== '0'); + for (const key of keys) { + const sessionRaw = await redis.get(key); + if (!sessionRaw) continue; + const game = JSON.parse(sessionRaw) as MinesSession; + if (now - game.lastActivity > 5 * 60 * 1000) { + if (game.revealedTiles.length === 0) { + try { + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, game.userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const newBalance = Math.round((currentBalance + game.betAmount) * 100000000) / 100000000; + + await db + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, game.userId)); + } catch (error) { + console.error(`Failed to refund inactive game ${game.sessionToken}:`, error); + } + } + await redis.del(getSessionKey(game.sessionToken)); + } + } +} + +export async function minesAutoCashout() { + const now = Date.now(); + const keys: string[] = []; + let cursor = '0'; + do { + const scanResult = await redis.scan(cursor, { MATCH: `${MINES_SESSION_PREFIX}*` }); + cursor = scanResult.cursor; + keys.push(...scanResult.keys); + } while (cursor !== '0'); + for (const key of keys) { + const sessionRaw = await redis.get(key); + if (!sessionRaw) continue; + const game = JSON.parse(sessionRaw) as MinesSession; + + if ( + game.status === 'active' && + game.revealedTiles.length > 0 && + now - game.lastActivity > 20000 && + !game.revealedTiles.some(idx => game.minePositions.includes(idx)) + ) { + try { + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, game.userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const payout = game.betAmount * game.currentMultiplier; + const roundedPayout = Math.round(payout * 100000000) / 100000000; + const newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000; + + await db + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, game.userId)); + + await redis.del(getSessionKey(game.sessionToken)); + } catch (error) { + console.error(`Failed to auto cashout game ${game.sessionToken}:`, error); + } + } + } +} + +export function calculateMultiplier(picks: number, mines: number, betAmount: number): number { + return calculateMinesMultiplier(picks, mines, betAmount); +} \ No newline at end of file diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 94fc051..a7f0017 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -384,3 +384,38 @@ export function getPrestigeColor(level: number): string { export function getMaxPrestigeLevel(): number { return 5; } + +export function calculateMinesMultiplier(picks: number, mines: number, betAmount: number): number { + const TOTAL_TILES = 25; + const HOUSE_EDGE = 0.05; + + let probability = 1; + for (let i = 0; i < picks; i++) { + probability *= (TOTAL_TILES - mines - i) / (TOTAL_TILES - i); + } + if (probability <= 0) return 1.0; + + const fairMultiplier = (1 / probability) * (1 - HOUSE_EDGE); + + // Backend payout cap logic + const MAX_PAYOUT = 2_000_000; + const HIGH_BET_THRESHOLD = 50_000; + const mineFactor = 1 + (mines / 25); + const baseMultiplier = (1.4 + Math.pow(picks, 0.45)) * mineFactor; + let maxPayout: number; + if (betAmount > HIGH_BET_THRESHOLD) { + const betRatio = Math.pow(Math.min(1, (betAmount - HIGH_BET_THRESHOLD) / (MAX_PAYOUT - HIGH_BET_THRESHOLD)), 1); + const maxAllowedMultiplier = 1.05 + (picks * 0.1); + const highBetMultiplier = Math.min(baseMultiplier, maxAllowedMultiplier) * (1 - (betAmount / MAX_PAYOUT) * 0.9); + const betSizeFactor = Math.max(0.1, 1 - (betAmount / MAX_PAYOUT) * 0.9); + const minMultiplier = (1.1 + (picks * 0.15 * betSizeFactor)) * mineFactor; + const reducedMultiplier = highBetMultiplier - ((highBetMultiplier - minMultiplier) * betRatio); + maxPayout = Math.min(betAmount * reducedMultiplier, MAX_PAYOUT); + } else { + maxPayout = Math.min(betAmount * baseMultiplier, MAX_PAYOUT); + } + const rawPayout = fairMultiplier * betAmount; + const cappedPayout = Math.min(rawPayout, maxPayout); + const effectiveMultiplier = cappedPayout / betAmount; + return Math.max(1.0, Number(effectiveMultiplier.toFixed(2))); +} diff --git a/website/src/routes/+page.svelte b/website/src/routes/+page.svelte index e51493d..0fee172 100644 --- a/website/src/routes/+page.svelte +++ b/website/src/routes/+page.svelte @@ -95,11 +95,7 @@ class="text-primary underline hover:cursor-pointer" onclick={() => (shouldSignIn = !shouldSignIn)}>sign in - or{' '} - to play. + to play. {/if}

diff --git a/website/src/routes/api/gambling/dice/+server.ts b/website/src/routes/api/gambling/dice/+server.ts new file mode 100644 index 0000000..d023cf6 --- /dev/null +++ b/website/src/routes/api/gambling/dice/+server.ts @@ -0,0 +1,87 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { randomBytes } from 'crypto'; +import type { RequestHandler } from './$types'; + +interface DiceRequest { + selectedNumber: number; + amount: number; +} + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { selectedNumber, amount }: DiceRequest = await request.json(); + + if (!selectedNumber || selectedNumber < 1 || selectedNumber > 6 || !Number.isInteger(selectedNumber)) { + return json({ error: 'Invalid number selection' }, { status: 400 }); + } + + if (!amount || amount <= 0 || !Number.isFinite(amount)) { + return json({ error: 'Invalid bet amount' }, { status: 400 }); + } + + if (amount > 1000000) { + return json({ error: 'Bet amount too large' }, { status: 400 }); + } + + const userId = Number(session.user.id); + + const result = await db.transaction(async (tx) => { + const [userData] = await tx + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + + const roundedAmount = Math.round(amount * 100000000) / 100000000; + const roundedBalance = Math.round(currentBalance * 100000000) / 100000000; + + if (roundedAmount > roundedBalance) { + throw new Error(`Insufficient funds. You need *${roundedAmount.toFixed(2)} but only have *${roundedBalance.toFixed(2)}`); + } + + const gameResult = Math.floor(randomBytes(1)[0] / 42.67) + 1; // This gives us a number between 1-6 + const won = gameResult === selectedNumber; + + const multiplier = 3; + const payout = won ? roundedAmount * multiplier : 0; + const newBalance = roundedBalance - roundedAmount + payout; + + await tx + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + return { + won, + result: gameResult, + newBalance, + payout, + amountWagered: roundedAmount + }; + }); + + return json(result); + } catch (e) { + console.error('Dice API error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; diff --git a/website/src/routes/api/gambling/mines/cashout/+server.ts b/website/src/routes/api/gambling/mines/cashout/+server.ts new file mode 100644 index 0000000..dd9b7a7 --- /dev/null +++ b/website/src/routes/api/gambling/mines/cashout/+server.ts @@ -0,0 +1,76 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis } from '$lib/server/redis'; +import { getSessionKey } from '$lib/server/games/mines'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { sessionToken } = await request.json(); + const sessionRaw = await redis.get(getSessionKey(sessionToken)); + const game = sessionRaw ? JSON.parse(sessionRaw) : null; + const userId = Number(session.user.id); + + if (!game) { + return json({ error: 'Invalid session' }, { status: 400 }); + } + + const result = await db.transaction(async (tx) => { + const [userData] = await tx + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + let payout: number; + let newBalance: number; + + // If no tiles revealed, treat as abort and return full bet. + if (game.revealedTiles.length === 0) { + payout = game.betAmount; + newBalance = Math.round((currentBalance + payout) * 100000000) / 100000000; + } else { + payout = game.betAmount * game.currentMultiplier; + const roundedPayout = Math.round(payout * 100000000) / 100000000; + newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000; + } + + await tx + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + await redis.del(getSessionKey(sessionToken)); + + return { + newBalance, + payout, + amountWagered: game.betAmount, + isAbort: game.revealedTiles.length === 0, + minePositions: game.minePositions + }; + }); + + return json(result); + } catch (e) { + console.error('Mines cashout error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; \ No newline at end of file diff --git a/website/src/routes/api/gambling/mines/reveal/+server.ts b/website/src/routes/api/gambling/mines/reveal/+server.ts new file mode 100644 index 0000000..e36744d --- /dev/null +++ b/website/src/routes/api/gambling/mines/reveal/+server.ts @@ -0,0 +1,127 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { calculateMultiplier } from '$lib/server/games/mines'; +import type { RequestHandler } from './$types'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis, } from '$lib/server/redis'; +import { getSessionKey } from '$lib/server/games/mines'; + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { sessionToken, tileIndex } = await request.json(); + + if (!Number.isInteger(tileIndex) || tileIndex < 0 || tileIndex > 24) { + return json({ error: 'Invalid tileIndex' }, { status: 400 }); + } + + const sessionRaw = await redis.get(getSessionKey(sessionToken)); + const game = sessionRaw ? JSON.parse(sessionRaw) : null; + + if (!game) { + return json({ error: 'Invalid session' }, { status: 400 }); + } + + if (game.revealedTiles.includes(tileIndex)) { + return json({ error: 'Tile already revealed' }, { status: 400 }); + } + + game.lastActivity = Date.now(); + + if (game.minePositions.includes(tileIndex)) { + game.status = 'lost'; + const minePositions = game.minePositions; + + const userId = Number(session.user.id); + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + + await db + .update(user) + .set({ + baseCurrencyBalance: currentBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + await redis.del(getSessionKey(sessionToken)); + + return json({ + hitMine: true, + minePositions, + newBalance: currentBalance, + status: 'lost', + amountWagered: game.betAmount + }); + } + + // Safe tile + game.revealedTiles.push(tileIndex); + game.currentMultiplier = calculateMultiplier( + game.revealedTiles.length, + game.mineCount, + game.betAmount + ); + + if (game.revealedTiles.length === 25 - game.mineCount) { + game.status = 'won'; + const userId = Number(session.user.id); + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const payout = game.betAmount * game.currentMultiplier; + const roundedPayout = Math.round(payout * 100000000) / 100000000; + const newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000; + + await db + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + await redis.del(getSessionKey(sessionToken)); + + return json({ + hitMine: false, + currentMultiplier: game.currentMultiplier, + status: 'won', + newBalance, + payout + }); + } + + await redis.set(getSessionKey(sessionToken), JSON.stringify(game)); + + return json({ + hitMine: false, + currentMultiplier: game.currentMultiplier, + status: game.status + }); + } catch (e) { + console.error('Mines reveal error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; \ No newline at end of file diff --git a/website/src/routes/api/gambling/mines/start/+server.ts b/website/src/routes/api/gambling/mines/start/+server.ts new file mode 100644 index 0000000..b7ab5f6 --- /dev/null +++ b/website/src/routes/api/gambling/mines/start/+server.ts @@ -0,0 +1,100 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis } from '$lib/server/redis'; +import { getSessionKey } from '$lib/server/games/mines'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { betAmount, mineCount } = await request.json(); + const userId = Number(session.user.id); + + if (!betAmount || betAmount <= 0 || !mineCount || mineCount < 3 || mineCount > 24) { + return json({ error: 'Invalid bet amount or mine count' }, { status: 400 }); + } + + if (betAmount > 1000000) { + return json({ error: 'Bet amount too large' }, { status: 400 }); + } + + const result = await db.transaction(async (tx) => { + const [userData] = await tx + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const roundedAmount = Math.round(betAmount * 100000000) / 100000000; + const roundedBalance = Math.round(currentBalance * 100000000) / 100000000; + + if (roundedAmount > roundedBalance) { + throw new Error(`Insufficient funds. You need *${roundedAmount.toFixed(2)} but only have *${roundedBalance.toFixed(2)}`); + } + + // Generate mine positions + const positions = new Set(); + while (positions.size < mineCount) { + positions.add(Math.floor(Math.random() * 25)); + } + + // transaction token for authentication stuff + const randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + const sessionToken = Array.from(randomBytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + const now = Date.now(); + const newBalance = roundedBalance - roundedAmount; + + await redis.set( + getSessionKey(sessionToken), + JSON.stringify({ + sessionToken, + betAmount: roundedAmount, + mineCount, + minePositions: Array.from(positions), + revealedTiles: [], + startTime: now, + lastActivity: now, + currentMultiplier: 1, + status: 'active', + userId + }) + ); + + // Update user balance + await tx + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + return { + sessionToken, + newBalance + }; + }); + + return json(result); + } catch (e) { + console.error('Mines start error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; \ No newline at end of file diff --git a/website/src/routes/gambling/+page.svelte b/website/src/routes/gambling/+page.svelte index 4df7bde..996aeb4 100644 --- a/website/src/routes/gambling/+page.svelte +++ b/website/src/routes/gambling/+page.svelte @@ -1,6 +1,7 @@