├── .gitignore ├── db-service ├── .gitignore ├── migrations │ ├── 20250510140151_creating_database_schema.sql │ ├── 20250712084500_market_expiry_status_update_cron.sql │ └── 20250511185915_adding_updated_at_triggers.sql ├── src │ ├── schema │ │ ├── mod.rs │ │ └── user_transactions.rs │ ├── lib.rs │ ├── utils │ │ └── mod.rs │ └── pagination.rs ├── .sqlx │ ├── query-94fad0ce463675da937024e1b48f601737a15c948ea331eec674855508ff3d0e.json │ ├── query-c67839bd0236b2649f0b4e747c7e63f243bdae338ab38c36483ab7aee7ea3645.json │ ├── query-7203474d8b51beb1b57f60f693b2d58b3869cb1a3851ff50d80b020da751f045.json │ ├── query-2ca45a196eb94bbce98ed447d7cba1e8ee13675dc2d01dce5dab4e8947e80f4b.json │ ├── query-3e7ae91019299fd72310b34477a8ecedc42ea47077358f0fc31b0f93aa7c2906.json │ ├── query-6fc630f27d3d3742e29ece78f3b79549a1b9349c3c5be3829535dbad60d6efd2.json │ ├── query-c4108342974345c21df71dc2a0d0fd888fe04c4103c55187f8320939fe3cbf5f.json │ ├── query-a875cc02e19624440f0cc2ddfdfa18a8d5cfeb74f1be35ddce4f7fdb8799f0c8.json │ ├── query-d8aaf64d7fddd8fd8f423ddf69072a3fea237fbffbfbb7a2bf1b1adc11c045ca.json │ ├── query-91215a121a4204abefaeea09948ff2acc41472775717b70adac676af0e2c8c4b.json │ ├── query-83f4f175c42f942f7516714742768b50b2a03ef5d919edb4fce3b2df7bde0f71.json │ ├── query-915ddbabff23c487b42bd0de7ae464b11f7222653593f9b0dabdc5d63d3d58e6.json │ ├── query-e5b5a989c149e01730594e0b1fde40a6ca21c74925760067d3a4fad23f82133d.json │ ├── query-3602c5e6b25a8c1d0914642b945cf8b08ce2f83d31c6c0a947a39147e5c50d36.json │ ├── query-58224c9555ed86c399af1de18c19e99219c3fb2c7cfe844a53bbf4735d4faee8.json │ ├── query-bb97f2bc468dc2eef671e92573618b76482714d717490dddc67d56c53f54dd43.json │ ├── query-cc50bacd9f45932bd1eb9b1bd418013857386363d20530b926a6bf776d268cda.json │ ├── query-cf2755ca9dfe18a0b14e421991253dc0ceffdbb5b26fb05059043e54b1b7a6a7.json │ ├── query-7929dc94bbc48788fd63faea4e7bd0c30af5d27299cd7e233122d965a135b8e6.json │ ├── query-5011f4a665cb8905c1daf331fbbf874cc23a88b2f52c82274fb55235df58bdc8.json │ ├── query-10c169c01176ed93e662dbb89d69ebdd1f791a40ee4f3939f9f49c822926714e.json │ ├── query-79a4a9089b2d5a970fc17992045ccd567b813c5cef731c981a279bbeb4f8682d.json │ ├── query-745a2afe244a16070631760b88ea4a159cac489f3c58c8f2e3766805aeceaaea.json │ ├── query-87509857401e65de9da78f71b98113f91b612df227665c2c739a00b1b729c889.json │ ├── query-cccdb2bbd51432463f65eff9517bad135a15ad99e50fc9562cb394e3d68c38c5.json │ ├── query-03e6ad429665b989e171bf7563abf9ab2f362a673e4b3d780643a215f8857f80.json │ ├── query-f496e6ee7345609953e70fddaf2597a9e679b0a682623c1cd88b92dff538f628.json │ ├── query-6a3532430782df1151628ba3890c2de75ec3bf347e71ee9318bfbffabea8a7a3.json │ ├── query-026c1e9ae0d63ab2a5077c04b3103274c321202383e90e94b3aa07670e2aeaf7.json │ ├── query-3673e672daeb6b4656b07cbe50b07208faa9e1f650f87cd7cb36942ab17a503d.json │ ├── query-abb9570ad6d4cc6dd8d0e55ac167c30430643edb5d2aca74f66ac0ac61e29cb9.json │ ├── query-de3559807340810a2a40bd545b2a99c3b432643a8a526d0805630f8456b198c7.json │ ├── query-90d29bab84a5b59c50aad52bdc43c81be05a816f93471ee0b17db5e40153c773.json │ └── query-7fe1cc922d607079adede58de4d4a9a08ee0b9cb04d5c525c5bc870935e65024.json └── Cargo.toml ├── .dockerignore ├── grpc-service ├── .gitignore ├── src │ ├── generated │ │ ├── mod.rs │ │ ├── descriptor.bin │ │ └── common.rs │ ├── lib.rs │ ├── .DS_Store │ ├── utils │ │ ├── macros.rs │ │ └── timeframe.rs │ ├── state.rs │ └── main.rs ├── .DS_Store ├── proto │ ├── common.proto │ └── price.proto ├── Cargo.toml └── build.rs ├── service-api ├── .gitignore ├── src │ ├── utils │ │ ├── mod.rs │ │ ├── types.rs │ │ ├── macros.rs │ │ └── middleware.rs │ ├── routes │ │ ├── admin │ │ │ ├── mod.rs │ │ │ └── markets │ │ │ │ └── mod.rs │ │ ├── user │ │ │ ├── trades │ │ │ │ ├── mod.rs │ │ │ │ └── get_user_trades.rs │ │ │ ├── mod.rs │ │ │ ├── orders │ │ │ │ ├── mod.rs │ │ │ │ ├── get_all_users_orders.rs │ │ │ │ └── cancel_order.rs │ │ │ ├── metadata.rs │ │ │ ├── holdings.rs │ │ │ └── profile.rs │ │ ├── mod.rs │ │ └── login.rs │ ├── main.rs │ ├── bloom_f.rs │ └── state.rs └── Cargo.toml ├── proto-defs ├── src │ ├── lib.rs │ └── proto_types │ │ ├── mod.rs │ │ ├── ws_market_price.rs │ │ └── order_book.rs ├── Cargo.toml ├── proto │ └── ws_server │ │ ├── market_price.proto │ │ ├── order_book.proto │ │ └── common.proto └── build.rs ├── app ├── .dockerignore ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── market │ │ │ └── [id] │ │ │ ├── _components │ │ │ ├── HoldingsInfoClient.tsx │ │ │ └── TradeForm.tsx │ │ │ └── TabsClient.tsx │ ├── hooks │ │ ├── useUserInfo.ts │ │ ├── useModal.ts │ │ └── useRevalidate.ts │ ├── utils │ │ ├── constants.ts │ │ ├── interactions │ │ │ └── axios.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ └── api.ts │ │ ├── grpc │ │ │ └── clients.ts │ │ ├── index.ts │ │ └── protoHelpers.ts │ ├── components │ │ ├── ui │ │ │ ├── provider.tsx │ │ │ ├── toaster.tsx │ │ │ └── tooltip.tsx │ │ ├── EmptyStateCustom.tsx │ │ ├── NavbarNotificationButton.tsx │ │ ├── modals │ │ │ └── index.tsx │ │ ├── Navbar.tsx │ │ └── GoogleSignInButton.tsx │ └── generated │ │ └── grpc_service_types │ │ └── price.client.ts ├── postcss.config.mjs ├── public │ ├── vercel.svg │ ├── proto │ │ ├── proto_defs │ │ │ └── ws_server │ │ │ │ ├── market_price.proto │ │ │ │ ├── order_book.proto │ │ │ │ └── common.proto │ │ └── grpc_services │ │ │ ├── common.proto │ │ │ └── price.proto │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg ├── .prettierrc ├── eslint.config.mjs ├── next.config.ts ├── .gitignore ├── tsconfig.json ├── client.Dockerfile ├── package.json ├── README.md └── Makefile ├── .DS_Store ├── order-service ├── src │ ├── handlers │ │ ├── mod.rs │ │ ├── nats_handler │ │ │ ├── add_order_handler.rs │ │ │ ├── cancel_order_handler.rs │ │ │ └── update_order_handler.rs │ │ └── ws_handler │ │ │ ├── mod.rs │ │ │ └── handle_text_messages.rs │ ├── order_book │ │ └── mod.rs │ └── utils │ │ └── mod.rs └── Cargo.toml ├── websocket-service ├── src │ ├── core │ │ ├── message_handlers │ │ │ ├── channel_handlers │ │ │ │ ├── mod.rs │ │ │ │ └── price_posters.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── handle_connection.rs │ ├── state.rs │ ├── nats_handler │ │ ├── handle_market_book_update.rs │ │ └── mod.rs │ └── main.rs └── Cargo.toml ├── pg-docker ├── init-scripts │ └── init.sql ├── entrypoint.sh └── pg.Dockerfile ├── assets ├── architecture_1.png ├── architecture_v2.png ├── architecture_v3.png ├── architecture_v4.png ├── order-ops-arch-1.png ├── architecture_v2_1.png └── order-book-reading-arch-1.png ├── queries └── pg_queries │ ├── ResetDB.sql │ ├── dropConstraints.sql │ ├── cron_filter.sql │ ├── UserTable.sql │ ├── marketTable.sql │ ├── user_trades.sql │ ├── userHoldings.sql │ ├── orders.sql │ └── metadata.sql ├── scripts ├── payload1.json ├── payload2.json ├── tmux.sh ├── result_sell.txt ├── result_buy.txt └── stress-test.sh ├── utility-helpers ├── src │ ├── message_pack_helper.rs │ ├── ws │ │ ├── mod.rs │ │ ├── publisher_types.rs │ │ └── types.rs │ ├── redis │ │ └── keys.rs │ ├── kafka_topics.rs │ ├── lib.rs │ ├── macros.rs │ ├── symmetric.rs │ └── nats_helper │ │ └── types.rs └── Cargo.toml ├── auth-service ├── Cargo.toml └── src │ ├── token_services.rs │ └── types.rs ├── Dockerfile └── Cargo.toml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .env -------------------------------------------------------------------------------- /db-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .env 3 | .git -------------------------------------------------------------------------------- /grpc-service/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /service-api/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /proto-defs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod proto_types; 2 | -------------------------------------------------------------------------------- /app/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .next 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/.DS_Store -------------------------------------------------------------------------------- /order-service/src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod nats_handler; 2 | pub mod ws_handler; 3 | -------------------------------------------------------------------------------- /grpc-service/src/generated/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod price; 2 | pub mod markets; 3 | pub mod common; 4 | -------------------------------------------------------------------------------- /service-api/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod macros; 2 | pub mod middleware; 3 | pub mod types; 4 | -------------------------------------------------------------------------------- /websocket-service/src/core/message_handlers/channel_handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod price_posters; 2 | -------------------------------------------------------------------------------- /grpc-service/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/grpc-service/.DS_Store -------------------------------------------------------------------------------- /pg-docker/init-scripts/init.sql: -------------------------------------------------------------------------------- 1 | -- Enable the pg_cron extension 2 | CREATE EXTENSION IF NOT EXISTS pg_cron; -------------------------------------------------------------------------------- /app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/app/src/app/favicon.ico -------------------------------------------------------------------------------- /grpc-service/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod procedures; 2 | pub mod state; 3 | pub mod utils; 4 | 5 | pub mod generated; 6 | -------------------------------------------------------------------------------- /assets/architecture_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/architecture_1.png -------------------------------------------------------------------------------- /assets/architecture_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/architecture_v2.png -------------------------------------------------------------------------------- /assets/architecture_v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/architecture_v3.png -------------------------------------------------------------------------------- /assets/architecture_v4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/architecture_v4.png -------------------------------------------------------------------------------- /assets/order-ops-arch-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/order-ops-arch-1.png -------------------------------------------------------------------------------- /grpc-service/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/grpc-service/src/.DS_Store -------------------------------------------------------------------------------- /proto-defs/src/proto_types/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod order_book; 2 | pub mod ws_market_price; 3 | pub mod ws_common_types; 4 | -------------------------------------------------------------------------------- /app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /assets/architecture_v2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/architecture_v2_1.png -------------------------------------------------------------------------------- /app/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @keyframes blinker { 4 | 50% { 5 | opacity: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /queries/pg_queries/ResetDB.sql: -------------------------------------------------------------------------------- 1 | drop schema polymarket cascade; 2 | drop schema public cascade; 3 | 4 | create schema public; -------------------------------------------------------------------------------- /order-service/src/order_book/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod global_book; 2 | pub(crate) mod market_book; 3 | pub(crate) mod outcome_book; 4 | -------------------------------------------------------------------------------- /assets/order-book-reading-arch-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/assets/order-book-reading-arch-1.png -------------------------------------------------------------------------------- /db-service/migrations/20250510140151_creating_database_schema.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | 3 | CREATE SCHEMA IF NOT EXISTS "polymarket"; -------------------------------------------------------------------------------- /grpc-service/src/generated/descriptor.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CryptomSol/prediction-market/HEAD/grpc-service/src/generated/descriptor.bin -------------------------------------------------------------------------------- /app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order-service/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod update_matched_orders; 2 | pub mod update_services; 3 | 4 | pub type OrderServiceError = Box; 5 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": false, 4 | "semi": true, 5 | "arrowParens": "always", 6 | "plugins": ["prettier-plugin-tailwindcss"] 7 | } -------------------------------------------------------------------------------- /scripts/payload1.json: -------------------------------------------------------------------------------- 1 | { 2 | "market_id": "898a074c-48da-49e7-90f4-417e6e5e5886", 3 | "price": 0.4, 4 | "quantity": 12, 5 | "side": "BUY", 6 | "outcome_side": "YES" 7 | } 8 | -------------------------------------------------------------------------------- /scripts/payload2.json: -------------------------------------------------------------------------------- 1 | { 2 | "market_id": "898a074c-48da-49e7-90f4-417e6e5e5886", 3 | "price": 0.34, 4 | "quantity": 12, 5 | "side": "SELL", 6 | "outcome_side": "YES" 7 | } 8 | -------------------------------------------------------------------------------- /db-service/src/schema/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod enums; 2 | pub mod market; 3 | pub mod orders; 4 | pub mod user_holdings; 5 | pub mod user_trades; 6 | pub mod user_transactions; 7 | pub mod users; 8 | -------------------------------------------------------------------------------- /service-api/src/routes/admin/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::Router; 2 | 3 | use crate::state::AppState; 4 | 5 | pub mod markets; 6 | 7 | pub fn router() -> Router { 8 | Router::new().nest("/market", markets::market_router()) 9 | } 10 | -------------------------------------------------------------------------------- /queries/pg_queries/dropConstraints.sql: -------------------------------------------------------------------------------- 1 | SELECT conname 2 | FROM pg_constraint 3 | WHERE conrelid = 'polymarket.orders'::regclass 4 | AND contype = 'c' 5 | AND pg_get_constraintdef(oid) ILIKE '%price%'; 6 | 7 | ALTER TABLE polymarket.orders DROP CONSTRAINT orders_price_check; -------------------------------------------------------------------------------- /service-api/src/routes/user/trades/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{Router, routing::get}; 2 | 3 | use crate::state::AppState; 4 | 5 | mod get_user_trades; 6 | 7 | pub fn router() -> Router { 8 | Router::new().route("/", get(get_user_trades::get_user_trades)) 9 | } 10 | -------------------------------------------------------------------------------- /proto-defs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proto-defs" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | prost = { workspace = true } 8 | prost-types = { workspace = true } 9 | serde = { workspace = true } 10 | 11 | 12 | [build-dependencies] 13 | prost-build = "0.13.5" 14 | -------------------------------------------------------------------------------- /proto-defs/proto/ws_server/market_price.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ws_market_price; 4 | 5 | 6 | message WsParamsPayload { // stringified payload linked to -> (string params) in WsData 7 | string market_id = 1; 8 | double yes_price = 2; 9 | double no_price = 3; 10 | uint64 timestamp = 4; 11 | } -------------------------------------------------------------------------------- /app/public/proto/proto_defs/ws_server/market_price.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ws_market_price; 4 | 5 | 6 | message WsParamsPayload { // stringified payload linked to -> (string params) in WsData 7 | string market_id = 1; 8 | double yes_price = 2; 9 | double no_price = 3; 10 | uint64 timestamp = 4; 11 | } -------------------------------------------------------------------------------- /service-api/src/utils/types.rs: -------------------------------------------------------------------------------- 1 | use axum::{http::StatusCode, response::Response}; 2 | use serde::Deserialize; 3 | 4 | pub type ReturnType = (StatusCode, Response); 5 | 6 | #[derive(Deserialize, Clone, Debug)] 7 | pub struct PaginationRequestQuery { 8 | pub page: u64, 9 | #[serde(rename = "pageSize")] 10 | pub page_size: u64, 11 | } 12 | -------------------------------------------------------------------------------- /app/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/hooks/useUserInfo.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | import { UserGetters } from "@/utils/interactions/dataGetter"; 4 | 5 | export default function useUserInfo() { 6 | const { data, isLoading } = useQuery({ 7 | queryKey: ["userData"], 8 | queryFn: UserGetters.getUserData, 9 | }); 10 | return { 11 | data, 12 | isLoading, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /pg-docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Default value for POSTGRES_DB if not set 5 | POSTGRES_DB=${POSTGRES_DB:-postgres} 6 | 7 | # Append the cron.database_name configuration to postgresql.conf.sample 8 | echo "cron.database_name='${POSTGRES_DB}'" >> /usr/share/postgresql/postgresql.conf.sample 9 | 10 | # Execute the original entrypoint script 11 | exec docker-entrypoint.sh "$@" -------------------------------------------------------------------------------- /utility-helpers/src/message_pack_helper.rs: -------------------------------------------------------------------------------- 1 | pub fn serialize_to_message_pack( 2 | data: &T, 3 | ) -> Result, rmp_serde::encode::Error> { 4 | rmp_serde::to_vec_named(data) 5 | } 6 | 7 | pub fn deserialize_from_message_pack( 8 | data: &[u8], 9 | ) -> Result { 10 | rmp_serde::from_slice(data) 11 | } 12 | -------------------------------------------------------------------------------- /utility-helpers/src/ws/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json; 3 | 4 | pub mod publisher_types; 5 | pub mod types; 6 | 7 | pub fn to_json_string(val: &T) -> Result 8 | where 9 | T: Serialize, 10 | { 11 | serde_json::to_string(val) 12 | } 13 | 14 | pub fn from_json_str(s: &str) -> Result 15 | where 16 | T: for<'de> Deserialize<'de>, 17 | { 18 | serde_json::from_str(s) 19 | } 20 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-94fad0ce463675da937024e1b48f601737a15c948ea331eec674855508ff3d0e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "CALL polymarket.update_order_and_process_trade($1, $2, $3);", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Uuid", 10 | "Numeric" 11 | ] 12 | }, 13 | "nullable": [] 14 | }, 15 | "hash": "94fad0ce463675da937024e1b48f601737a15c948ea331eec674855508ff3d0e" 16 | } 17 | -------------------------------------------------------------------------------- /app/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // global websocket instance 2 | let instance: WebSocket | null = null; 3 | 4 | export function getWsInstance(forceReconnect?: boolean) { 5 | if (forceReconnect && instance) { 6 | instance.close(); 7 | instance = null; 8 | } 9 | if (!instance) { 10 | instance = new WebSocket(process.env.NEXT_PUBLIC_WS_API_URL!); 11 | instance.onopen = () => { 12 | console.log("WebSocket connection established."); 13 | }; 14 | } 15 | return instance; 16 | } 17 | -------------------------------------------------------------------------------- /service-api/src/routes/admin/markets/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{Router, routing::post}; 2 | 3 | use crate::state::AppState; 4 | 5 | pub mod create_market; 6 | pub mod finalize_market; 7 | pub mod initialize_market; 8 | 9 | pub fn market_router() -> Router { 10 | Router::new() 11 | .route("/create", post(create_market::create_new_market)) 12 | .route("/initialize", post(initialize_market::initialize_market)) 13 | .route("/finalize", post(finalize_market::finalize_market)) 14 | } 15 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-c67839bd0236b2649f0b4e747c7e63f243bdae338ab38c36483ab7aee7ea3645.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE polymarket.user_holdings\n SET shares = 0\n WHERE market_id = $1\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "c67839bd0236b2649f0b4e747c7e63f243bdae338ab38c36483ab7aee7ea3645" 14 | } 15 | -------------------------------------------------------------------------------- /app/src/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | export type ModalType = `update-order-${string}`; 4 | interface ModelState { 5 | type: ModalType | null; 6 | isOpen: boolean; 7 | open: (type: ModalType) => void; 8 | close: () => void; 9 | } 10 | 11 | const useModal = create((set) => ({ 12 | type: null, 13 | isOpen: false, 14 | open: (type: ModalType) => set({ type, isOpen: true }), 15 | close: () => set({ type: null, isOpen: false }), 16 | })); 17 | 18 | export default useModal; 19 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-7203474d8b51beb1b57f60f693b2d58b3869cb1a3851ff50d80b020da751f045.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE \"polymarket\".\"markets\" \n SET liquidity_b = $1\n WHERE id = $2\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Numeric", 9 | "Uuid" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "7203474d8b51beb1b57f60f693b2d58b3869cb1a3851ff50d80b020da751f045" 15 | } 16 | -------------------------------------------------------------------------------- /app/src/utils/interactions/axios.ts: -------------------------------------------------------------------------------- 1 | import { Axios } from "axios"; 2 | 3 | let axiosInstance: Axios; 4 | 5 | function createAxiosInstance() { 6 | if (!axiosInstance) { 7 | axiosInstance = new Axios({ 8 | baseURL: process.env.NEXT_PUBLIC_SERVICE_API_URL, 9 | headers: { 10 | "Content-Type": "application/json", 11 | }, 12 | timeout: 10000, // 10 seconds timeout 13 | }); 14 | } 15 | return axiosInstance; 16 | } 17 | 18 | axiosInstance = createAxiosInstance(); 19 | 20 | export { axiosInstance }; 21 | -------------------------------------------------------------------------------- /queries/pg_queries/cron_filter.sql: -------------------------------------------------------------------------------- 1 | select * from cron.job; 2 | 3 | SELECT * FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10; 4 | 5 | 6 | ̇ 7 | -- CREATE OR REPLACE FUNCTION polymarket.close_market(market_id UUID) 8 | -- RETURNS VOID AS $$ 9 | -- BEGIN 10 | -- UPDATE polymarket.markets 11 | -- SET status = 'closed'::polymarket.market_status, updated_at = CURRENT_TIMESTAMP 12 | -- WHERE id = market_id AND market_expiry <= CURRENT_TIMESTAMP AND status = 'open'::polymarket.market_status; 13 | -- END; 14 | -- $$ LANGUAGE plpgsql; 15 | -------------------------------------------------------------------------------- /proto-defs/proto/ws_server/order_book.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package order_book; 4 | 5 | 6 | // which price, how many shares (total quantity) and how many users on same price (histogram) 7 | message OrderLevel { 8 | double price = 1; 9 | double shares = 2; 10 | uint32 users = 3; 11 | } 12 | 13 | message OrderBook { 14 | repeated OrderLevel bids = 2; 15 | repeated OrderLevel asks = 3; 16 | } 17 | 18 | message MarketBook { 19 | string market_id = 1; 20 | OrderBook yes_book = 2; 21 | OrderBook no_book = 3; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /scripts/tmux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SESSION="poly" 3 | 4 | tmux new-session -d -s $SESSION 5 | 6 | # Pane 0: top-left 7 | tmux send-keys -t $SESSION 'make start-service-api' C-m 8 | 9 | # Split vertical (bottom) 10 | tmux split-window -v -t $SESSION 11 | tmux send-keys -t $SESSION.1 'make start-grpc-server' C-m 12 | 13 | # Select top pane and split horizontally (top-right) 14 | tmux select-pane -t $SESSION.0 15 | tmux split-window -h -t $SESSION 16 | tmux send-keys -t $SESSION.2 'make start-websocket-server' C-m 17 | 18 | # Attach 19 | tmux attach-session -t $SESSION -------------------------------------------------------------------------------- /app/public/proto/proto_defs/ws_server/order_book.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package order_book; 4 | 5 | 6 | // which price, how many shares (total quantity) and how many users on same price (histogram) 7 | message OrderLevel { 8 | double price = 1; 9 | double shares = 2; 10 | uint32 users = 3; 11 | } 12 | 13 | message OrderBook { 14 | repeated OrderLevel bids = 2; 15 | repeated OrderLevel asks = 3; 16 | } 17 | 18 | message MarketBook { 19 | string market_id = 1; 20 | OrderBook yes_book = 2; 21 | OrderBook no_book = 3; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /service-api/src/routes/user/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{Router, routing::get}; 2 | 3 | use crate::state::AppState; 4 | 5 | pub mod holdings; 6 | pub mod metadata; 7 | pub mod orders; 8 | pub mod profile; 9 | pub mod trades; 10 | 11 | pub fn router() -> Router { 12 | Router::new() 13 | .nest("/orders", orders::router()) 14 | .nest("/trades", trades::router()) 15 | .route("/profile", get(profile::get_profile)) 16 | .route("/metadata", get(metadata::get_metadata)) 17 | .route("/holdings", get(holdings::get_user_holdings)) 18 | } 19 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-2ca45a196eb94bbce98ed447d7cba1e8ee13675dc2d01dce5dab4e8947e80f4b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT id FROM \"polymarket\".\"users\"\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [] 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "2ca45a196eb94bbce98ed447d7cba1e8ee13675dc2d01dce5dab4e8947e80f4b" 20 | } 21 | -------------------------------------------------------------------------------- /proto-defs/src/proto_types/ws_market_price.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | /// stringified payload linked to -> (string params) in WsData 3 | #[derive(serde::Serialize, serde::Deserialize)] 4 | #[derive(Clone, PartialEq, ::prost::Message)] 5 | pub struct WsParamsPayload { 6 | #[prost(string, tag = "1")] 7 | pub market_id: ::prost::alloc::string::String, 8 | #[prost(double, tag = "2")] 9 | pub yes_price: f64, 10 | #[prost(double, tag = "3")] 11 | pub no_price: f64, 12 | #[prost(uint64, tag = "4")] 13 | pub timestamp: u64, 14 | } 15 | -------------------------------------------------------------------------------- /db-service/migrations/20250712084500_market_expiry_status_update_cron.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | 3 | CREATE OR REPLACE FUNCTION polymarket.close_market(market_id UUID) 4 | RETURNS VOID AS $$ 5 | BEGIN 6 | UPDATE polymarket.markets 7 | SET status = 'closed'::polymarket.market_status, updated_at = CURRENT_TIMESTAMP 8 | WHERE id = market_id 9 | AND status = 'open'::polymarket.market_status; 10 | 11 | -- Remove the cron job for this market 12 | PERFORM cron.unschedule('close_market_job_' || market_id::text); 13 | END; 14 | $$ LANGUAGE plpgsql; 15 | -------------------------------------------------------------------------------- /db-service/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | 3 | pub mod pagination; 4 | pub mod procedures; 5 | pub mod schema; 6 | pub mod utils; 7 | 8 | pub struct DbService; 9 | 10 | impl DbService { 11 | pub async fn run_migrations(pg_pool: &sqlx::PgPool) -> Result<(), Box> { 12 | sqlx::migrate!("./migrations") 13 | .run(pg_pool) 14 | .await 15 | .map_err(|e| format!("Migration failed: {}", e))?; 16 | 17 | tracing::info!("Database migrations completed successfully."); 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | rules: { 16 | "@typescript-eslint/no-explicit-any": "nothing", 17 | }, 18 | }, 19 | ]; 20 | 21 | export default eslintConfig; 22 | -------------------------------------------------------------------------------- /grpc-service/proto/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package common; 4 | 5 | message PageInfo { 6 | uint64 page = 1; 7 | uint64 page_size = 2; 8 | uint64 total_items = 3; 9 | uint64 total_pages = 4; 10 | } 11 | 12 | message PageRequest { 13 | uint64 page = 1; 14 | uint64 page_size = 2; 15 | } 16 | 17 | 18 | enum Timeframe { 19 | TIMEFRAME_UNSPECIFIED = 0; 20 | TIMEFRAME_ONE_HOUR = 1; 21 | TIMEFRAME_SIX_HOUR = 2; 22 | TIMEFRAME_ONE_DAY = 3; 23 | TIMEFRAME_ONE_WEEK = 4; 24 | TIMEFRAME_ONE_MONTH = 5; 25 | TIMEFRAME_ALL = 6; 26 | } -------------------------------------------------------------------------------- /app/public/proto/grpc_services/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package common; 4 | 5 | message PageInfo { 6 | uint64 page = 1; 7 | uint64 page_size = 2; 8 | uint64 total_items = 3; 9 | uint64 total_pages = 4; 10 | } 11 | 12 | message PageRequest { 13 | uint64 page = 1; 14 | uint64 page_size = 2; 15 | } 16 | 17 | 18 | enum Timeframe { 19 | TIMEFRAME_UNSPECIFIED = 0; 20 | TIMEFRAME_ONE_HOUR = 1; 21 | TIMEFRAME_SIX_HOUR = 2; 22 | TIMEFRAME_ONE_DAY = 3; 23 | TIMEFRAME_ONE_WEEK = 4; 24 | TIMEFRAME_ONE_MONTH = 5; 25 | TIMEFRAME_ALL = 6; 26 | } -------------------------------------------------------------------------------- /utility-helpers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utility-helpers" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | aes = "0.8.4" 8 | aes-gcm = "0.10.3" 9 | jsonwebtoken = { workspace = true } 10 | dotenv = { workspace = true } 11 | serde = { workspace = true } 12 | serde_json = { workspace = true } 13 | base64 = { workspace = true } 14 | uuid = { workspace = true } 15 | rust_decimal = { workspace = true } 16 | deadpool-redis = { workspace = true } 17 | tracing = { workspace = true } 18 | rmp-serde = { workspace = true } 19 | 20 | 21 | proto-defs = { path = "../proto-defs" } 22 | -------------------------------------------------------------------------------- /auth-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auth-service" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | db-service = { path = "../db-service" } 8 | utility-helpers = { path = "../utility-helpers" } 9 | jsonwebtoken = "9.3.1" 10 | aes = "0.8.4" 11 | aes-gcm = "0.10.3" 12 | chrono = { workspace = true } 13 | serde = { workspace = true } 14 | serde_json = { workspace = true } 15 | dotenv = { workspace = true } 16 | reqwest = { workspace = true } 17 | base64 = { workspace = true } 18 | tokio = { workspace = true } 19 | uuid = { workspace = true } 20 | sqlx = { workspace = true } 21 | -------------------------------------------------------------------------------- /websocket-service/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::extract::ws::{Message, WebSocket}; 4 | use futures::{SinkExt, stream::SplitSink}; 5 | use tokio::sync::Mutex; 6 | 7 | pub mod client_manager; 8 | pub mod handle_connection; 9 | pub mod message_handlers; 10 | 11 | // mutex because rx.next() method requires mutable access, so one reader and writer at a time... 12 | pub type SafeSender = Arc>>; 13 | 14 | pub(super) async fn send_message(tx: &SafeSender, message: Message) -> Result<(), axum::Error> { 15 | tx.lock().await.send(message).await 16 | } 17 | -------------------------------------------------------------------------------- /app/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | experimental: { 6 | optimizePackageImports: ["@chakra-ui/react"], 7 | }, 8 | output: "standalone", 9 | env: { 10 | NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, 11 | NEXT_PUBLIC_SERVICE_API_URL: process.env.NEXT_PUBLIC_SERVICE_API_URL, 12 | NEXT_PUBLIC_GRPC_API_URL: process.env.NEXT_PUBLIC_GRPC_API_URL, 13 | NEXT_PUBLIC_WS_API_URL: process.env.NEXT_PUBLIC_WS_API_URL, 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-3e7ae91019299fd72310b34477a8ecedc42ea47077358f0fc31b0f93aa7c2906.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) as total_count\n FROM \"polymarket\".\"markets\"\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "total_count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [] 14 | }, 15 | "nullable": [ 16 | null 17 | ] 18 | }, 19 | "hash": "3e7ae91019299fd72310b34477a8ecedc42ea47077358f0fc31b0f93aa7c2906" 20 | } 21 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-6fc630f27d3d3742e29ece78f3b79549a1b9349c3c5be3829535dbad60d6efd2.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT balance FROM polymarket.users WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "balance", 9 | "type_info": "Numeric" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "6fc630f27d3d3742e29ece78f3b79549a1b9349c3c5be3829535dbad60d6efd2" 22 | } 23 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-c4108342974345c21df71dc2a0d0fd888fe04c4103c55187f8320939fe3cbf5f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT user_id FROM polymarket.orders\n WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Uuid" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "c4108342974345c21df71dc2a0d0fd888fe04c4103c55187f8320939fe3cbf5f" 22 | } 23 | -------------------------------------------------------------------------------- /db-service/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Datelike, NaiveDateTime, Timelike}; 2 | use uuid::Uuid; 3 | 4 | pub fn to_cron_expression(datetime: NaiveDateTime) -> String { 5 | format!( 6 | "{} {} {} {} *", 7 | datetime.minute(), 8 | datetime.hour(), 9 | datetime.day(), 10 | datetime.month() 11 | ) 12 | } 13 | 14 | pub enum CronJobName { 15 | CloseMarket(Uuid), 16 | } 17 | 18 | impl CronJobName { 19 | pub fn to_string(&self) -> String { 20 | match self { 21 | CronJobName::CloseMarket(market_id) => format!("close_market_job_{}", market_id), 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-a875cc02e19624440f0cc2ddfdfa18a8d5cfeb74f1be35ddce4f7fdb8799f0c8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT user_id FROM polymarket.orders\n WHERE id = $1 OR id = $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Uuid" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid", 15 | "Uuid" 16 | ] 17 | }, 18 | "nullable": [ 19 | false 20 | ] 21 | }, 22 | "hash": "a875cc02e19624440f0cc2ddfdfa18a8d5cfeb74f1be35ddce4f7fdb8799f0c8" 23 | } 24 | -------------------------------------------------------------------------------- /grpc-service/proto/price.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package price; 4 | 5 | import "common.proto"; 6 | 7 | service PriceService { 8 | rpc GetPriceDataWithinInterval(GetPriceDataWithinIntervalRequest) returns (GetMarketPriceDataWithinIntervalResponse); 9 | } 10 | 11 | 12 | message GetPriceDataWithinIntervalRequest { 13 | string market_id = 1; 14 | common.Timeframe timeframe = 2; 15 | } 16 | 17 | message PriceData { 18 | double yes_price = 1; 19 | double no_price = 2; 20 | uint64 timestamp = 3; 21 | } 22 | 23 | message GetMarketPriceDataWithinIntervalResponse { 24 | repeated PriceData price_data = 1; 25 | string market_id = 2; 26 | } -------------------------------------------------------------------------------- /app/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /app/public/proto/grpc_services/price.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package price; 4 | 5 | import "common.proto"; 6 | 7 | service PriceService { 8 | rpc GetPriceDataWithinInterval(GetPriceDataWithinIntervalRequest) returns (GetMarketPriceDataWithinIntervalResponse); 9 | } 10 | 11 | 12 | message GetPriceDataWithinIntervalRequest { 13 | string market_id = 1; 14 | common.Timeframe timeframe = 2; 15 | } 16 | 17 | message PriceData { 18 | double yes_price = 1; 19 | double no_price = 2; 20 | uint64 timestamp = 3; 21 | } 22 | 23 | message GetMarketPriceDataWithinIntervalResponse { 24 | repeated PriceData price_data = 1; 25 | string market_id = 2; 26 | } -------------------------------------------------------------------------------- /db-service/.sqlx/query-d8aaf64d7fddd8fd8f423ddf69072a3fea237fbffbfbb7a2bf1b1adc11c045ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) FROM polymarket.orders\n WHERE user_id = $1 AND market_id = $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid", 15 | "Uuid" 16 | ] 17 | }, 18 | "nullable": [ 19 | null 20 | ] 21 | }, 22 | "hash": "d8aaf64d7fddd8fd8f423ddf69072a3fea237fbffbfbb7a2bf1b1adc11c045ca" 23 | } 24 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-91215a121a4204abefaeea09948ff2acc41472775717b70adac676af0e2c8c4b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT balance from polymarket.users where id in (\n $1, $2\n );\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "balance", 9 | "type_info": "Numeric" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid", 15 | "Uuid" 16 | ] 17 | }, 18 | "nullable": [ 19 | false 20 | ] 21 | }, 22 | "hash": "91215a121a4204abefaeea09948ff2acc41472775717b70adac676af0e2c8c4b" 23 | } 24 | -------------------------------------------------------------------------------- /db-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db-service" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | utility-helpers = { path = "../utility-helpers" } 8 | sqlx = { workspace = true } 9 | uuid = { workspace = true } 10 | serde = { workspace = true } 11 | serde_json = { workspace = true } 12 | chrono = { workspace = true } 13 | tracing = { workspace = true } 14 | tracing-subscriber = { workspace = true } 15 | rust_decimal = { workspace = true } 16 | rust_decimal_macros = { workspace = true } 17 | solana-sdk = { workspace = true } 18 | base64 = { workspace = true } 19 | dotenv = { workspace = true } 20 | 21 | [dev-dependencies] 22 | tokio = { workspace = true } 23 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-83f4f175c42f942f7516714742768b50b2a03ef5d919edb4fce3b2df7bde0f71.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) FROM polymarket.user_holdings uh\n JOIN polymarket.markets m ON uh.market_id = m.id\n WHERE uh.user_id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid" 15 | ] 16 | }, 17 | "nullable": [ 18 | null 19 | ] 20 | }, 21 | "hash": "83f4f175c42f942f7516714742768b50b2a03ef5d919edb4fce3b2df7bde0f71" 22 | } 23 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-915ddbabff23c487b42bd0de7ae464b11f7222653593f9b0dabdc5d63d3d58e6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) FROM polymarket.user_trades ut\n JOIN polymarket.markets m ON m.id = ut.market_id\n WHERE ut.user_id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid" 15 | ] 16 | }, 17 | "nullable": [ 18 | null 19 | ] 20 | }, 21 | "hash": "915ddbabff23c487b42bd0de7ae464b11f7222653593f9b0dabdc5d63d3d58e6" 22 | } 23 | -------------------------------------------------------------------------------- /app/src/utils/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Order = { 2 | created_at: string; 3 | filled_quantity: string; 4 | id: string; 5 | market_id: string; 6 | outcome: "yes" | "no" | "settled"; 7 | price: string; 8 | quantity: string; 9 | side: "buy" | "sell"; 10 | status: OrderCategory; 11 | updated_at: string; 12 | user_id: string; 13 | order_type: "limit" | "market"; 14 | }; 15 | 16 | export type OrderCategory = 17 | | "open" 18 | | "cancelled" 19 | | "filled" 20 | | "expired" 21 | | "pending_update" 22 | | "pending_cancel" 23 | | "all"; 24 | 25 | export type PageInfoServiceAPi = { 26 | page: number; 27 | page_size: number; 28 | total_items: number; 29 | total_pages: number; 30 | }; 31 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "ESNext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/src/utils/grpc/clients.ts: -------------------------------------------------------------------------------- 1 | import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport"; 2 | 3 | import { MarketServiceClient } from "@/generated/grpc_service_types/markets.client"; 4 | import { PriceServiceClient } from "@/generated/grpc_service_types/price.client"; 5 | 6 | const transport = new GrpcWebFetchTransport({ 7 | baseUrl: process.env.NEXT_PUBLIC_GRPC_API_URL || "http://localhost:5010", 8 | meta: { 9 | "Access-Control-Allow-Origin": "*", 10 | "some-random-shit": "Admin", 11 | }, 12 | }) as never; 13 | 14 | const marketServiceClient = new MarketServiceClient(transport); 15 | const priceServiceClient = new PriceServiceClient(transport); 16 | 17 | export { marketServiceClient, priceServiceClient }; 18 | -------------------------------------------------------------------------------- /app/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function formatDate(date: T): string { 2 | const dt = new Date(date).toLocaleDateString("en-US", { 3 | year: "numeric", 4 | month: "long", 5 | day: "numeric", 6 | hour: "2-digit", 7 | minute: "2-digit", 8 | second: "2-digit", 9 | }); 10 | 11 | return dt; 12 | } 13 | 14 | export function formatPriceString( 15 | price: number | string, 16 | precision: number = 2, 17 | ): string { 18 | const formatter = new Intl.NumberFormat("en-US", { 19 | style: "currency", 20 | currency: "USD", 21 | minimumFractionDigits: precision, 22 | maximumFractionDigits: precision, 23 | }); 24 | 25 | return formatter.format(Number(price || 0)); 26 | } 27 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-e5b5a989c149e01730594e0b1fde40a6ca21c74925760067d3a4fad23f82133d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) FROM polymarket.user_trades ut\n JOIN polymarket.users u ON u.id = ut.user_id\n WHERE ut.market_id = $1 AND u.name != $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid", 15 | "Text" 16 | ] 17 | }, 18 | "nullable": [ 19 | null 20 | ] 21 | }, 22 | "hash": "e5b5a989c149e01730594e0b1fde40a6ca21c74925760067d3a4fad23f82133d" 23 | } 24 | -------------------------------------------------------------------------------- /app/client.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:slim AS builder 2 | 3 | ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID 4 | ARG NEXT_PUBLIC_SERVICE_API_URL 5 | ARG NEXT_PUBLIC_GRPC_API_URL 6 | ARG NEXT_PUBLIC_WS_API_URL 7 | 8 | ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID 9 | ENV NEXT_PUBLIC_SERVICE_API_URL=$NEXT_PUBLIC_SERVICE_API_URL 10 | ENV NEXT_PUBLIC_GRPC_API_URL=$NEXT_PUBLIC_GRPC_API_URL 11 | ENV NEXT_PUBLIC_WS_API_URL=$NEXT_PUBLIC_WS_API_URL 12 | 13 | 14 | 15 | WORKDIR /app 16 | 17 | COPY package.json bun.lock ./ 18 | 19 | RUN bun install --frozen-lockfile 20 | 21 | COPY . . 22 | 23 | RUN bun run build 24 | 25 | FROM oven/bun:slim AS base 26 | 27 | WORKDIR /app 28 | 29 | COPY --from=builder /app /app 30 | 31 | CMD ["bun", ".next/standalone/server.js"] -------------------------------------------------------------------------------- /grpc-service/src/utils/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! validate_strings { 3 | ( $($input:expr),* ) => { 4 | $( 5 | if $input.is_empty() { 6 | let error_message = format!("{} cannot be empty", stringify!($input)); 7 | return Err(tonic::Status::invalid_argument(error_message)); 8 | } 9 | )* 10 | }; 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! validate_numbers { 15 | ( $($input:expr),* ) => { 16 | $( 17 | if $input == 0 { 18 | let error_message = format!("{} cannot be 0", stringify!($input)); 19 | return Err(tonic::Status::invalid_argument(error_message)); 20 | } 21 | )* 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /utility-helpers/src/redis/keys.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Deserialize, Serialize, Debug, Clone)] 7 | pub enum RedisKey { 8 | Market(Uuid), 9 | User(Uuid), 10 | Markets(u64, u64, u64), 11 | } 12 | 13 | impl fmt::Display for RedisKey { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | match self { 16 | RedisKey::Market(uuid) => write!(f, "market:{}", uuid), 17 | RedisKey::User(uuid) => write!(f, "user:{}", uuid), 18 | RedisKey::Markets(page_no, page_size, market_status) => { 19 | write!(f, "markets:{}:{}:{}", page_no, page_size, market_status) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-3602c5e6b25a8c1d0914642b945cf8b08ce2f83d31c6c0a947a39147e5c50d36.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT SUM((price * quantity) * 100) FROM polymarket.orders \n WHERE user_id = $1 \n AND side = 'buy'::polymarket.order_side\n AND status = 'open'::polymarket.order_status\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "sum", 9 | "type_info": "Numeric" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid" 15 | ] 16 | }, 17 | "nullable": [ 18 | null 19 | ] 20 | }, 21 | "hash": "3602c5e6b25a8c1d0914642b945cf8b08ce2f83d31c6c0a947a39147e5c50d36" 22 | } 23 | -------------------------------------------------------------------------------- /websocket-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket-service" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | tokio = { workspace = true } 8 | serde = { workspace = true } 9 | serde_json = { workspace = true } 10 | uuid = { workspace = true } 11 | futures = { workspace = true } 12 | tracing = { workspace = true } 13 | tracing-subscriber = { workspace = true } 14 | chrono = { workspace = true } 15 | prost = { workspace = true } 16 | dotenv = { workspace = true } 17 | async-nats = { workspace = true } 18 | futures-util = { workspace = true } 19 | tower = "0.5.2" 20 | tokio-tungstenite = "0.26.2" 21 | axum = { version = "0.8.4", features = ["ws"] } 22 | 23 | utility-helpers = { path = "../utility-helpers" } 24 | proto-defs = { path = "../proto-defs" } 25 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-58224c9555ed86c399af1de18c19e99219c3fb2c7cfe844a53bbf4735d4faee8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE polymarket.orders\n SET status = 'expired'::polymarket.order_status\n WHERE market_id = $1 AND status in (\n 'open'::polymarket.order_status,\n 'partial_fill'::polymarket.order_status,\n 'pending_update'::polymarket.order_status,\n 'pending_cancel'::polymarket.order_status\n )\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "58224c9555ed86c399af1de18c19e99219c3fb2c7cfe844a53bbf4735d4faee8" 14 | } 15 | -------------------------------------------------------------------------------- /app/src/hooks/useRevalidate.ts: -------------------------------------------------------------------------------- 1 | import { useQueryClient } from "@tanstack/react-query"; 2 | 3 | export default function useRevalidation() { 4 | const queryClient = useQueryClient(); 5 | 6 | function revalidateKeys(keys: string[] | string) { 7 | (async function () { 8 | try { 9 | await queryClient.refetchQueries({ 10 | queryKey: Array.isArray(keys) ? keys : [keys], 11 | }); 12 | await queryClient.invalidateQueries({ 13 | queryKey: Array.isArray(keys) ? keys : [keys], 14 | }); 15 | // console.log("Queries refetched successfully:", keys); 16 | } catch (error) { 17 | console.error("Error refetching queries:", error); 18 | throw error; 19 | } 20 | })(); 21 | } 22 | 23 | return revalidateKeys; 24 | } 25 | -------------------------------------------------------------------------------- /grpc-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grpc-service" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | tonic = "0.13.1" 8 | tonic-reflection = "0.13.1" 9 | tonic-web = "0.13.1" 10 | tower-http = { version = "0.6.6", features = ["cors"] } 11 | clickhouse = { version = "0.13.3", features = ["uuid", "chrono"] } 12 | chrono = { workspace = true } 13 | sqlx = { workspace = true } 14 | prost = { workspace = true } 15 | prost-types = { workspace = true } 16 | tokio = { workspace = true } 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | tracing = { workspace = true } 20 | tracing-subscriber = { workspace = true } 21 | 22 | utility-helpers = { path = "../utility-helpers" } 23 | db-service = { path = "../db-service" } 24 | 25 | [build-dependencies] 26 | tonic-build = "0.13.1" 27 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-bb97f2bc468dc2eef671e92573618b76482714d717490dddc67d56c53f54dd43.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE polymarket.markets\n SET status = 'settled'::polymarket.market_status,\n final_outcome = $2\n WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | { 10 | "Custom": { 11 | "name": "polymarket.outcome", 12 | "kind": { 13 | "Enum": [ 14 | "yes", 15 | "no", 16 | "unspecified" 17 | ] 18 | } 19 | } 20 | } 21 | ] 22 | }, 23 | "nullable": [] 24 | }, 25 | "hash": "bb97f2bc468dc2eef671e92573618b76482714d717490dddc67d56c53f54dd43" 26 | } 27 | -------------------------------------------------------------------------------- /pg-docker/pg.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:16-bookworm 2 | 3 | ARG POSTGRES_DB=postgres 4 | 5 | RUN echo "POSTGRES_DB: $POSTGRES_DB" 6 | 7 | ENV POSTGRES_DB=${POSTGRES_DB} 8 | 9 | RUN apt-get update && \ 10 | apt-get -y install postgresql-16-cron && \ 11 | apt-get clean && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | RUN echo "shared_preload_libraries='pg_cron'" >> /usr/share/postgresql/postgresql.conf.sample 15 | 16 | # Change permissions of the postgresql.conf.sample file 17 | RUN chown postgres:postgres /usr/share/postgresql/postgresql.conf.sample && \ 18 | chmod 664 /usr/share/postgresql/postgresql.conf.sample 19 | 20 | COPY init-scripts/ /docker-entrypoint-initdb.d/ 21 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh 22 | 23 | RUN chmod +x /usr/local/bin/entrypoint.sh 24 | 25 | USER postgres 26 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 27 | CMD ["postgres"] -------------------------------------------------------------------------------- /order-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "order-service" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | utility-helpers = { path = "../utility-helpers" } 8 | db-service = { path = "../db-service" } 9 | proto-defs = { path = "../proto-defs" } 10 | async-nats = { workspace = true } 11 | prost = { workspace = true } 12 | tokio = { workspace = true } 13 | uuid = { workspace = true } 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | chrono = { workspace = true } 17 | tracing = { workspace = true } 18 | tracing-subscriber = { workspace = true } 19 | rust_decimal = { workspace = true } 20 | rust_decimal_macros = { workspace = true } 21 | dotenv = { workspace = true } 22 | sqlx = { workspace = true } 23 | parking_lot = { workspace = true } 24 | futures-util = { workspace = true } 25 | rdkafka = { workspace = true } 26 | tokio-tungstenite = "0.26.2" 27 | -------------------------------------------------------------------------------- /utility-helpers/src/kafka_topics.rs: -------------------------------------------------------------------------------- 1 | pub enum KafkaTopics { 2 | PriceUpdates, 3 | MarketOrderBookUpdate, 4 | VolumeUpdates, 5 | } 6 | 7 | impl KafkaTopics { 8 | pub fn from_str(topic: &str) -> Option { 9 | if topic == "order-book-updates" { 10 | Some(KafkaTopics::MarketOrderBookUpdate) 11 | } else if topic == "price-updates" { 12 | Some(KafkaTopics::PriceUpdates) 13 | } else if topic == "volume-updates" { 14 | Some(KafkaTopics::VolumeUpdates) 15 | } else { 16 | None 17 | } 18 | } 19 | 20 | pub fn to_string(&self) -> &str { 21 | match self { 22 | KafkaTopics::PriceUpdates => "price-updates", 23 | KafkaTopics::MarketOrderBookUpdate => "order-book-updates", 24 | KafkaTopics::VolumeUpdates => "volume-updates", 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-cc50bacd9f45932bd1eb9b1bd418013857386363d20530b926a6bf776d268cda.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO polymarket.user_holdings (user_id, market_id, shares, outcome)\n VALUES ($1, $2, 0, $3)\n ON CONFLICT (user_id, market_id, outcome) DO NOTHING\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Uuid", 10 | { 11 | "Custom": { 12 | "name": "polymarket.outcome", 13 | "kind": { 14 | "Enum": [ 15 | "yes", 16 | "no", 17 | "unspecified" 18 | ] 19 | } 20 | } 21 | } 22 | ] 23 | }, 24 | "nullable": [] 25 | }, 26 | "hash": "cc50bacd9f45932bd1eb9b1bd418013857386363d20530b926a6bf776d268cda" 27 | } 28 | -------------------------------------------------------------------------------- /service-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "service-api" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | axum = "0.8.4" 8 | axum-extra = { version = "0.10.0", features = ["protobuf"] } 9 | rand = "0.9.1" 10 | db-service = { path = "../db-service" } 11 | auth-service = { path = "../auth-service" } 12 | utility-helpers = { path = "../utility-helpers" } 13 | proto-defs = { path = "../proto-defs" } 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | tokio = { workspace = true } 17 | tracing = { workspace = true } 18 | tracing-subscriber = { workspace = true } 19 | tower-http = { workspace = true } 20 | sqlx = { workspace = true } 21 | dotenv = { workspace = true } 22 | rust_decimal = { workspace = true } 23 | rust_decimal_macros = { workspace = true } 24 | async-nats = { workspace = true } 25 | bloom = { workspace = true } 26 | uuid = { workspace = true } 27 | parking_lot = { workspace = true } 28 | -------------------------------------------------------------------------------- /app/src/components/ui/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChakraProvider, defaultSystem } from "@chakra-ui/react"; 4 | import { GoogleOAuthProvider } from "@react-oauth/google"; 5 | import { useState } from "react"; 6 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 7 | 8 | import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode"; 9 | import { Toaster } from "./toaster"; 10 | 11 | export function Provider(props: ColorModeProviderProps) { 12 | const [client] = useState(() => new QueryClient()); 13 | return ( 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/components/EmptyStateCustom.tsx: -------------------------------------------------------------------------------- 1 | import { Box, EmptyState, VStack } from "@chakra-ui/react"; 2 | import { Scroll } from "lucide-react"; 3 | import React from "react"; 4 | 5 | type Props = { 6 | title?: string; 7 | description?: string; 8 | actionButton?: React.ReactNode; 9 | }; 10 | 11 | const EmptyStateCustom = ({ description, title, actionButton }: Props) => { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | {title} 21 | {description} 22 | {actionButton} 23 | 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default EmptyStateCustom; 31 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-cf2755ca9dfe18a0b14e421991253dc0ceffdbb5b26fb05059043e54b1b7a6a7.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) as total_count\n FROM \"polymarket\".\"markets\"\n WHERE status = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "total_count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | { 15 | "Custom": { 16 | "name": "polymarket.market_status", 17 | "kind": { 18 | "Enum": [ 19 | "open", 20 | "closed", 21 | "settled" 22 | ] 23 | } 24 | } 25 | } 26 | ] 27 | }, 28 | "nullable": [ 29 | null 30 | ] 31 | }, 32 | "hash": "cf2755ca9dfe18a0b14e421991253dc0ceffdbb5b26fb05059043e54b1b7a6a7" 33 | } 34 | -------------------------------------------------------------------------------- /utility-helpers/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rust_decimal::Decimal; 2 | 3 | pub mod kafka_topics; 4 | pub mod macros; 5 | pub mod message_pack_helper; 6 | pub mod nats_helper; 7 | pub mod redis; 8 | pub mod symmetric; 9 | pub mod types; 10 | pub mod ws; 11 | 12 | pub const SHOW_LOGS: bool = true; 13 | 14 | pub fn to_f64(value: Decimal) -> Option { 15 | let value_str = value.to_string(); 16 | let parsed_value = value_str.parse::(); 17 | match parsed_value { 18 | Ok(v) => Some(v), 19 | Err(_) => None, 20 | } 21 | } 22 | 23 | pub fn to_f64_verbose(num: Decimal) -> f64 { 24 | let num_str = num.to_string(); 25 | let num_f64: f64 = num_str.parse().unwrap(); 26 | num_f64 27 | } 28 | 29 | pub fn to_u32(value: Decimal) -> Option { 30 | let value_str = value.to_string(); 31 | let parsed_value = value_str.parse::(); 32 | match parsed_value { 33 | Ok(v) => Some(v), 34 | Err(_) => None, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /utility-helpers/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! log_info { 3 | ($($arg:tt)*) => { 4 | if $crate::SHOW_LOGS { 5 | tracing::info!($($arg)*); 6 | } 7 | }; 8 | } 9 | 10 | #[macro_export] 11 | macro_rules! log_error { 12 | ($($arg:tt)*) => { 13 | if $crate::SHOW_LOGS { 14 | tracing::error!($($arg)*); 15 | } 16 | }; 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! log_debug { 21 | ($($arg:tt)*) => { 22 | if $crate::SHOW_LOGS { 23 | tracing::debug!($($arg)*); 24 | } 25 | }; 26 | } 27 | 28 | #[macro_export] 29 | macro_rules! log_warn { 30 | ($($arg:tt)*) => { 31 | if $crate::SHOW_LOGS { 32 | tracing::warn!($($arg)*); 33 | } 34 | }; 35 | } 36 | 37 | #[macro_export] 38 | macro_rules! log_trace { 39 | ($($arg:tt)*) => { 40 | if $crate::SHOW_LOGS { 41 | tracing::trace!($($arg)*); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /websocket-service/src/state.rs: -------------------------------------------------------------------------------- 1 | use async_nats::{connect, jetstream}; 2 | use tokio::sync::RwLock; 3 | use utility_helpers::{log_info, types::EnvVarConfig}; 4 | 5 | use crate::core::client_manager::SubscriptionAndClientManager; 6 | 7 | #[derive(Debug)] 8 | pub struct WebSocketAppState { 9 | pub client_manager: RwLock, 10 | pub jetstream: jetstream::Context, 11 | } 12 | 13 | impl WebSocketAppState { 14 | pub async fn new() -> Result> { 15 | dotenv::dotenv().ok(); 16 | let env_var_config = EnvVarConfig::new()?; 17 | 18 | let nc = connect(&env_var_config.nc_url) 19 | .await 20 | .expect("Failed to connect to NATS server"); 21 | log_info!("Connected to NATS"); 22 | let jetstream = jetstream::new(nc); 23 | 24 | Ok(WebSocketAppState { 25 | jetstream, 26 | client_manager: RwLock::new(SubscriptionAndClientManager::new()), 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /queries/pg_queries/UserTable.sql: -------------------------------------------------------------------------------- 1 | -- truncate table polymarket.users cascade; 2 | 3 | -- delete from polymarket.users where id not in ('3a308ea2-003c-41e0-adeb-3bd1f7e0e884'::uuid, 'f45d9e53-820c-425b-b9bc-56af30ea1351'::uuid); 4 | SELECT id, balance, email, name from polymarket.users; 5 | -- SELECT * FROM polymarket.users; 6 | 7 | -- delete from polymarket.users where id='cd31934d-0019-41d1-9ccc-bafc6b9330de'; 8 | 9 | -- update polymarket.users set balance=100 where balance=10.0 returning *; 10 | 11 | -- update polymarket.users 12 | -- set balance = 13 | -- case 14 | -- when id = '24fa20ac-822f-49e9-9cb6-e25e940ad608'::uuid then 100::numeric 15 | -- when id = '27db8053-2640-45bf-894b-dcd420eb4886'::uuid then 100::numeric 16 | -- end 17 | -- where id in ('24fa20ac-822f-49e9-9cb6-e25e940ad608'::uuid, '27db8053-2640-45bf-894b-dcd420eb4886'::uuid); 18 | 19 | 20 | -- select id,balance from polymarket.users where id in ('24fa20ac-822f-49e9-9cb6-e25e940ad608'::uuid, '27db8053-2640-45bf-894b-dcd420eb4886'::uuid); 21 | 22 | -------------------------------------------------------------------------------- /service-api/src/routes/user/orders/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | Router, 3 | routing::{delete, get, patch, post}, 4 | }; 5 | 6 | use crate::state::AppState; 7 | 8 | pub mod cancel_order; 9 | pub mod create_limit_order; 10 | pub mod create_market_order; 11 | pub mod get_all_users_orders; 12 | pub mod get_orders_by_markets; 13 | pub mod update_order; 14 | 15 | pub fn router() -> Router { 16 | Router::new() 17 | .route("/get", get(get_all_users_orders::get_all_users_orders)) 18 | .route( 19 | "/create/limit", 20 | post(create_limit_order::create_limit_order), 21 | ) 22 | .route( 23 | "/create/market", 24 | post(create_market_order::create_limit_order), 25 | ) 26 | .route( 27 | "/get/{id}", 28 | get(get_orders_by_markets::get_user_orders_by_market), 29 | ) 30 | .route("/cancel/{id}", delete(cancel_order::cancel_order)) 31 | .route("/update", patch(update_order::update_order)) 32 | } 33 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-7929dc94bbc48788fd63faea4e7bd0c30af5d27299cd7e233122d965a135b8e6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT \n SUM(CASE WHEN outcome = 'yes'::polymarket.outcome THEN shares ELSE 0 END) as \"yes_shares\",\n SUM(CASE WHEN outcome = 'no'::polymarket.outcome THEN shares ELSE 0 END) as \"no_shares\"\n FROM polymarket.user_holdings\n WHERE user_id = $1 AND market_id = $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "yes_shares", 9 | "type_info": "Numeric" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "no_shares", 14 | "type_info": "Numeric" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Uuid", 20 | "Uuid" 21 | ] 22 | }, 23 | "nullable": [ 24 | null, 25 | null 26 | ] 27 | }, 28 | "hash": "7929dc94bbc48788fd63faea4e7bd0c30af5d27299cd7e233122d965a135b8e6" 29 | } 30 | -------------------------------------------------------------------------------- /proto-defs/proto/ws_server/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ws_common_types; 4 | 5 | // sample payload (note this type of payload is only send by internal services, not by clients): 6 | /* 7 | { 8 | "id":"AdminIsLegend", 9 | "params":{ 10 | "ops": "Subscribe", 11 | "data": { 12 | "channel": "price_update:67df943a-09a5-4ddb-adeb-11042c37c32e" 13 | } 14 | } 15 | } 16 | */ 17 | 18 | 19 | enum OperationType { 20 | SUBSCRIBE = 0; 21 | UNSUBSCRIBE = 1; 22 | POST = 2; 23 | HANDSHAKE = 3; 24 | } 25 | 26 | enum Channel { 27 | PRICEUPDATE = 0; 28 | PRICEPOSTER = 1; 29 | ORDERSERVICE = 2; 30 | } 31 | 32 | message WsMessage { 33 | optional string id = 1; 34 | Payload payload = 2; 35 | } 36 | 37 | // wrapped data payload 38 | message Payload { 39 | OperationType ops = 1; 40 | WsData data = 2; 41 | } 42 | 43 | // data to send to the channel 44 | message WsData { 45 | Channel channel = 1; 46 | string params = 2; 47 | } 48 | -------------------------------------------------------------------------------- /app/public/proto/proto_defs/ws_server/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ws_common_types; 4 | 5 | // sample payload (note this type of payload is only send by internal services, not by clients): 6 | /* 7 | { 8 | "id":"AdminIsLegend", 9 | "params":{ 10 | "ops": "Subscribe", 11 | "data": { 12 | "channel": "price_update:67df943a-09a5-4ddb-adeb-11042c37c32e" 13 | } 14 | } 15 | } 16 | */ 17 | 18 | 19 | enum OperationType { 20 | SUBSCRIBE = 0; 21 | UNSUBSCRIBE = 1; 22 | POST = 2; 23 | HANDSHAKE = 3; 24 | } 25 | 26 | enum Channel { 27 | PRICEUPDATE = 0; 28 | PRICEPOSTER = 1; 29 | ORDERSERVICE = 2; 30 | } 31 | 32 | message WsMessage { 33 | optional string id = 1; 34 | Payload payload = 2; 35 | } 36 | 37 | // wrapped data payload 38 | message Payload { 39 | OperationType ops = 1; 40 | WsData data = 2; 41 | } 42 | 43 | // data to send to the channel 44 | message WsData { 45 | Channel channel = 1; 46 | string params = 2; 47 | } 48 | -------------------------------------------------------------------------------- /service-api/src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::Router; 2 | use tower_http::cors::{Any, CorsLayer}; 3 | 4 | use state::AppState; 5 | use utility_helpers::log_info; 6 | 7 | mod routes; 8 | mod state; 9 | mod utils; 10 | 11 | pub mod bloom_f; 12 | 13 | const PORT: u16 = 8080; 14 | 15 | #[tokio::main(flavor = "multi_thread")] 16 | async fn main() -> Result<(), Box> { 17 | tracing_subscriber::fmt::init(); 18 | let addr = format!("[::]:{}", PORT); 19 | 20 | let state = AppState::new().await?; 21 | state.run_migrations().await?; 22 | 23 | let cors = CorsLayer::new() 24 | .allow_origin(Any) 25 | .allow_headers(Any) 26 | .allow_methods(Any); 27 | 28 | let app = Router::new() 29 | .merge(routes::router(state.clone())) 30 | .layer(cors) 31 | .with_state(state); 32 | 33 | let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 34 | 35 | log_info!("service-api is listening on http://localhost:{}", PORT); 36 | 37 | axum::serve(listener, app).await.unwrap(); 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-5011f4a665cb8905c1daf331fbbf874cc23a88b2f52c82274fb55235df58bdc8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE polymarket.users\n SET balance = CASE\n WHEN id = $1 THEN balance + ($2::numeric * (CASE WHEN $3 = 'sell'::polymarket.order_side THEN 1 ELSE -1 END))\n WHEN id = $4 THEN balance + ($2::numeric * (CASE WHEN $3 = 'buy'::polymarket.order_side THEN 1 ELSE -1 END))\n END\n WHERE id IN ($1, $4);\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Numeric", 10 | { 11 | "Custom": { 12 | "name": "polymarket.order_side", 13 | "kind": { 14 | "Enum": [ 15 | "buy", 16 | "sell" 17 | ] 18 | } 19 | } 20 | }, 21 | "Uuid" 22 | ] 23 | }, 24 | "nullable": [] 25 | }, 26 | "hash": "5011f4a665cb8905c1daf331fbbf874cc23a88b2f52c82274fb55235df58bdc8" 27 | } 28 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-10c169c01176ed93e662dbb89d69ebdd1f791a40ee4f3939f9f49c822926714e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT SUM(quantity) FROM polymarket.orders \n WHERE user_id = $1\n AND outcome = $2\n AND side = 'sell'::polymarket.order_side\n AND status = 'open'::polymarket.order_status\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "sum", 9 | "type_info": "Numeric" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid", 15 | { 16 | "Custom": { 17 | "name": "polymarket.outcome", 18 | "kind": { 19 | "Enum": [ 20 | "yes", 21 | "no", 22 | "unspecified" 23 | ] 24 | } 25 | } 26 | } 27 | ] 28 | }, 29 | "nullable": [ 30 | null 31 | ] 32 | }, 33 | "hash": "10c169c01176ed93e662dbb89d69ebdd1f791a40ee4f3939f9f49c822926714e" 34 | } 35 | -------------------------------------------------------------------------------- /app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | 4 | import { Provider } from "@/components/ui/provider"; 5 | import Navbar from "@/components/Navbar"; 6 | 7 | import "./globals.css"; 8 | 9 | const geistSans = Geist({ 10 | variable: "--font-geist-sans", 11 | subsets: ["latin"], 12 | }); 13 | 14 | const geistMono = Geist_Mono({ 15 | variable: "--font-geist-mono", 16 | subsets: ["latin"], 17 | }); 18 | 19 | export const metadata: Metadata = { 20 | title: "Polymarket clone", 21 | description: "Next level system designed for prediction markets", 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-79a4a9089b2d5a970fc17992045ccd567b813c5cef731c981a279bbeb4f8682d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE polymarket.users u\n SET balance = balance + (payout.total * 100) -- Each share is worth 100 after settlement\n FROM (\n SELECT user_id, SUM(shares) AS total\n FROM polymarket.user_holdings\n WHERE market_id = $1 AND outcome = $2\n GROUP BY user_id\n ) AS payout\n WHERE u.id = payout.user_id\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | { 10 | "Custom": { 11 | "name": "polymarket.outcome", 12 | "kind": { 13 | "Enum": [ 14 | "yes", 15 | "no", 16 | "unspecified" 17 | ] 18 | } 19 | } 20 | } 21 | ] 22 | }, 23 | "nullable": [] 24 | }, 25 | "hash": "79a4a9089b2d5a970fc17992045ccd567b813c5cef731c981a279bbeb4f8682d" 26 | } 27 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-745a2afe244a16070631760b88ea4a159cac489f3c58c8f2e3766805aeceaaea.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT COUNT(*) FROM polymarket.orders\n WHERE user_id = $1 AND status = $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "count", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Uuid", 15 | { 16 | "Custom": { 17 | "name": "polymarket.order_status", 18 | "kind": { 19 | "Enum": [ 20 | "open", 21 | "filled", 22 | "cancelled", 23 | "unspecified", 24 | "expired", 25 | "pending_cancel", 26 | "partial_fill", 27 | "pending_update" 28 | ] 29 | } 30 | } 31 | } 32 | ] 33 | }, 34 | "nullable": [ 35 | null 36 | ] 37 | }, 38 | "hash": "745a2afe244a16070631760b88ea4a159cac489f3c58c8f2e3766805aeceaaea" 39 | } 40 | -------------------------------------------------------------------------------- /scripts/result_sell.txt: -------------------------------------------------------------------------------- 1 | 2 | Summary: 3 | Total: 0.0453 secs 4 | Slowest: 0.0227 secs 5 | Fastest: 0.0002 secs 6 | Average: 0.0043 secs 7 | Requests/sec: 11042.1490 8 | 9 | Total data: 37500 bytes 10 | Size/request: 75 bytes 11 | 12 | Response time histogram: 13 | 0.000 [1] | 14 | 0.002 [260] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 15 | 0.005 [79] |■■■■■■■■■■■■ 16 | 0.007 [57] |■■■■■■■■■ 17 | 0.009 [39] |■■■■■■ 18 | 0.011 [27] |■■■■ 19 | 0.014 [12] |■■ 20 | 0.016 [13] |■■ 21 | 0.018 [6] |■ 22 | 0.020 [2] | 23 | 0.023 [4] |■ 24 | 25 | 26 | Latency distribution: 27 | 10% in 0.0013 secs 28 | 25% in 0.0016 secs 29 | 50% in 0.0023 secs 30 | 75% in 0.0059 secs 31 | 90% in 0.0100 secs 32 | 95% in 0.0138 secs 33 | 99% in 0.0185 secs 34 | 35 | Details (average, fastest, slowest): 36 | DNS+dialup: 0.0004 secs, 0.0002 secs, 0.0227 secs 37 | DNS-lookup: 0.0002 secs, 0.0000 secs, 0.0024 secs 38 | req write: 0.0000 secs, 0.0000 secs, 0.0007 secs 39 | resp wait: 0.0038 secs, 0.0001 secs, 0.0180 secs 40 | resp read: 0.0001 secs, 0.0000 secs, 0.0009 secs 41 | 42 | Status code distribution: 43 | [400] 500 responses 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /scripts/result_buy.txt: -------------------------------------------------------------------------------- 1 | 2 | Summary: 3 | Total: 0.0452 secs 4 | Slowest: 0.0212 secs 5 | Fastest: 0.0002 secs 6 | Average: 0.0043 secs 7 | Requests/sec: 11062.0795 8 | 9 | Total data: 37500 bytes 10 | Size/request: 75 bytes 11 | 12 | Response time histogram: 13 | 0.000 [1] | 14 | 0.002 [247] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 15 | 0.004 [73] |■■■■■■■■■■■■ 16 | 0.006 [60] |■■■■■■■■■■ 17 | 0.009 [42] |■■■■■■■ 18 | 0.011 [34] |■■■■■■ 19 | 0.013 [20] |■■■ 20 | 0.015 [10] |■■ 21 | 0.017 [5] |■ 22 | 0.019 [5] |■ 23 | 0.021 [3] | 24 | 25 | 26 | Latency distribution: 27 | 10% in 0.0009 secs 28 | 25% in 0.0015 secs 29 | 50% in 0.0023 secs 30 | 75% in 0.0063 secs 31 | 90% in 0.0101 secs 32 | 95% in 0.0124 secs 33 | 99% in 0.0177 secs 34 | 35 | Details (average, fastest, slowest): 36 | DNS+dialup: 0.0004 secs, 0.0002 secs, 0.0212 secs 37 | DNS-lookup: 0.0002 secs, 0.0000 secs, 0.0022 secs 38 | req write: 0.0000 secs, 0.0000 secs, 0.0005 secs 39 | resp wait: 0.0038 secs, 0.0002 secs, 0.0202 secs 40 | resp read: 0.0001 secs, 0.0000 secs, 0.0013 secs 41 | 42 | Status code distribution: 43 | [400] 500 responses 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /service-api/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | Json, Router, 3 | http::StatusCode, 4 | middleware, 5 | response::IntoResponse, 6 | routing::{get, post}, 7 | }; 8 | use serde_json::json; 9 | use std::sync::Arc; 10 | 11 | use crate::state::AppState; 12 | use crate::utils::middleware as custom_middleware; 13 | 14 | pub mod admin; 15 | pub mod login; 16 | pub mod user; 17 | 18 | async fn default_home_route() -> (StatusCode, impl IntoResponse) { 19 | let welcome_message = json!({ 20 | "message": "Welcome to the Polymarket clone service API!" 21 | }); 22 | (StatusCode::OK, Json(welcome_message)) 23 | } 24 | 25 | pub fn router(app_state: AppState) -> Router { 26 | let app_state = Arc::new(app_state.clone()); 27 | let user_routes = user::router().layer(middleware::from_fn_with_state( 28 | app_state, 29 | custom_middleware::validate_jwt, 30 | )); 31 | 32 | let admin_routes = admin::router(); // for now without middleware 33 | 34 | Router::new() 35 | .route("/", get(default_home_route)) 36 | .route("/login", post(login::oauth_login)) 37 | .nest("/user", user_routes) 38 | .nest("/admin", admin_routes) 39 | } 40 | -------------------------------------------------------------------------------- /service-api/src/routes/user/metadata.rs: -------------------------------------------------------------------------------- 1 | use auth_service::types::SessionTokenClaims; 2 | use axum::{ 3 | Extension, Json, 4 | extract::State, 5 | http::StatusCode, 6 | response::{IntoResponse, Response}, 7 | }; 8 | use db_service::schema::users::User; 9 | use serde_json::json; 10 | 11 | use crate::state::AppState; 12 | 13 | pub async fn get_metadata( 14 | State(app_state): State, 15 | Extension(claims): Extension, 16 | ) -> Result { 17 | let user_id = claims.user_id; 18 | 19 | let user_profile_insight = User::get_user_metadata(&app_state.pg_pool, user_id) 20 | .await 21 | .map_err(|e| { 22 | ( 23 | StatusCode::INTERNAL_SERVER_ERROR, 24 | Json(json!({ 25 | "error": "Failed to fetch user metadata", 26 | "details": e.to_string() 27 | })) 28 | .into_response(), 29 | ) 30 | })?; 31 | 32 | Ok(( 33 | StatusCode::OK, 34 | Json(json!({ 35 | "user_id": user_id, 36 | "profile_insight": user_profile_insight 37 | })), 38 | )) 39 | } 40 | -------------------------------------------------------------------------------- /db-service/src/pagination.rs: -------------------------------------------------------------------------------- 1 | /// Paginated response containing both items and page information 2 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 3 | pub struct PaginatedResponse { 4 | /// Items for the current page 5 | pub items: Vec, 6 | /// Page information 7 | pub page_info: PageInfo, 8 | } 9 | 10 | /// Page information returned with paginated results 11 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 12 | pub struct PageInfo { 13 | /// Current page number (1-based) 14 | pub page: u64, 15 | /// Number of items per page 16 | pub page_size: u64, 17 | /// Total number of items across all pages 18 | pub total_items: u64, 19 | /// Total number of pages 20 | pub total_pages: u64, 21 | } 22 | 23 | impl PaginatedResponse { 24 | /// Creates a new `PaginatedResponse` with the given items and page information 25 | pub fn new(items: Vec, page: u64, page_size: u64, total_items: u64) -> Self { 26 | let total_pages = (total_items + page_size - 1) / page_size; 27 | let page_info = PageInfo { 28 | page, 29 | page_size, 30 | total_items, 31 | total_pages, 32 | }; 33 | PaginatedResponse { items, page_info } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/components/NavbarNotificationButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconButton, 3 | Input, 4 | Popover, 5 | Portal, 6 | Separator, 7 | Text, 8 | } from "@chakra-ui/react"; 9 | import { Bell } from "lucide-react"; 10 | import React from "react"; 11 | import EmptyStateCustom from "./EmptyStateCustom"; 12 | 13 | const NavbarNotificationButton = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Unread notifications 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default NavbarNotificationButton; 44 | -------------------------------------------------------------------------------- /proto-defs/src/proto_types/order_book.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | /// which price, how many shares (total quantity) and how many users on same price (histogram) 3 | #[derive(serde::Serialize, serde::Deserialize)] 4 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 5 | pub struct OrderLevel { 6 | #[prost(double, tag = "1")] 7 | pub price: f64, 8 | #[prost(double, tag = "2")] 9 | pub shares: f64, 10 | #[prost(uint32, tag = "3")] 11 | pub users: u32, 12 | } 13 | #[derive(serde::Serialize, serde::Deserialize)] 14 | #[derive(Clone, PartialEq, ::prost::Message)] 15 | pub struct OrderBook { 16 | #[prost(message, repeated, tag = "2")] 17 | pub bids: ::prost::alloc::vec::Vec, 18 | #[prost(message, repeated, tag = "3")] 19 | pub asks: ::prost::alloc::vec::Vec, 20 | } 21 | #[derive(serde::Serialize, serde::Deserialize)] 22 | #[derive(Clone, PartialEq, ::prost::Message)] 23 | pub struct MarketBook { 24 | #[prost(string, tag = "1")] 25 | pub market_id: ::prost::alloc::string::String, 26 | #[prost(message, optional, tag = "2")] 27 | pub yes_book: ::core::option::Option, 28 | #[prost(message, optional, tag = "3")] 29 | pub no_book: ::core::option::Option, 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ----- Chef Stage ----- 2 | FROM rust:1.88-bullseye AS chef 3 | 4 | WORKDIR /app 5 | ARG DATABASE_URL 6 | 7 | RUN cargo install cargo-chef 8 | COPY . . 9 | 10 | RUN cargo chef prepare --recipe-path recipe.json 11 | 12 | # ----- Builder Stage ---- 13 | 14 | FROM rust:1.88-bullseye AS builder 15 | 16 | # install necessary tools for lib rdkafka and protobuf compilation 17 | RUN apt-get update && apt-get install -y \ 18 | build-essential \ 19 | cmake \ 20 | pkg-config \ 21 | zlib1g-dev \ 22 | protobuf-compiler \ 23 | && rm -rf /var/lib/apt/lists/* 24 | 25 | WORKDIR /app 26 | 27 | RUN cargo install cargo-chef 28 | 29 | COPY --from=chef /app/recipe.json recipe.json 30 | 31 | RUN cargo chef cook --release --recipe-path recipe.json 32 | ENV DATABASE_URL=$DATABASE_URL 33 | ENV SQLX_OFFLINE=true 34 | 35 | COPY . . 36 | 37 | RUN cargo build --release --workspace 38 | 39 | # ---- Runtime stage: Final, small image -- 40 | 41 | FROM debian:bullseye-slim AS runtime 42 | 43 | WORKDIR /usr/local/bin 44 | 45 | COPY --from=builder /app/target/release/grpc-service . 46 | COPY --from=builder /app/target/release/order-service . 47 | COPY --from=builder /app/target/release/service-api . 48 | COPY --from=builder /app/target/release/websocket-service . 49 | 50 | CMD ['./grpc-service'] -------------------------------------------------------------------------------- /auth-service/src/token_services.rs: -------------------------------------------------------------------------------- 1 | use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct Claims { 6 | pub sub: String, 7 | pub exp: usize, 8 | } 9 | 10 | impl Claims { 11 | pub fn new(sub: String, exp: usize) -> Self { 12 | Claims { sub, exp } 13 | } 14 | 15 | pub fn new_token(&self) -> Result> { 16 | dotenv::dotenv().ok(); 17 | 18 | let header = Header::new(Algorithm::HS256); 19 | let secret = std::env::var("JWT_SECRET")?; 20 | let encoding_key = EncodingKey::from_secret(secret.as_bytes()); 21 | let token = encode(&header, self, &encoding_key)?; 22 | 23 | Ok(token) 24 | } 25 | 26 | pub fn verify_token(token: &str) -> Result> { 27 | dotenv::dotenv().ok(); 28 | 29 | let secret = std::env::var("JWT_SECRET")?; 30 | let decoding_key = DecodingKey::from_secret(secret.as_bytes()); 31 | let validation = Validation::new(Algorithm::HS256); 32 | let token_data = decode::(token, &decoding_key, &validation)?; 33 | 34 | Ok(token_data.claims) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /queries/pg_queries/marketTable.sql: -------------------------------------------------------------------------------- 1 | -- truncate table polymarket.markets CASCADE; 2 | 3 | -- select * from polymarket.markets where status = 'open'::polymarket.market_status; 4 | 5 | -- SELECT 6 | -- o.id, o.user_id, o.market_id, 7 | -- o.outcome as "outcome: Outcome", 8 | -- o.price, o.quantity, o.filled_quantity, 9 | -- o.status as "status: OrderStatus", 10 | -- o.side as "side: OrderSide", 11 | -- o.created_at, o.updated_at, m.liquidity_b 12 | -- FROM polymarket.orders o 13 | -- LEFT JOIN polymarket.markets m ON o.market_id = m.id 14 | -- WHERE o.status = 'open'::polymarket.order_status 15 | 16 | 17 | -- UPDATE polymarket.markets 18 | -- SET status = 'closed'::polymarket.market_status, updated_at = CURRENT_TIMESTAMP 19 | -- WHERE id = 'c3cc74a7-6695-41e1-8bdf-d5affa5b4aac'::uuid AND market_expiry <= CURRENT_TIMESTAMP AND status = 'open'::polymarket.market_status; 20 | 21 | select * from polymarket.markets ORDER BY created_at DESC; 22 | 23 | -- delete from polymarket.markets where id = '06761131-a639-4dae-9dff-d652ff3b3832'::uuid; 24 | 25 | -- SELECT 26 | -- market_id, 27 | -- SUM(quantity * price) AS notional_volume 28 | -- FROM 29 | -- polymarket.user_trades 30 | -- GROUP BY 31 | -- market_id; -------------------------------------------------------------------------------- /app/src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Toaster as ChakraToaster, 5 | Portal, 6 | Spinner, 7 | Stack, 8 | Toast, 9 | createToaster, 10 | } from "@chakra-ui/react"; 11 | 12 | export const toaster = createToaster({ 13 | placement: "top", 14 | pauseOnPageIdle: true, 15 | }); 16 | 17 | export const Toaster = () => { 18 | return ( 19 | 20 | 21 | {(toast) => ( 22 | 23 | {toast.type === "loading" ? ( 24 | 25 | ) : ( 26 | 27 | )} 28 | 29 | {toast.title && {toast.title}} 30 | {toast.description && ( 31 | {toast.description} 32 | )} 33 | 34 | {toast.action && ( 35 | {toast.action.label} 36 | )} 37 | {toast.meta?.closable && } 38 | 39 | )} 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /websocket-service/src/nats_handler/handle_market_book_update.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::ws::Message as WsMessage; 2 | use prost::Message; 3 | use utility_helpers::{log_info, nats_helper::types::OrderBookUpdateData, ws::types::ChannelType}; 4 | 5 | use crate::{SafeAppState, core::send_message}; 6 | 7 | pub async fn handle_market_book_update( 8 | state: SafeAppState, 9 | data: OrderBookUpdateData, 10 | ) -> Result<(), Box> { 11 | let client_manager_guard = state.client_manager.read().await; 12 | let market_id = data.market_id; 13 | 14 | let subscribers_opt = 15 | client_manager_guard.get_clients(&ChannelType::OrderBookUpdate(market_id)); 16 | let message = data.get_prost_market_book(market_id); 17 | let message = message.encode_to_vec(); 18 | 19 | if let Some(subscribers) = subscribers_opt { 20 | for (_, tx) in subscribers.iter() { 21 | if let Err(e) = send_message(tx, WsMessage::Binary(message.clone().into())).await { 22 | log_info!("Failed to send market book update to client: {}", e); 23 | } else { 24 | log_info!( 25 | "Market book update sent to client for market ID: {}", 26 | market_id 27 | ); 28 | } 29 | } 30 | } 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/components/modals/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CloseButton, Dialog, Portal } from "@chakra-ui/react"; 4 | 5 | import useModal, { ModalType } from "@/hooks/useModal"; 6 | 7 | type Props = { 8 | children: React.ReactNode; 9 | type: ModalType; 10 | closeOnInteractOutside?: boolean; 11 | scrollBehavior?: "inside" | "outside"; 12 | }; 13 | 14 | const Modal = ({ 15 | children, 16 | type, 17 | closeOnInteractOutside = true, 18 | scrollBehavior = "outside", 19 | }: Props) => { 20 | const { isOpen, type: modalType, close } = useModal(); 21 | const isOpenModal = isOpen && modalType === type; 22 | return ( 23 | 29 | 30 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default Modal; 48 | -------------------------------------------------------------------------------- /order-service/src/handlers/nats_handler/add_order_handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use db_service::schema::orders::Order; 4 | use rust_decimal::Decimal; 5 | use utility_helpers::log_error; 6 | 7 | use crate::{ 8 | state::AppState, 9 | utils::{OrderServiceError, update_services::update_service_state}, 10 | }; 11 | 12 | pub async fn add_order_handler( 13 | state: Arc, 14 | orders: &Vec, 15 | liquidity_b: Decimal, 16 | ) -> Result<(), OrderServiceError> { 17 | // synchronous write lock to the order book 18 | { 19 | let mut order_book_guard = state.order_book.write(); 20 | 21 | for order in orders.iter() { 22 | order_book_guard.add_order(order, liquidity_b); 23 | } 24 | } 25 | 26 | // asynchronous service state update 27 | for order in orders.iter() { 28 | update_service_state(state.clone(), order) 29 | .await 30 | .map_err(|e| { 31 | log_error!( 32 | "Failed to update service state for order {}: {}", 33 | order.id, 34 | e 35 | ); 36 | format!( 37 | "Failed to update service state for order {}: {}", 38 | order.id, e 39 | ) 40 | })?; 41 | } 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /utility-helpers/src/ws/publisher_types.rs: -------------------------------------------------------------------------------- 1 | use rust_decimal::Decimal; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::json; 4 | use uuid::Uuid; 5 | 6 | use crate::ws::types::ChannelType; 7 | 8 | #[derive(Debug)] 9 | pub struct GenericWrapper 10 | where 11 | T: Serialize + for<'de> Deserialize<'de>, 12 | { 13 | pub channel: ChannelType, 14 | pub data: T, 15 | } 16 | 17 | impl GenericWrapper 18 | where 19 | T: Serialize + for<'de> Deserialize<'de>, 20 | { 21 | pub fn wrap_with_channel(channel: ChannelType, data: T) -> Self { 22 | GenericWrapper { channel, data } 23 | } 24 | 25 | pub fn to_string(&self) -> Result { 26 | let channel_str = self.channel.to_str(); 27 | 28 | // let channel_str = self.channel.to_str(); 29 | let final_data = json!({ 30 | "payload":{ 31 | "channel": channel_str, 32 | "params": self.data, 33 | } 34 | }) 35 | .to_string(); 36 | 37 | Ok(final_data) 38 | } 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Debug)] 42 | pub struct PricePosterDataStruct { 43 | pub market_id: Uuid, 44 | pub yes_price: Decimal, 45 | pub no_price: Decimal, 46 | } 47 | 48 | // REUSABLE TYPES ACROSS SERVICES --- till here 49 | // and send websocket message from order service 50 | -------------------------------------------------------------------------------- /scripts/stress-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tokens 4 | TOKEN_ONE="test1" 5 | TOKEN_TWO="test2" 6 | 7 | # Config 8 | URL="http://localhost:8080/user/orders/create" 9 | REQ_COUNT=500 10 | CONCURRENCY=50 11 | 12 | # Headers 13 | HEADER_ONE="Authorization: Bearer $TOKEN_ONE" 14 | HEADER_TWO="Authorization: Bearer $TOKEN_TWO" 15 | CONTENT_TYPE="Content-Type: application/json" 16 | 17 | # Payloads 18 | cat > payload1.json < payload2.json < result_buy.txt & 46 | 47 | hey -n $REQ_COUNT -c $CONCURRENCY -m POST \ 48 | -H "$HEADER_TWO" \ 49 | -H "$CONTENT_TYPE" \ 50 | -d @payload2.json \ 51 | "$URL" > result_sell.txt & 52 | 53 | # Wait for both to finish 54 | wait 55 | 56 | echo "Stress test complete. Results saved to result_buy.txt and result_sell.txt" -------------------------------------------------------------------------------- /grpc-service/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use sqlx::PgPool; 4 | use utility_helpers::{log_info, redis::RedisHelper, types::EnvVarConfig}; 5 | 6 | pub type SafeState = Arc; 7 | pub struct AppState { 8 | pub db_pool: PgPool, 9 | pub redis_helper: RedisHelper, 10 | pub clickhouse_client: clickhouse::Client, 11 | pub admin_username: String, 12 | } 13 | 14 | impl AppState { 15 | pub async fn new() -> Result> { 16 | let env_config = EnvVarConfig::new()?; 17 | let redis_helper = RedisHelper::new(&env_config.redis_url, 60).await?; // 60 seconds TTL for Redis keys 18 | log_info!("Connected to Redis"); 19 | 20 | let db_pool = PgPool::connect(&env_config.database_url).await?; 21 | log_info!("Connected to Postgres"); 22 | 23 | let clickhouse_client = clickhouse::Client::default() 24 | .with_url(env_config.clickhouse_url) 25 | .with_database("polyMarket") 26 | .with_user("polyMarket") 27 | .with_password(env_config.clickhouse_password); 28 | let admin_username = env_config.admin_username.clone(); 29 | log_info!("Connected to ClickHouse"); 30 | 31 | Ok(AppState { 32 | admin_username, 33 | db_pool, 34 | redis_helper, 35 | clickhouse_client, 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /service-api/src/bloom_f.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use bloom::{ASMS, BloomFilter}; 4 | use db_service::schema::users::User; 5 | use parking_lot::RwLock; 6 | use utility_helpers::log_info; 7 | use uuid::Uuid; 8 | 9 | #[derive(Clone)] 10 | pub struct BloomFilterWrapper { 11 | /// A thread-safe wrapper around a Bloom filter. 12 | /// 13 | /// **NOTE:** Direct DB update does not update the filter, so it is critical to prevent direct db updates for user 14 | filter: Arc>, 15 | } 16 | 17 | impl BloomFilterWrapper { 18 | pub async fn new(db_pool: &sqlx::PgPool) -> Result> { 19 | let mut filter = BloomFilter::with_rate(0.01, 1_000_000_000); // 1% false positive rate, 1 billion items 20 | 21 | let user_ids = User::get_all_user_ids(db_pool) 22 | .await 23 | .map_err(|e| format!("Failed to fetch user IDs: {}", e))?; 24 | 25 | for id in &user_ids { 26 | filter.insert(id); 27 | } 28 | 29 | log_info!("Bloom filter initialized with {} user IDs", user_ids.len()); 30 | 31 | Ok(BloomFilterWrapper { 32 | filter: Arc::new(RwLock::new(filter)), 33 | }) 34 | } 35 | pub fn contains(&self, id: &Uuid) -> bool { 36 | self.filter.read().contains(id) 37 | } 38 | 39 | pub fn insert(&self, id: &Uuid) { 40 | self.filter.write().insert(id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Container, HStack, Text } from "@chakra-ui/react"; 4 | import { useQueries } from "@tanstack/react-query"; 5 | 6 | import { MarketGetters } from "@/utils/interactions/dataGetter"; 7 | import TrendingMarketCard from "@/components/TrendingMarketCard"; 8 | import { MarketStatus } from "@/generated/grpc_service_types/markets"; 9 | 10 | export default function Home() { 11 | console.log("Variables", process.env); 12 | const [{ data }, { data: closedMarkets }] = useQueries({ 13 | queries: [ 14 | { 15 | queryKey: ["marketData", 1, 10, "open"], 16 | queryFn: () => MarketGetters.getMarketData(1, 10, MarketStatus.OPEN), 17 | }, 18 | { 19 | queryKey: ["recentlyClosedMarkets", 1, 10, "closed"], 20 | queryFn: () => MarketGetters.getMarketData(1, 10, MarketStatus.CLOSED), 21 | }, 22 | ], 23 | }); 24 | return ( 25 | 26 | 27 | Trending Markets 28 | 29 | 30 | {data?.map((item) => )} 31 | 32 | 33 | 34 | Recently Closed 35 | 36 | 37 | {closedMarkets?.map((item) => )} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface TooltipProps extends ChakraTooltip.RootProps { 5 | showArrow?: boolean 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | content: React.ReactNode 9 | contentProps?: ChakraTooltip.ContentProps 10 | disabled?: boolean 11 | } 12 | 13 | export const Tooltip = React.forwardRef( 14 | function Tooltip(props, ref) { 15 | const { 16 | showArrow, 17 | children, 18 | disabled, 19 | portalled = true, 20 | content, 21 | contentProps, 22 | portalRef, 23 | ...rest 24 | } = props 25 | 26 | if (disabled) return children 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | 33 | 34 | {showArrow && ( 35 | 36 | 37 | 38 | )} 39 | {content} 40 | 41 | 42 | 43 | 44 | ) 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /db-service/migrations/20250511185915_adding_updated_at_triggers.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | 3 | 4 | -- function to update the updated_at field 5 | CREATE OR REPLACE FUNCTION polymarket.set_updated_at() 6 | RETURNS TRIGGER AS $$ 7 | BEGIN 8 | NEW.updated_at = CURRENT_TIMESTAMP; 9 | RETURN NEW; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | 13 | 14 | -- triggers 15 | 16 | -- users 17 | CREATE TRIGGER set_updated_at_users_trigger 18 | BEFORE UPDATE ON "polymarket"."users" 19 | FOR EACH ROW 20 | EXECUTE FUNCTION polymarket.set_updated_at(); 21 | 22 | -- markets 23 | CREATE TRIGGER set_updated_at_markets_trigger 24 | BEFORE UPDATE ON "polymarket"."markets" 25 | FOR EACH ROW 26 | EXECUTE FUNCTION polymarket.set_updated_at(); 27 | 28 | -- orders 29 | CREATE TRIGGER set_updated_at_orders_trigger 30 | BEFORE UPDATE ON "polymarket"."orders" 31 | FOR EACH ROW 32 | EXECUTE FUNCTION polymarket.set_updated_at(); 33 | 34 | -- user_trades 35 | CREATE TRIGGER set_updated_at_user_trades_trigger 36 | BEFORE UPDATE ON "polymarket"."user_trades" 37 | FOR EACH ROW 38 | EXECUTE FUNCTION polymarket.set_updated_at(); 39 | 40 | -- user_holdings 41 | CREATE TRIGGER set_updated_at_user_holdings_trigger 42 | BEFORE UPDATE ON "polymarket"."user_holdings" 43 | FOR EACH ROW 44 | EXECUTE FUNCTION polymarket.set_updated_at(); 45 | 46 | -- user_transactions 47 | CREATE TRIGGER set_updated_at_user_transactions_trigger 48 | BEFORE UPDATE ON "polymarket"."user_transactions" 49 | FOR EACH ROW 50 | EXECUTE FUNCTION polymarket.set_updated_at(); 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/charts": "^3.21.0", 13 | "@chakra-ui/react": "^3.17.0", 14 | "@emotion/react": "^11.14.0", 15 | "@protobuf-ts/grpcweb-transport": "2.9.3", 16 | "@protobuf-ts/runtime": "2.9.3", 17 | "@protobuf-ts/runtime-rpc": "2.9.3", 18 | "@react-oauth/google": "^0.12.2", 19 | "@tanstack/react-query": "^5.76.0", 20 | "@uidotdev/usehooks": "^2.4.1", 21 | "axios": "^1.9.0", 22 | "js-cookie": "^3.0.5", 23 | "lucide-react": "^0.518.0", 24 | "next": "15.3.2", 25 | "next-themes": "^0.4.6", 26 | "protobufjs": "^7.5.1", 27 | "react": "^19.0.0", 28 | "react-dom": "^19.0.0", 29 | "react-hook-form": "^7.58.1", 30 | "recharts": "^2.15.4", 31 | "zustand": "^5.0.5" 32 | }, 33 | "devDependencies": { 34 | "@eslint/eslintrc": "^3", 35 | "@protobuf-ts/plugin": "^2.10.0", 36 | "@protobuf-ts/protoc": "^2.10.0", 37 | "@tailwindcss/postcss": "^4", 38 | "@types/js-cookie": "^3.0.6", 39 | "@types/node": "^20", 40 | "@types/react": "^19", 41 | "@types/react-dom": "^19", 42 | "eslint": "^9", 43 | "eslint-config-next": "15.3.2", 44 | "prettier": "^3.4.2", 45 | "prettier-plugin-tailwindcss": "^0.6.9", 46 | "tailwindcss": "^4", 47 | "ts-proto": "^2.6.1", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /app/Makefile: -------------------------------------------------------------------------------- 1 | get-proto-files: 2 | @cp -r ../proto-defs/proto/ws_server/*.proto ./public/proto/proto_defs 3 | @cp -r ../proto-defs/proto/api_service/*.proto ./public/proto/proto_defs 4 | @cp -r ../grpc-service/proto/*.proto ./public/proto/grpc_services 5 | @echo "Proto files copied to public/proto directory." 6 | 7 | generate-all-types: generate-service-types generate-grpc-types 8 | 9 | generate-service-types: 10 | @cp -r ../proto-defs/proto/* ./public/proto/proto_defs 11 | @echo "Generating types make sure that all files are in public/proto directory" 12 | @protoc -I=./public/proto/proto_defs \ 13 | -I=$(shell pkg-config --variable=prefix protobuf)/include \ 14 | --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \ 15 | --ts_out=./src/generated/service_types \ 16 | --ts_opt=generate_dependencies,long_type_number \ 17 | $(shell find ./public/proto/proto_defs/ws_server -name '*.proto') 18 | $(shell find ./public/proto/proto_defs/api_service -name '*.proto') 19 | @echo "Types generated successfully." 20 | 21 | 22 | generate-grpc-types: 23 | @cp -r ../grpc-service/proto/*.proto ./public/proto/grpc_services 24 | @echo "Generating types make sure that all files are in public/proto directory" 25 | @protoc -I=./public/proto/grpc_services \ 26 | -I=$(shell pkg-config --variable=prefix protobuf)/include \ 27 | --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \ 28 | --ts_out=./src/generated/grpc_service_types \ 29 | --ts_opt=generate_dependencies,long_type_number \ 30 | $(shell find ./public/proto/grpc_services -name '*.proto') 31 | @echo "Types generated successfully." 32 | -------------------------------------------------------------------------------- /app/src/app/market/[id]/_components/HoldingsInfoClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "@tanstack/react-query"; 4 | import { Box, Flex, Separator, Text } from "@chakra-ui/react"; 5 | 6 | import { OrderGetters } from "@/utils/interactions/dataGetter"; 7 | 8 | type Props = { 9 | marketId: string; 10 | }; 11 | 12 | const HoldingsInfoClient = ({ marketId }: Props) => { 13 | const { isLoading, data } = useQuery({ 14 | queryKey: ["marketOrders", marketId], 15 | queryFn: () => OrderGetters.getUserOrdersByMarket(marketId, 1, 10), 16 | }); 17 | const yesHoldings = isLoading 18 | ? "--" 19 | : Number(data?.holdings.yes).toFixed(3) || "0"; 20 | const noHoldings = isLoading 21 | ? "--" 22 | : Number(data?.holdings.no).toFixed(3) || "0"; 23 | return ( 24 | 25 | 26 | 27 | Holdings 28 | 29 | 30 | 31 | 32 | 33 | {yesHoldings} 34 | 35 | 36 | 37 | 38 | 39 | {noHoldings} 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default HoldingsInfoClient; 48 | -------------------------------------------------------------------------------- /service-api/src/routes/user/holdings.rs: -------------------------------------------------------------------------------- 1 | use auth_service::types::SessionTokenClaims; 2 | use axum::{ 3 | Extension, Json, 4 | extract::{Query, State}, 5 | http::StatusCode, 6 | response::{IntoResponse, Response}, 7 | }; 8 | use db_service::schema::user_holdings::UserHoldings; 9 | use serde_json::json; 10 | use utility_helpers::log_error; 11 | 12 | use crate::{state::AppState, utils::types::PaginationRequestQuery, validate_paginated_fields}; 13 | 14 | pub async fn get_user_holdings( 15 | State(state): State, 16 | Query(params): Query, 17 | Extension(claims): Extension, 18 | ) -> Result { 19 | let user_id = claims.user_id; 20 | 21 | validate_paginated_fields!(params.page, params.page_size); 22 | 23 | let holdings = UserHoldings::get_user_holdings_by_market_paginated( 24 | user_id, 25 | params.page, 26 | params.page_size, 27 | &state.pg_pool, 28 | ) 29 | .await 30 | .map_err(|e| { 31 | log_error!("Failed to fetch user holdings {e:?}"); 32 | ( 33 | StatusCode::INTERNAL_SERVER_ERROR, 34 | Json(json!({"message": "Failed to fetch user holdings"})).into_response(), 35 | ) 36 | })?; 37 | 38 | Ok(( 39 | StatusCode::OK, 40 | Json(json!({ 41 | "message": "User holdings fetched successfully", 42 | "data": { 43 | "holdings": holdings.items, 44 | "page_info": holdings.page_info 45 | } 46 | })), 47 | )) 48 | } 49 | -------------------------------------------------------------------------------- /grpc-service/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fs::{self, File}, 4 | io::Write, 5 | path::PathBuf, 6 | }; 7 | 8 | fn main() -> Result<(), Box> { 9 | let out_dir = PathBuf::from("./src/generated"); 10 | fs::create_dir_all(&out_dir)?; 11 | 12 | tonic_build::configure() 13 | .protoc_arg("--proto_path=proto") 14 | .protoc_arg("--experimental_allow_proto3_optional") 15 | .file_descriptor_set_path(out_dir.join("descriptor.bin")) 16 | .out_dir(&out_dir) 17 | .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") 18 | .build_client(false) 19 | .compile_protos(&["proto/markets.proto", "proto/price.proto"], &["proto"])?; 20 | 21 | // building common mod.rs file with all module names 22 | 23 | let entries = fs::read_dir(&out_dir)? 24 | .filter_map(|e| e.ok()) 25 | .filter(|e| e.path().extension().map(|ext| ext == "rs").unwrap_or(false)); 26 | 27 | // for creating mod.rs with all modules (for allowing access to all crates) 28 | let mut mod_rs = File::create(out_dir.join("mod.rs"))?; 29 | for entry in entries { 30 | let file_name = entry.file_name(); 31 | let module_name = file_name 32 | .to_string_lossy() 33 | .trim_end_matches(".rs") 34 | .to_string(); 35 | if module_name == "mod" { 36 | // skip the mod.rs file itself 37 | continue; 38 | } 39 | writeln!(mod_rs, "pub mod {};", module_name)?; 40 | } 41 | 42 | println!("cargo:rerun-if-changed=proto/"); 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /service-api/src/routes/user/profile.rs: -------------------------------------------------------------------------------- 1 | use auth_service::types::SessionTokenClaims; 2 | use axum::{ 3 | Extension, Json, 4 | extract::State, 5 | http::StatusCode, 6 | response::{IntoResponse, Response}, 7 | }; 8 | use db_service::schema::users::User; 9 | use serde_json::json; 10 | use utility_helpers::{log_error, redis::keys::RedisKey}; 11 | 12 | use crate::state::AppState; 13 | 14 | pub async fn get_profile( 15 | State(app_state): State, 16 | Extension(claims): Extension, 17 | ) -> Result { 18 | let user_id = claims.user_id; 19 | 20 | let user_key = RedisKey::User(user_id); 21 | let user = app_state 22 | .redis_helper 23 | .get_or_set_cache( 24 | user_key, 25 | || async { Ok(User::get_user_by_id(&app_state.pg_pool, user_id).await?) }, 26 | Some(20), // Cache for 20 seconds 27 | ) 28 | .await 29 | .map_err(|err| { 30 | log_error!("Failed to retrieve user profile: {}", err); 31 | ( 32 | StatusCode::INTERNAL_SERVER_ERROR, 33 | Json(json!({ 34 | "error": "Failed to retrieve user profile" 35 | })) 36 | .into_response(), 37 | ) 38 | })?; 39 | 40 | let response = json!({ 41 | "email": user.email, 42 | "name": user.name, 43 | "avatar": user.avatar, 44 | "public_key": user.public_key, 45 | "balance": user.balance, 46 | }); 47 | 48 | Ok((StatusCode::OK, Json(response))) 49 | } 50 | -------------------------------------------------------------------------------- /service-api/src/routes/user/trades/get_user_trades.rs: -------------------------------------------------------------------------------- 1 | use auth_service::types::SessionTokenClaims; 2 | use axum::{ 3 | Extension, Json, 4 | extract::{Query, State}, 5 | http::StatusCode, 6 | response::{IntoResponse, Response}, 7 | }; 8 | use db_service::schema::user_trades::UserTrades; 9 | use serde_json::json; 10 | use utility_helpers::log_error; 11 | 12 | use crate::{state::AppState, utils::types::PaginationRequestQuery, validate_paginated_fields}; 13 | 14 | pub async fn get_user_trades( 15 | State(app_state): State, 16 | Query(params): Query, 17 | Extension(claims): Extension, 18 | ) -> Result { 19 | let user_id = claims.user_id; 20 | 21 | validate_paginated_fields!(params.page, params.page_size); 22 | 23 | let user_trades = UserTrades::get_user_trades_paginated( 24 | user_id, 25 | params.page, 26 | params.page_size, 27 | &app_state.pg_pool, 28 | ) 29 | .await 30 | .map_err(|e| { 31 | log_error!("Failed to fetch user trades {e:?}"); 32 | ( 33 | StatusCode::INTERNAL_SERVER_ERROR, 34 | Json(json!({"message": "Failed to fetch user trades"})).into_response(), 35 | ) 36 | })?; 37 | 38 | Ok(( 39 | StatusCode::OK, 40 | Json(json!({ 41 | "message": "User trades fetched successfully", 42 | "data": { 43 | "trades": user_trades.items, 44 | "page_info": user_trades.page_info 45 | } 46 | })), 47 | )) 48 | } 49 | -------------------------------------------------------------------------------- /proto-defs/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | path::PathBuf, 5 | }; 6 | 7 | pub fn main() -> Result<(), Box> { 8 | let out_dir = PathBuf::from("./src/proto_types"); 9 | 10 | fs::create_dir_all(&out_dir)?; 11 | 12 | let mut config = prost_build::Config::new(); 13 | 14 | config 15 | .protoc_arg("--proto_path=proto") 16 | .protoc_arg("--experimental_allow_proto3_optional") 17 | .out_dir(&out_dir) 18 | .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") 19 | .compile_protos( 20 | &[ 21 | "proto/ws_server/market_price.proto", 22 | "proto/ws_server/order_book.proto", 23 | "proto/ws_server/common.proto", 24 | ], 25 | &["proto"], 26 | )?; 27 | 28 | let entries = fs::read_dir(&out_dir)? 29 | .filter_map(|e| e.ok()) 30 | .filter(|e| e.path().extension().map(|ext| ext == "rs").unwrap_or(false)); 31 | 32 | // for creating mod.rs with all modules (for allowing access to all crates) 33 | let mut mod_rs = File::create(out_dir.join("mod.rs"))?; 34 | for entry in entries { 35 | let file_name = entry.file_name(); 36 | let module_name = file_name 37 | .to_string_lossy() 38 | .trim_end_matches(".rs") 39 | .to_string(); 40 | if module_name == "mod" { 41 | continue; 42 | } 43 | writeln!(mod_rs, "pub mod {};", module_name)?; 44 | } 45 | 46 | println!("cargo:rerun-if-changed=proto/"); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | workspace.resolver = "2" 2 | 3 | workspace.members = [ 4 | "auth-service", 5 | "db-service", 6 | "grpc-service", 7 | "order-service", 8 | "proto-defs", 9 | "service-api", 10 | "utility-helpers", 11 | "websocket-service", 12 | ] 13 | 14 | [workspace.dependencies] 15 | tokio = { version = "1.45.0", features = ["full"] } 16 | uuid = { version = "1.16.0", features = ["serde", "v5", "v4"] } 17 | serde = { version = "1.0.219", features = ["derive"] } 18 | serde_json = "1.0.140" 19 | chrono = { version = "0.4.41", features = ["serde"] } 20 | tracing = "0.1" 21 | tracing-subscriber = "0.3.0" 22 | rust_decimal = { version = "1.37.1", features = ["serde"] } 23 | rust_decimal_macros = "1.37.1" 24 | solana-sdk = "2.2.2" 25 | dotenv = "0.15.0" 26 | base64 = "0.22.1" 27 | deadpool-redis = { version = "0.20.0", features = [ 28 | "tokio-comp", 29 | "tokio-rustls-comp", 30 | "rt_tokio_1", 31 | ] } 32 | redis = { version = "0.28.1", features = ["tokio-comp", "tokio-rustls-comp"] } 33 | tower-http = { version = "0.6.4", features = ["cors"] } 34 | sqlx = { version = "0.8", features = [ 35 | "runtime-tokio", 36 | "tls-rustls-ring-webpki", 37 | "postgres", 38 | "chrono", 39 | "uuid", 40 | "rust_decimal", 41 | "macros", 42 | ] } 43 | reqwest = { version = "0.12.15", features = ["json"] } 44 | jsonwebtoken = "9.3.1" 45 | prost = "0.13.5" 46 | prost-types = "0.13.5" 47 | async-nats = { version = "0.40.0" } 48 | futures-util = "0.3.31" 49 | futures = "0.3.31" 50 | bloom = "0.3.2" 51 | parking_lot = { version = "0.12.3" } 52 | rmp-serde = "1.3.0" 53 | rdkafka = { version = "0.38.0", features = ["cmake-build"] } 54 | -------------------------------------------------------------------------------- /queries/pg_queries/user_trades.sql: -------------------------------------------------------------------------------- 1 | -- truncate table polymarket.user_trades; 2 | 3 | select * from polymarket.user_trades order by created_at DESC; 4 | 5 | -- SELECT 6 | -- market_id, 7 | -- SUM(quantity) AS total_volume 8 | -- FROM polymarket.user_trades 9 | -- WHERE timestamp >= NOW() - INTERVAL '6 hours' 10 | -- GROUP BY market_id; 11 | 12 | -- select t.id ,u.name, u.email, u.avatar, t.trade_type, t.outcome, t.price, t.quantity, t.timestamp 13 | -- FROM polymarket.user_trades t 14 | -- RIGHT JOIN polymarket.users u ON u.id = t.user_id 15 | -- WHERE u.name != 'Admin' AND t.market_id = '20ec3758-04ef-4300-a24c-c9019cf55c95'::uuid 16 | -- ORDER BY t.timestamp DESC; 17 | 18 | -- select 19 | -- m.name as market_name, 20 | -- m.logo as market_logo, 21 | -- m.status as market_status, 22 | -- m.final_outcome as market_final_outcome, 23 | 24 | -- t.trade_type, 25 | -- t.outcome as trade_outcome, 26 | -- t.price as trade_price, 27 | -- t.quantity as trade_quantity 28 | -- FROM polymarket.user_trades t 29 | -- JOIN polymarket.markets m ON t.market_id = m.id 30 | -- ORDER BY t.created_at DESC; 31 | 32 | 33 | 34 | -- paginated 35 | 36 | SELECT 37 | m.name AS market_name, 38 | m.logo AS market_logo, 39 | m.status AS "market_status: MarketStatus", 40 | m.final_outcome AS "market_final_outcome: Outcome", 41 | 42 | t.trade_type AS "trade_type: OrderSide", 43 | t.outcome AS "trade_outcome: Outcome", 44 | t.price AS trade_price, 45 | t.quantity AS trade_quantity 46 | FROM polymarket.user_trades t 47 | JOIN polymarket.markets m ON t.market_id = m.id 48 | WHERE t.user_id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid 49 | ORDER BY t.timestamp DESC 50 | LIMIT 10 OFFSET 1; -------------------------------------------------------------------------------- /app/src/utils/protoHelpers.ts: -------------------------------------------------------------------------------- 1 | import { load, Type } from "protobufjs"; 2 | 3 | const protoMap = new Map(); 4 | 5 | async function loadProtoFile(filePath: string, lookupKey: string) { 6 | const cacheKey = `${filePath}:${lookupKey}`; 7 | 8 | if (protoMap.has(cacheKey)) { 9 | return protoMap.get(cacheKey)!; 10 | } 11 | 12 | if (filePath.endsWith(".proto")) { 13 | const root = await load(filePath); 14 | const messageType = root.lookupType(lookupKey); 15 | protoMap.set(cacheKey, messageType); 16 | 17 | return messageType; 18 | } else { 19 | throw new Error("Invalid file type. Only .proto files are supported."); 20 | } 21 | } 22 | 23 | export async function encodeProtoMessage( 24 | filePath: string, 25 | lookupKey: string, 26 | message: object, 27 | ): Promise { 28 | const messageType = await loadProtoFile(filePath, lookupKey); 29 | const errMsg = messageType.verify(message); 30 | if (errMsg) { 31 | throw new Error(errMsg); 32 | } 33 | const buffer = messageType.encode(message).finish(); 34 | return buffer; 35 | } 36 | 37 | export async function decodeProtoMessage( 38 | filePath: string, 39 | lookupKey: string, 40 | buffer: ArrayBufferLike, 41 | ): Promise { 42 | const messageType = await loadProtoFile(filePath, lookupKey); 43 | const uintData = new Uint8Array(buffer); 44 | const message = messageType.decode(uintData); 45 | const verified = messageType.verify(uintData); 46 | if (verified) throw new Error("Invalid data"); 47 | const obj = messageType.toObject(message, { 48 | longs: String, 49 | enums: String, 50 | bytes: String, 51 | }) as unknown as T; 52 | return obj; 53 | } 54 | -------------------------------------------------------------------------------- /websocket-service/src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | Router, 3 | extract::{State, ws::WebSocketUpgrade}, 4 | routing::any, 5 | }; 6 | use std::sync::Arc; 7 | use tracing_subscriber; 8 | use utility_helpers::{log_error, log_info}; 9 | 10 | use crate::{ 11 | core::handle_connection::handle_connection, nats_handler::nats_handler, 12 | state::WebSocketAppState, 13 | }; 14 | 15 | mod core; 16 | mod nats_handler; 17 | mod state; 18 | 19 | pub type SafeAppState = Arc; 20 | 21 | #[tokio::main(flavor = "multi_thread")] 22 | async fn main() -> Result<(), Box> { 23 | tracing_subscriber::fmt::init(); 24 | let app_state = WebSocketAppState::new().await?; 25 | let app_state = Arc::new(app_state); 26 | let nats_handler_state = app_state.clone(); 27 | 28 | let app = Router::new() 29 | .route("/", any(|| async { "Hello from WebSocket server!" })) 30 | .route("/ws", any(socket_handler)) 31 | .with_state(app_state); 32 | 33 | let listener = tokio::net::TcpListener::bind("[::]:4010").await?; 34 | 35 | log_info!("Starting WebSocket server on port 4010"); 36 | tokio::spawn(async move { 37 | if let Err(e) = nats_handler(nats_handler_state).await { 38 | log_error!("Error in NATS handler: {}", e); 39 | panic!("NATS handler encountered an error"); 40 | }; 41 | }); 42 | 43 | axum::serve(listener, app).await?; 44 | 45 | Ok(()) 46 | } 47 | 48 | async fn socket_handler( 49 | ws: WebSocketUpgrade, 50 | State(state): State, 51 | ) -> impl axum::response::IntoResponse { 52 | ws.on_upgrade(move |socket| handle_connection(socket, state)) 53 | } 54 | -------------------------------------------------------------------------------- /service-api/src/utils/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! require_field { 3 | ($field:expr) => { 4 | if $field.is_none() { 5 | let full = stringify!($field); 6 | let short = full.split('.').last().unwrap_or(full); 7 | return Err(( 8 | axum::http::StatusCode::BAD_REQUEST, 9 | axum::Json(serde_json::json!({ 10 | "message": format!("Missing required field: {}", short), 11 | })).into_response(), 12 | )) 13 | } 14 | }; 15 | } 16 | 17 | #[macro_export] 18 | macro_rules! require_fields_raw_response { 19 | ($field:expr) => { 20 | if $field.is_none() { 21 | let full = stringify!($field); 22 | let short = full.split('.').last().unwrap_or(full); 23 | return Err(( 24 | axum::http::StatusCode::BAD_REQUEST, 25 | axum::Json(serde_json::json!({ 26 | "message": format!("Missing required field: {}", short), 27 | })), 28 | )) 29 | } 30 | }; 31 | } 32 | 33 | #[macro_export] 34 | macro_rules! validate_paginated_fields { 35 | ($page:expr, $page_size:expr) => { 36 | 37 | let page = $page; 38 | let page_size = $page_size; 39 | 40 | if page == 0 || page_size == 0 { 41 | return Err(( 42 | axum::http::StatusCode::BAD_REQUEST, 43 | axum::Json(serde_json::json!({ 44 | "message": "Page and page_size must be greater than 0" 45 | })) 46 | .into_response(), 47 | )); 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /websocket-service/src/core/handle_connection.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::extract::ws::{Message, WebSocket}; 4 | use futures::StreamExt; 5 | use tokio::sync::Mutex; 6 | use utility_helpers::{log_error, log_info}; 7 | use uuid::Uuid; 8 | 9 | use crate::{ 10 | SafeAppState, 11 | core::{SafeSender, message_handlers::handle_message, send_message}, 12 | }; 13 | 14 | pub async fn handle_connection(stream: WebSocket, state: SafeAppState) { 15 | let (tx, mut rx) = stream.split(); 16 | 17 | let tx = Arc::new(Mutex::new(tx)); 18 | let client_id = Uuid::new_v4(); 19 | log_info!("New client connected: {client_id}"); 20 | 21 | let heart_beat_handler = start_heartbeat(tx.clone(), client_id).await; // spawns task and return join handler immediately 22 | handle_message(&mut rx, &tx, &client_id, &state).await; 23 | 24 | // cleanup 25 | log_info!("Client {client_id} disconnected, cleaning up resources"); 26 | let mut channel_manager_guard = state.client_manager.write().await; 27 | channel_manager_guard.remove_client_without_channel(&client_id); 28 | heart_beat_handler.abort(); 29 | log_info!("Resource cleaned"); 30 | } 31 | 32 | async fn start_heartbeat(tx: SafeSender, client_id: Uuid) -> tokio::task::JoinHandle<()> { 33 | tokio::spawn(async move { 34 | let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(45)); 35 | 36 | loop { 37 | interval.tick().await; 38 | 39 | if let Err(e) = send_message(&tx, Message::Ping(vec![].into())).await { 40 | log_error!("Heartbeat failed for client {client_id}: {e}"); 41 | break; 42 | } 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /queries/pg_queries/userHoldings.sql: -------------------------------------------------------------------------------- 1 | -- truncate table polymarket.user_holdings; 2 | 3 | -- delete from polymarket.user_holdings where id != '8c848e79-c2d2-46e5-bdac-f6af89e59afb'::uuid; 4 | 5 | 6 | -- INSERT INTO polymarket.user_holdings (user_id, market_id, shares) 7 | -- VALUES ('24fa20ac-822f-49e9-9cb6-e25e940ad608'::uuid, 'bd609b17-d3d3-4f70-a5e2-0a3f3aa2160c'::uuid, -10) 8 | -- ON CONFLICT (user_id, market_id) 9 | -- DO UPDATE SET shares = polymarket.user_holdings.shares + -10, 10 | -- updated_at = NOW() 11 | -- RETURNING id, user_id, market_id, shares, created_at, updated_at; 12 | 13 | 14 | -- INSERT INTO polymarket.user_holdings (user_id, market_id, shares) 15 | -- VALUES ('24fa20ac-822f-49e9-9cb6-e25e940ad608'::uuid, 'bd609b17-d3d3-4f70-a5e2-0a3f3aa2160c'::uuid, 200) 16 | -- ON CONFLICT (user_id, market_id) DO NOTHING; 17 | 18 | -- SELECT 19 | -- uh.market_id, 20 | -- uh.outcome, 21 | -- uh.shares, 22 | 23 | -- m.name AS market_name, 24 | -- m.description AS market_description, 25 | -- m.logo AS market_logo, 26 | -- m.status AS market_status, 27 | -- m.final_outcome, 28 | -- m.market_expiry, 29 | -- m.created_at AS market_created_at, 30 | -- m.updated_at AS market_updated_at 31 | 32 | -- FROM polymarket.user_holdings uh 33 | -- JOIN polymarket.markets m ON uh.market_id = m.id 34 | -- WHERE uh.user_id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid; 35 | 36 | 37 | 38 | select * from polymarket.user_holdings order by created_at DESC; 39 | 40 | select 41 | uh.user_id, 42 | uh.shares, 43 | uh.outcome, 44 | u.balance, 45 | u.email 46 | FROM 47 | polymarket.user_holdings uh 48 | JOIN 49 | polymarket.users u ON uh.user_id = u.id 50 | ORDER BY 51 | uh.created_at DESC; -------------------------------------------------------------------------------- /service-api/src/utils/middleware.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::{ 4 | Json, 5 | body::Body, 6 | extract::{Request, State}, 7 | http::StatusCode, 8 | middleware::Next, 9 | response::IntoResponse, 10 | }; 11 | use serde_json::json; 12 | 13 | use crate::state::AppState; 14 | 15 | pub async fn validate_jwt( 16 | State(app_state): State>, 17 | req: Request, 18 | next: Next, 19 | ) -> Result { 20 | let missing_token_error = ( 21 | StatusCode::BAD_REQUEST, 22 | Json(json!({ 23 | "error": "Missing auth token" 24 | })), 25 | ); 26 | let invalid_token_error = ( 27 | StatusCode::UNAUTHORIZED, 28 | Json(json!({ 29 | "error": "Invalid token" 30 | })), 31 | ); 32 | let auth_header = req.headers().get("Authorization"); 33 | let token = match auth_header { 34 | Some(header) => header 35 | .to_str() 36 | .ok() 37 | .and_then(|h| h.strip_prefix("Bearer ")) 38 | .ok_or(missing_token_error)?, 39 | None => return Err(missing_token_error), 40 | }; 41 | 42 | let claims = app_state 43 | .auth_service 44 | .verify_session_token(token) 45 | .map_err(|_| invalid_token_error)?; 46 | 47 | // bloom filter check 48 | let user_id = claims.user_id; 49 | 50 | if !app_state.bloom_filter.contains(&user_id) { 51 | return Err(( 52 | StatusCode::NOT_FOUND, 53 | Json(json!({ 54 | "error": "User not found" 55 | })), 56 | )); 57 | } 58 | 59 | let mut req = req; 60 | req.extensions_mut().insert(claims); 61 | 62 | Ok(next.run(req).await) 63 | } 64 | -------------------------------------------------------------------------------- /db-service/src/schema/user_transactions.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDateTime; 2 | use rust_decimal::Decimal; 3 | use serde::Serialize; 4 | use uuid::Uuid; 5 | 6 | use super::enums::{UserTransactionStatus, UserTransactionType}; 7 | 8 | #[derive(Debug, sqlx::FromRow, Default, Serialize)] 9 | pub struct UserTransactions { 10 | pub id: Uuid, 11 | pub user_id: Uuid, 12 | pub amount: Decimal, 13 | pub transaction_type: UserTransactionType, 14 | pub transaction_status: UserTransactionStatus, 15 | pub tx_hash: String, 16 | pub confirmed_at: Option, 17 | pub created_at: NaiveDateTime, 18 | pub updated_at: NaiveDateTime, 19 | } 20 | 21 | impl UserTransactions { 22 | pub async fn create_user_transaction( 23 | pg_pool: &sqlx::PgPool, 24 | user_id: Uuid, 25 | amount: Decimal, 26 | transaction_type: UserTransactionType, 27 | transaction_status: UserTransactionStatus, 28 | tx_hash: String, 29 | ) -> Result { 30 | let transaction = sqlx::query_as!( 31 | UserTransactions, 32 | r#" 33 | INSERT INTO polymarket.user_transactions (user_id, amount, transaction_type, transaction_status, tx_hash) 34 | VALUES ($1, $2, $3, $4, $5) 35 | RETURNING id, user_id, amount, transaction_type as "transaction_type: UserTransactionType", transaction_status as "transaction_status: UserTransactionStatus", tx_hash, confirmed_at, created_at, updated_at 36 | "#, 37 | user_id, 38 | amount, 39 | transaction_type as UserTransactionType, 40 | transaction_status as UserTransactionStatus, 41 | tx_hash 42 | ) 43 | .fetch_one(pg_pool) 44 | .await?; 45 | 46 | Ok(transaction) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /auth-service/src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Deserialize, Serialize, Debug)] 5 | pub enum GoogleClaimsError { 6 | InvalidTokenId, 7 | MissingKid, 8 | FailedToGetKeyFromGoogle, 9 | InvalidResponseTypeFromGoogle, 10 | InvalidKeyComponentFromGoogle, 11 | FailedToDecodeRsaComponents, 12 | KeyNotFound, 13 | ExpiredToken, 14 | FailedToDecodeKeyFromGoogle, 15 | FailedToSetJwkSetFromGoogle, 16 | FailedToDecodeHeader, 17 | FailedToGetHeaderSlice, 18 | FailedToGetTokenDataClaims, 19 | FailedToValidateTokenFromGoogle, 20 | ExpiredOrInvalidToken, 21 | FailedToDecodeAuthResponseFromGoogle, 22 | InvalidIssuer, 23 | MissingIssuer, 24 | InvalidClientId, 25 | MissingClientId, 26 | } 27 | 28 | #[derive(Deserialize, Serialize, Debug)] 29 | pub enum AuthenticateUserError { 30 | InvalidToken, 31 | FailedToInsertUser, 32 | FailedToGenerateSessionToken, 33 | } 34 | 35 | #[derive(Deserialize, Serialize, Debug, Clone)] 36 | pub struct GoogleTokenInfoResponse { 37 | pub iss: String, 38 | pub azp: Option, 39 | pub aud: String, 40 | pub sub: String, 41 | pub email: String, 42 | pub email_verified: Option, 43 | pub nbf: Option, 44 | pub name: String, 45 | pub picture: String, 46 | pub given_name: Option, 47 | pub family_name: Option, 48 | pub iat: Option, 49 | pub exp: String, 50 | pub jti: Option, 51 | pub alg: Option, 52 | pub kid: Option, 53 | pub typ: Option, 54 | } 55 | 56 | #[derive(Deserialize, Serialize, Debug, Clone)] 57 | pub struct SessionTokenClaims { 58 | pub user_id: Uuid, 59 | pub google_sub: String, 60 | pub email: Option, 61 | pub exp: usize, 62 | } 63 | -------------------------------------------------------------------------------- /order-service/src/handlers/ws_handler/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures_util::{SinkExt, StreamExt}; 4 | use tokio_tungstenite::tungstenite::Message; 5 | use utility_helpers::{log_error, log_warn, ws::types::ClientMessage}; 6 | 7 | use crate::{handlers::ws_handler::handle_text_messages::handle_text_messages, state::AppState}; 8 | 9 | mod handle_text_messages; 10 | 11 | pub async fn handle_ws_messages(state: Arc) -> Result<(), Box> { 12 | let mut rx = state.ws_rx.write().await; 13 | 14 | while let Some(data) = rx.next().await { 15 | match data { 16 | Ok(data) => match data { 17 | Message::Text(text) => { 18 | let parsed_message: Result = serde_json::from_str(&text); 19 | match parsed_message { 20 | Ok(client_message) => { 21 | handle_text_messages(&state, &client_message).await; 22 | } 23 | Err(e) => { 24 | log_error!("Failed to parse client message: {}", e); 25 | continue; 26 | } 27 | } 28 | } 29 | Message::Ping(_) => { 30 | let mut tx = state.ws_tx.write().await; 31 | if let Err(e) = tx.send(Message::Pong(vec![].into())).await { 32 | log_error!("Failed to send Pong to server : {e}"); 33 | } 34 | } 35 | 36 | _ => { 37 | log_warn!("Received non-text message: {:?}", data); 38 | } 39 | }, 40 | Err(e) => { 41 | log_error!("WebSocket error: {}", e); 42 | } 43 | } 44 | } 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /grpc-service/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use grpc_service::{ 4 | generated::{ 5 | markets::market_service_server::MarketServiceServer, 6 | price::price_service_server::PriceServiceServer, 7 | }, 8 | procedures::{market_services::MarketServiceStub, price_services::PriceServiceStub}, 9 | state::AppState, 10 | }; 11 | use tonic::transport::Server; 12 | use tonic_web::GrpcWebLayer; 13 | use tower_http::cors::CorsLayer; 14 | use utility_helpers::log_info; 15 | 16 | const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("generated/descriptor.bin"); 17 | 18 | #[tokio::main(flavor = "multi_thread")] 19 | async fn main() -> Result<(), Box> { 20 | // enabling tracer 21 | tracing_subscriber::fmt::init(); 22 | 23 | let reflector_service = tonic_reflection::server::Builder::configure() 24 | .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET) 25 | .include_reflection_service(true) 26 | .build_v1alpha()?; 27 | 28 | let app_state = AppState::new().await?; 29 | let state = Arc::new(app_state); 30 | 31 | // services 32 | let market_service_layer = MarketServiceStub { 33 | state: state.clone(), 34 | }; 35 | let pair_service_layer = PriceServiceStub { 36 | state: state.clone(), 37 | }; 38 | 39 | log_info!("GRPC server running on port grpc://localhost:5010"); 40 | 41 | let addr = "0.0.0.0:5010".parse()?; 42 | 43 | Server::builder() 44 | .accept_http1(true) 45 | .layer(CorsLayer::permissive()) // allows CORS for all origins 46 | .layer(GrpcWebLayer::new()) 47 | .add_service(reflector_service) 48 | .add_service(MarketServiceServer::new(market_service_layer)) 49 | .add_service(PriceServiceServer::new(pair_service_layer)) 50 | .serve(addr) 51 | .await?; 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-87509857401e65de9da78f71b98113f91b612df227665c2c739a00b1b729c889.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT id, user_id, market_id, shares, created_at, updated_at, outcome as \"outcome: Outcome\"\n FROM polymarket.user_holdings\n WHERE user_id = $1 AND market_id = $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "user_id", 14 | "type_info": "Uuid" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "market_id", 19 | "type_info": "Uuid" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "shares", 24 | "type_info": "Numeric" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "created_at", 29 | "type_info": "Timestamp" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "updated_at", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "outcome: Outcome", 39 | "type_info": { 40 | "Custom": { 41 | "name": "polymarket.outcome", 42 | "kind": { 43 | "Enum": [ 44 | "yes", 45 | "no", 46 | "unspecified" 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | ], 53 | "parameters": { 54 | "Left": [ 55 | "Uuid", 56 | "Uuid" 57 | ] 58 | }, 59 | "nullable": [ 60 | false, 61 | false, 62 | false, 63 | false, 64 | false, 65 | false, 66 | false 67 | ] 68 | }, 69 | "hash": "87509857401e65de9da78f71b98113f91b612df227665c2c739a00b1b729c889" 70 | } 71 | -------------------------------------------------------------------------------- /queries/pg_queries/orders.sql: -------------------------------------------------------------------------------- 1 | 2 | -- select count(*) from polymarket.orders where status = 'open'::polymarket.order_status; 3 | -- select * from polymarket.orders where id = 'c6df1d26-e223-4dc3-98a9-663eb51b293f'::uuid ORDER BY created_at DESC; 4 | 5 | -- select price, status from polymarket.orders where status in ('open'::polymarket.order_status, 'pending_update'::polymarket.order_status) group by price, status; 6 | 7 | -- delete from polymarket.orders where outcome = 'no'::polymarket.outcome CASCADE; 8 | 9 | -- truncate table polymarket.orders CASCADE; 10 | select * from polymarket.orders order by created_at DESC; 11 | 12 | -- for buy 13 | SELECT SUM((price * quantity) * 100) FROM polymarket.orders 14 | WHERE user_id = 'def7d541-6c70-4571-abff-311574ce43ce'::uuid 15 | AND status = 'open'::polymarket.order_status; 16 | 17 | -- for sell check 18 | SELECT SUM(quantity) FROM polymarket.orders 19 | WHERE user_id = 'def7d541-6c70-4571-abff-311574ce43ce'::uuid 20 | AND side = 'sell'::polymarket.order_side 21 | AND outcome = 'yes'::polymarket.outcome 22 | AND status = 'open'::polymarket.order_status; 23 | 24 | -- select * from polymarket.orders where status = 'open'::polymarket.order_status ORDER BY created_at DESC; 25 | 26 | 27 | -- DELETE FROM polymarket.orders 28 | -- WHERE status != ('open'::polymarket.order_status); 29 | 30 | -- SELECT 31 | -- o.id, o.user_id, o.market_id, 32 | -- o.outcome as "outcome: Outcome", 33 | -- o.price, o.quantity, o.filled_quantity, 34 | -- o.status as "status: OrderStatus", 35 | -- o.side as "side: OrderSide", 36 | -- o.created_at, o.updated_at, m.liquidity_b 37 | -- FROM polymarket.orders o 38 | -- JOIN polymarket.markets m ON o.market_id = m.id 39 | -- WHERE o.status = 'open'::polymarket.order_status; 40 | 41 | -- select * from polymarket.orders; -------------------------------------------------------------------------------- /grpc-service/src/utils/timeframe.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Duration, Utc}; 2 | 3 | use crate::generated::common::Timeframe; 4 | 5 | impl Timeframe { 6 | /// Get the duration for the time range 7 | pub fn to_duration(&self) -> Option { 8 | match self { 9 | Timeframe::OneHour => Some(Duration::hours(1)), 10 | Timeframe::SixHour => Some(Duration::hours(6)), 11 | Timeframe::OneDay => Some(Duration::days(1)), 12 | Timeframe::OneWeek => Some(Duration::weeks(1)), 13 | Timeframe::OneMonth => Some(Duration::days(30)), // Approximate month 14 | Timeframe::All => None, // No time limit 15 | Timeframe::Unspecified => None, // Unspecified timeframe 16 | } 17 | } 18 | pub fn as_db_interval_str(&self) -> &str { 19 | match self { 20 | Self::All => "100 years", 21 | Self::Unspecified => "100 years", 22 | Self::OneHour => "1 hour", 23 | Self::SixHour => "6 hours", 24 | Self::OneDay => "1 day", 25 | Self::OneWeek => "1 week", 26 | Self::OneMonth => "1 month", 27 | } 28 | } 29 | 30 | pub fn get_start_time(&self) -> Option> { 31 | self.to_duration().map(|duration| Utc::now() - duration) 32 | } 33 | 34 | pub fn to_sql_condition(&self) -> String { 35 | match self.get_start_time() { 36 | Some(start_time) => format!("ts >= '{}'", start_time.format("%Y-%m-%d %H:%M:%S")), 37 | None => "1=1".to_string(), 38 | } 39 | } 40 | 41 | pub fn to_parameterized_sql(&self) -> (String, Option>) { 42 | match self.get_start_time() { 43 | Some(start_time) => ("ts >= ?".to_string(), Some(start_time)), 44 | None => ("1=1".to_string(), None), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /service-api/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | 3 | use async_nats::{ 4 | connect, 5 | jetstream::{self, Context}, 6 | }; 7 | use auth_service::AuthService; 8 | use db_service::DbService; 9 | use utility_helpers::{log_info, redis::RedisHelper, types::EnvVarConfig}; 10 | 11 | use crate::bloom_f::BloomFilterWrapper; 12 | 13 | #[derive(Clone)] 14 | pub struct AppState { 15 | pub pg_pool: sqlx::PgPool, 16 | pub auth_service: AuthService, 17 | pub jetstream: Context, 18 | pub bloom_filter: BloomFilterWrapper, // already thread safe 19 | pub redis_helper: RedisHelper, 20 | } 21 | 22 | impl AppState { 23 | pub async fn new() -> Result> { 24 | dotenv::dotenv().ok(); 25 | 26 | let env_var_config = EnvVarConfig::new()?; 27 | 28 | let ns = connect(&env_var_config.nc_url).await?; 29 | let jetstream = jetstream::new(ns); 30 | 31 | let pg_pool = sqlx::PgPool::connect(&env_var_config.database_url).await?; 32 | let auth_service = AuthService::new(pg_pool.clone())?; 33 | 34 | let bloom_filter = BloomFilterWrapper::new(&pg_pool).await?; 35 | let redis_helper = RedisHelper::new( 36 | &env_var_config.redis_url, 37 | 60 * 60, // default cache expiration 60 sec * 60 sec = 1 hour 38 | ) 39 | .await?; 40 | 41 | let state = AppState { 42 | pg_pool, 43 | auth_service, 44 | jetstream, 45 | bloom_filter, 46 | redis_helper, 47 | }; 48 | 49 | Ok(state) 50 | } 51 | 52 | pub async fn run_migrations(&self) -> Result<(), Box> { 53 | DbService::run_migrations(&self.pg_pool) 54 | .await 55 | .map_err(|e| format!("Migration failed: {}", e))?; 56 | 57 | log_info!("Database migrations completed successfully."); 58 | 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/app/market/[id]/TabsClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, Tabs } from "@chakra-ui/react"; 4 | import OrderBook from "./_components/OrderBook"; 5 | import MyOrders from "./_components/MyOrders"; 6 | import TopMarketHolders from "./_components/TopMarketHolders"; 7 | import MarketTrades from "./_components/MarketTrades"; 8 | 9 | type Props = { 10 | marketId: string; 11 | yesPrice: number; 12 | noPrice: number; 13 | }; 14 | 15 | const TabsClient = ({ marketId: id, noPrice, yesPrice }: Props) => { 16 | // TODO: add persistent state for the selected tab 17 | return ( 18 |
19 | 20 | 21 | 22 | Trade yes 23 | Trade no 24 | My orders 25 | Top holders 26 | Trades 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | ); 51 | }; 52 | 53 | export default TabsClient; 54 | -------------------------------------------------------------------------------- /order-service/src/handlers/nats_handler/cancel_order_handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use db_service::schema::{enums::OrderStatus, orders::Order}; 4 | use utility_helpers::log_warn; 5 | use uuid::Uuid; 6 | 7 | use crate::{ 8 | state::AppState, 9 | utils::{OrderServiceError, update_services::update_service_state}, 10 | }; 11 | 12 | pub async fn cancel_order_handler( 13 | app_state: Arc, 14 | order_id: Uuid, 15 | ) -> Result<(), OrderServiceError> { 16 | let order = Order::find_order_by_id(order_id, &app_state.db_pool) 17 | .await 18 | .map_err(|e| format!("Failed to find order {:#?}", e))?; 19 | 20 | if order.is_none() { 21 | log_warn!("Order with ID {} not found", order_id); 22 | return Ok(()); 23 | } 24 | 25 | let order = order.unwrap(); 26 | 27 | if order.status != OrderStatus::PendingCancel { 28 | log_warn!( 29 | "Order with ID {} is not in a cancellable state: {:?}", 30 | order_id, 31 | order.status 32 | ); 33 | return Ok(()); 34 | } 35 | 36 | // remove order from the order book 37 | let update_flag = { 38 | // sync block 39 | { 40 | let mut order_book = app_state.order_book.write(); 41 | 42 | order_book.remove_order( 43 | order.market_id, 44 | order_id, 45 | order.side, 46 | order.outcome, 47 | order.price, 48 | ) 49 | } 50 | }; 51 | 52 | // perform db ops 53 | if update_flag { 54 | Order::update_order_status(order_id, OrderStatus::CANCELLED, &app_state.db_pool) 55 | .await 56 | .map_err(|e| format!("Failed to update order status: {:#?}", e))?; 57 | } 58 | 59 | // ws publish remaining if required... 60 | 61 | // update market state 62 | update_service_state(app_state.clone(), &order).await 63 | } 64 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-cccdb2bbd51432463f65eff9517bad135a15ad99e50fc9562cb394e3d68c38c5.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT\n u.id AS user_id,\n u.name AS username,\n u.avatar,\n SUM(uh.shares) AS total_shares,\n SUM(uh.shares) FILTER (WHERE uh.outcome = 'yes'::polymarket.outcome) AS total_yes_shares,\n SUM(uh.shares) FILTER (WHERE uh.outcome = 'no'::polymarket.outcome) AS total_no_shares\n FROM polymarket.user_holdings uh\n JOIN polymarket.users u ON uh.user_id = u.id\n WHERE uh.market_id = $1 AND u.name != $2\n GROUP BY u.id, u.name, u.avatar\n ORDER BY total_shares DESC\n LIMIT $3\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "username", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "avatar", 19 | "type_info": "Varchar" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "total_shares", 24 | "type_info": "Numeric" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "total_yes_shares", 29 | "type_info": "Numeric" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "total_no_shares", 34 | "type_info": "Numeric" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Uuid", 40 | "Text", 41 | "Int8" 42 | ] 43 | }, 44 | "nullable": [ 45 | false, 46 | false, 47 | false, 48 | null, 49 | null, 50 | null 51 | ] 52 | }, 53 | "hash": "cccdb2bbd51432463f65eff9517bad135a15ad99e50fc9562cb394e3d68c38c5" 54 | } 55 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-03e6ad429665b989e171bf7563abf9ab2f362a673e4b3d780643a215f8857f80.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT * FROM \"polymarket\".\"users\" WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "google_id", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "email", 19 | "type_info": "Varchar" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "name", 24 | "type_info": "Varchar" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "avatar", 29 | "type_info": "Varchar" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "last_login", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "public_key", 39 | "type_info": "Varchar" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "private_key", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "balance", 49 | "type_info": "Numeric" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "created_at", 54 | "type_info": "Timestamp" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "updated_at", 59 | "type_info": "Timestamp" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Uuid" 65 | ] 66 | }, 67 | "nullable": [ 68 | false, 69 | false, 70 | false, 71 | false, 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false 79 | ] 80 | }, 81 | "hash": "03e6ad429665b989e171bf7563abf9ab2f362a673e4b3d780643a215f8857f80" 82 | } 83 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-f496e6ee7345609953e70fddaf2597a9e679b0a682623c1cd88b92dff538f628.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT * FROM \"polymarket\".\"users\" WHERE google_id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "google_id", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "email", 19 | "type_info": "Varchar" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "name", 24 | "type_info": "Varchar" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "avatar", 29 | "type_info": "Varchar" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "last_login", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "public_key", 39 | "type_info": "Varchar" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "private_key", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "balance", 49 | "type_info": "Numeric" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "created_at", 54 | "type_info": "Timestamp" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "updated_at", 59 | "type_info": "Timestamp" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text" 65 | ] 66 | }, 67 | "nullable": [ 68 | false, 69 | false, 70 | false, 71 | false, 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false 79 | ] 80 | }, 81 | "hash": "f496e6ee7345609953e70fddaf2597a9e679b0a682623c1cd88b92dff538f628" 82 | } 83 | -------------------------------------------------------------------------------- /order-service/src/handlers/nats_handler/update_order_handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use db_service::schema::{enums::OrderStatus, orders::Order}; 4 | use utility_helpers::{log_error, nats_helper::types::UpdateOrderMessage}; 5 | 6 | use crate::{ 7 | state::AppState, 8 | utils::{ 9 | OrderServiceError, update_matched_orders::update_matched_orders, 10 | update_services::update_service_state, 11 | }, 12 | }; 13 | 14 | pub async fn update_order_handler( 15 | app_state: Arc, 16 | data: UpdateOrderMessage, 17 | ) -> Result<(), OrderServiceError> { 18 | let order = Order::find_order_by_id(data.order_id, &app_state.db_pool) 19 | .await 20 | .map_err(|e| { 21 | log_error!("Error finding order: {}", e); 22 | e 23 | })?; 24 | 25 | if order.is_none() { 26 | log_error!("Order not found with ID: {}", data.order_id); 27 | return Err("Order not found".into()); 28 | } 29 | 30 | let mut order = order.unwrap(); 31 | 32 | if order.status != OrderStatus::PendingUpdate { 33 | log_error!( 34 | "Order with ID {} is not in a updatable state: {:?}", 35 | data.order_id, 36 | order.status 37 | ); 38 | return Ok(()); 39 | } 40 | 41 | // sync block 42 | let matches = { 43 | let mut order_book = app_state.order_book.write(); 44 | 45 | let flg = order_book.update_order(&mut order, data.new_price, data.new_quantity); 46 | 47 | if flg { 48 | order_book.process_order_without_liquidity(&mut order) 49 | } else { 50 | Vec::new() 51 | } 52 | }; 53 | 54 | order 55 | .update(&app_state.db_pool) 56 | .await 57 | .map_err(|e| format!("Failed to update order: {e:#?}"))?; 58 | 59 | tokio::try_join!( 60 | update_matched_orders(matches, app_state.clone(), &order), 61 | update_service_state(app_state.clone(), &order) 62 | )?; 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /app/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Search } from "lucide-react"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | import { Flex, Input, InputGroup, Text } from "@chakra-ui/react"; 8 | 9 | import NavbarAvatarButton from "./NavbarAvatarButton"; 10 | import NavbarNotificationButton from "./NavbarNotificationButton"; 11 | 12 | const Navbar = () => { 13 | return ( 14 | 21 | {/* left side */} 22 | 23 | 24 | Logo 25 | 26 | {/* links */} 27 | 28 | {LINKS.map((link) => ( 29 | 30 | 36 | {link.name} 37 | 38 | 39 | ))} 40 | 41 | 42 | 43 | {/* right section */} 44 | 45 | } 47 | display={["none", "flex"]} 48 | > 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default Navbar; 59 | 60 | const LINKS = [ 61 | { 62 | name: "Home", 63 | href: "/", 64 | }, 65 | // { 66 | // name: "Browse", 67 | // href: "/browse", 68 | // }, 69 | { 70 | name: "Profile", 71 | href: "/profile", 72 | }, 73 | ]; 74 | -------------------------------------------------------------------------------- /websocket-service/src/core/message_handlers/channel_handlers/price_posters.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use axum::extract::ws::Message as WsSendMessage; 4 | use prost::Message; 5 | use proto_defs::proto_types::{ws_common_types::WsData, ws_market_price::WsParamsPayload}; 6 | use utility_helpers::{log_error, log_info, ws::types::ChannelType}; 7 | use uuid::Uuid; 8 | 9 | use crate::{SafeAppState, core::send_message}; 10 | 11 | pub async fn price_poster_handler_bin( 12 | data: &WsData, 13 | state: &SafeAppState, 14 | client_id: &Uuid, 15 | ) -> usize { 16 | let mut served_clients = 0; 17 | if let Ok(msg_payload) = serde_json::from_str::(&data.params) { 18 | // broadcast the message to all clients 19 | let clients = state.client_manager.write().await; 20 | let market_id = Uuid::from_str(&msg_payload.market_id).unwrap_or_else(|_| { 21 | log_error!( 22 | "Invalid market ID from client {client_id}: {}", 23 | msg_payload.market_id 24 | ); 25 | return Uuid::nil(); 26 | }); 27 | let clients = clients.get_clients(&ChannelType::PriceUpdate(market_id)); 28 | let data_to_send = msg_payload.encode_to_vec(); 29 | 30 | if let Some(clients) = clients { 31 | for (client_id, client_tx) in clients.iter() { 32 | if let Err(e) = send_message( 33 | client_tx, 34 | WsSendMessage::Binary(data_to_send.clone().into()), 35 | ) 36 | .await 37 | { 38 | log_error!("Failed to send message to {client_id} - {e:#?}"); 39 | } else { 40 | served_clients += 1; 41 | } 42 | } 43 | } 44 | } else { 45 | log_error!( 46 | "Failed to parse params from client {client_id}: {}", 47 | data.params 48 | ); 49 | } 50 | 51 | log_info!("Served {served_clients} clients"); 52 | 53 | served_clients 54 | } 55 | -------------------------------------------------------------------------------- /websocket-service/src/core/message_handlers/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::ws::{Message, WebSocket}; 2 | use futures::{StreamExt, stream::SplitStream}; 3 | use utility_helpers::{log_error, log_info}; 4 | use uuid::Uuid; 5 | 6 | use crate::{ 7 | SafeAppState, 8 | core::{ 9 | SafeSender, 10 | message_handlers::{ 11 | handle_binary_message::handle_binary_message, handle_text_message::handle_text_message, 12 | }, 13 | send_message, 14 | }, 15 | }; 16 | 17 | pub mod channel_handlers; 18 | pub mod handle_binary_message; 19 | pub mod handle_text_message; 20 | 21 | pub async fn handle_message( 22 | rx: &mut SplitStream, 23 | tx: &SafeSender, 24 | client_id: &Uuid, 25 | state: &SafeAppState, 26 | ) { 27 | while let Some(message) = rx.next().await { 28 | match message { 29 | Ok(message) => match message { 30 | Message::Text(text) => { 31 | handle_text_message(&text, client_id, tx, state).await; 32 | } 33 | Message::Binary(bin) => { 34 | // protobuf 35 | handle_binary_message(&bin, client_id, tx, state).await; 36 | } 37 | Message::Pong(_) => { 38 | log_info!("Received Pong from client {client_id}"); 39 | } 40 | Message::Ping(_) => { 41 | log_info!("Received Ping from client {client_id}"); 42 | if let Err(e) = send_message(tx, Message::Pong(vec![].into())).await { 43 | log_error!("Failed to send Pong to client {client_id}: {e}"); 44 | } 45 | } 46 | Message::Close(_) => { 47 | log_info!("Client {client_id} disconnected"); 48 | return; 49 | } 50 | }, 51 | Err(e) => { 52 | log_error!("Error receiving message from client {client_id}: {e}"); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utility-helpers/src/symmetric.rs: -------------------------------------------------------------------------------- 1 | use aes::cipher::generic_array::GenericArray; 2 | use aes_gcm::{ 3 | Aes256Gcm, KeyInit, Nonce, 4 | aead::{Aead, OsRng, rand_core::RngCore}, 5 | }; 6 | 7 | pub fn encrypt(data: &[u8]) -> Result, Box> { 8 | dotenv::dotenv().ok(); 9 | 10 | let key_str = std::env::var("SECRET_KEY")?; 11 | let key_raw = key_str.as_bytes(); 12 | 13 | if key_raw.len() != 32 { 14 | return Err("Key must be 32 bytes long for AES-256".into()); 15 | } 16 | 17 | let key = GenericArray::clone_from_slice(key_raw); 18 | let cipher = Aes256Gcm::new(&key); 19 | 20 | let mut nonce_bytes = [0u8; 12]; 21 | 22 | OsRng.fill_bytes(&mut nonce_bytes); 23 | let nonce = Nonce::from_slice(&nonce_bytes); 24 | 25 | let mut cipher_text = cipher 26 | .encrypt(nonce, data) 27 | .map_err(|_| "Encryption failed")?; 28 | 29 | cipher_text.extend_from_slice(&nonce_bytes); 30 | 31 | Ok(cipher_text) 32 | } 33 | 34 | pub fn decrypt(data: &[u8]) -> Result, Box> { 35 | dotenv::dotenv().ok(); 36 | 37 | let key_str = std::env::var("SECRET_KEY")?; 38 | let key_raw = key_str.as_bytes(); 39 | 40 | if key_raw.len() != 32 { 41 | return Err("Key must be 32 bytes long for AES-256".into()); 42 | } 43 | 44 | let key = GenericArray::clone_from_slice(key_raw); 45 | let cipher = Aes256Gcm::new(&key); 46 | 47 | let (cipher_text, nonce) = data.split_at(data.len() - 12); 48 | let nonce = Nonce::from_slice(nonce); 49 | let decrypted_data = cipher 50 | .decrypt(nonce, cipher_text) 51 | .map_err(|_| "Decryption failed")?; 52 | 53 | Ok(decrypted_data) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | 60 | #[test] 61 | fn test_encrypt_decrypt() { 62 | let data = b"Hello, world!"; 63 | let encrypted_data = encrypt(data).unwrap(); 64 | 65 | let decrypted_data = decrypt(&encrypted_data).unwrap(); 66 | 67 | assert_eq!(data.to_vec(), decrypted_data); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/components/GoogleSignInButton.tsx: -------------------------------------------------------------------------------- 1 | import { GoogleLogin } from "@react-oauth/google"; 2 | import { useMutation } from "@tanstack/react-query"; 3 | import React from "react"; 4 | import cookie from "js-cookie"; 5 | 6 | import { UserAuthActions } from "@/utils/interactions/dataPosters"; 7 | import useRevalidation from "@/hooks/useRevalidate"; 8 | import { toaster } from "./ui/toaster"; 9 | 10 | const GoogleSignInButton = () => { 11 | const { mutateAsync } = useMutation({ 12 | mutationFn: UserAuthActions.handleSignInWithGoogle, 13 | }); 14 | const revalidate = useRevalidation(); 15 | 16 | function handleLogin(loginId: string) { 17 | toaster.promise(mutateAsync({ id_token: loginId }), { 18 | error(arg: any) { 19 | return { 20 | title: "Error", 21 | description: arg?.message || "Failed to login with google", 22 | }; 23 | }, 24 | success(arg) { 25 | cookie.set("polymarketAuthToken", arg.sessionToken, { 26 | expires: 60 * 60 * 24 * 30, // 30 days, 27 | secure: true, 28 | }); 29 | queueMicrotask(() => revalidate(["userData"])); 30 | window.location.reload(); 31 | 32 | return { 33 | title: "Success", 34 | description: "Welcome to polymarket", 35 | }; 36 | }, 37 | loading: { 38 | title: "Waiting for sign in...", 39 | description: "Please complete your sign in process in popup window", 40 | }, 41 | }); 42 | } 43 | return ( 44 | <> 45 | { 47 | if (!credentialResponse.credential) { 48 | toaster.error({ title: "Failed to get credentials from google" }); 49 | return; 50 | } 51 | handleLogin(credentialResponse.credential); 52 | }} 53 | onError={() => { 54 | console.log("Login Failed"); 55 | toaster.error({ title: "Failed to login with google" }); 56 | }} 57 | logo_alignment="center" 58 | shape="circle" 59 | size="large" 60 | /> 61 | 62 | ); 63 | }; 64 | 65 | export default GoogleSignInButton; 66 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-6a3532430782df1151628ba3890c2de75ec3bf347e71ee9318bfbffabea8a7a3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT id, user_id, market_id, shares, created_at, updated_at, outcome as \"outcome: Outcome\"\n FROM polymarket.user_holdings\n WHERE user_id = $1 AND market_id = $2 AND outcome = $3\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "user_id", 14 | "type_info": "Uuid" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "market_id", 19 | "type_info": "Uuid" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "shares", 24 | "type_info": "Numeric" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "created_at", 29 | "type_info": "Timestamp" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "updated_at", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "outcome: Outcome", 39 | "type_info": { 40 | "Custom": { 41 | "name": "polymarket.outcome", 42 | "kind": { 43 | "Enum": [ 44 | "yes", 45 | "no", 46 | "unspecified" 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | ], 53 | "parameters": { 54 | "Left": [ 55 | "Uuid", 56 | "Uuid", 57 | { 58 | "Custom": { 59 | "name": "polymarket.outcome", 60 | "kind": { 61 | "Enum": [ 62 | "yes", 63 | "no", 64 | "unspecified" 65 | ] 66 | } 67 | } 68 | } 69 | ] 70 | }, 71 | "nullable": [ 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false 79 | ] 80 | }, 81 | "hash": "6a3532430782df1151628ba3890c2de75ec3bf347e71ee9318bfbffabea8a7a3" 82 | } 83 | -------------------------------------------------------------------------------- /app/src/generated/grpc_service_types/price.client.ts: -------------------------------------------------------------------------------- 1 | // @generated by protobuf-ts 2.10.0 with parameter generate_dependencies,long_type_number 2 | // @generated from protobuf file "price.proto" (package "price", syntax proto3) 3 | // tslint:disable 4 | import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; 5 | import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; 6 | import { PriceService } from "./price"; 7 | import { stackIntercept } from "@protobuf-ts/runtime-rpc"; 8 | import type { GetMarketPriceDataWithinIntervalResponse } from "./price"; 9 | import type { GetPriceDataWithinIntervalRequest } from "./price"; 10 | import type { UnaryCall } from "@protobuf-ts/runtime-rpc"; 11 | import type { RpcOptions } from "@protobuf-ts/runtime-rpc"; 12 | /** 13 | * @generated from protobuf service price.PriceService 14 | */ 15 | export interface IPriceServiceClient { 16 | /** 17 | * @generated from protobuf rpc: GetPriceDataWithinInterval(price.GetPriceDataWithinIntervalRequest) returns (price.GetMarketPriceDataWithinIntervalResponse); 18 | */ 19 | getPriceDataWithinInterval(input: GetPriceDataWithinIntervalRequest, options?: RpcOptions): UnaryCall; 20 | } 21 | /** 22 | * @generated from protobuf service price.PriceService 23 | */ 24 | export class PriceServiceClient implements IPriceServiceClient, ServiceInfo { 25 | typeName = PriceService.typeName; 26 | methods = PriceService.methods; 27 | options = PriceService.options; 28 | constructor(private readonly _transport: RpcTransport) { 29 | } 30 | /** 31 | * @generated from protobuf rpc: GetPriceDataWithinInterval(price.GetPriceDataWithinIntervalRequest) returns (price.GetMarketPriceDataWithinIntervalResponse); 32 | */ 33 | getPriceDataWithinInterval(input: GetPriceDataWithinIntervalRequest, options?: RpcOptions): UnaryCall { 34 | const method = this.methods[0], opt = this._transport.mergeOptions(options); 35 | return stackIntercept("unary", this._transport, method, opt, input); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-026c1e9ae0d63ab2a5077c04b3103274c321202383e90e94b3aa07670e2aeaf7.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE \"polymarket\".\"users\" SET\n email = $1,\n name = $2,\n avatar = $3,\n last_login = CURRENT_TIMESTAMP\n WHERE id = $4\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "google_id", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "email", 19 | "type_info": "Varchar" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "name", 24 | "type_info": "Varchar" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "avatar", 29 | "type_info": "Varchar" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "last_login", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "public_key", 39 | "type_info": "Varchar" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "private_key", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "balance", 49 | "type_info": "Numeric" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "created_at", 54 | "type_info": "Timestamp" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "updated_at", 59 | "type_info": "Timestamp" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Varchar", 65 | "Varchar", 66 | "Varchar", 67 | "Uuid" 68 | ] 69 | }, 70 | "nullable": [ 71 | false, 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false, 79 | false, 80 | false, 81 | false 82 | ] 83 | }, 84 | "hash": "026c1e9ae0d63ab2a5077c04b3103274c321202383e90e94b3aa07670e2aeaf7" 85 | } 86 | -------------------------------------------------------------------------------- /app/src/app/market/[id]/_components/TradeForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box, Button, Flex, Text } from "@chakra-ui/react"; 4 | import { useState } from "react"; 5 | 6 | import MarketOrderForm from "./MarketOrderForm"; 7 | import LimitOrderForm from "./LimitOrderForm"; 8 | import { MarketPrice } from "@/generated/grpc_service_types/markets"; 9 | import { formatPriceString } from "@/utils"; 10 | 11 | type Props = { 12 | mode: "buy" | "sell"; 13 | orderType: "market" | "limit"; 14 | market_id: string; 15 | marketPrice: MarketPrice; 16 | }; 17 | 18 | const TradeForm = ({ mode, orderType, market_id, marketPrice }: Props) => { 19 | const [stockMode, setStockMode] = useState<"yes" | "no">("yes"); 20 | const yesPrice = formatPriceString(marketPrice.latestYesPrice); 21 | const noPrice = formatPriceString(marketPrice.latestNoPrice); 22 | 23 | return ( 24 | 25 | 26 | 39 | 52 | 53 | 54 | {/* market / limit order form */} 55 | {orderType === "limit" ? ( 56 | 61 | ) : ( 62 | 67 | )} 68 | 69 | ); 70 | }; 71 | 72 | export default TradeForm; 73 | -------------------------------------------------------------------------------- /service-api/src/routes/login.rs: -------------------------------------------------------------------------------- 1 | use auth_service::types::AuthenticateUserError; 2 | use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::json; 5 | use utility_helpers::log_error; 6 | 7 | use crate::{require_field, state::AppState, utils::types::ReturnType}; 8 | 9 | #[derive(Deserialize, Serialize)] 10 | pub struct LoginRequest { 11 | id_token: Option, 12 | } 13 | 14 | pub async fn oauth_login( 15 | State(app_state): State, 16 | Json(payload): Json, 17 | ) -> Result { 18 | require_field!(payload.id_token); 19 | let id_token = payload.id_token.as_ref().unwrap(); // already verified by require_field! 20 | 21 | let (user_id, session_token, is_new_user) = app_state 22 | .auth_service 23 | .authenticate_user(id_token) 24 | .await 25 | .map_err(|e| { 26 | log_error!("Failed to authenticate user: {:?}", e); 27 | match e { 28 | AuthenticateUserError::InvalidToken => ( 29 | StatusCode::UNAUTHORIZED, 30 | Json(json!({"error": "Invalid token"})).into_response(), 31 | ), 32 | AuthenticateUserError::FailedToInsertUser => ( 33 | StatusCode::INTERNAL_SERVER_ERROR, 34 | Json(json!({"error": "Failed to insert user"})).into_response(), 35 | ), 36 | AuthenticateUserError::FailedToGenerateSessionToken => ( 37 | StatusCode::INTERNAL_SERVER_ERROR, 38 | Json(json!({"error": "Failed to generate session token"})).into_response(), 39 | ), 40 | } 41 | })?; 42 | 43 | // update bloom filter with new user id 44 | app_state.bloom_filter.insert(&user_id); 45 | 46 | Ok(( 47 | StatusCode::OK, 48 | Json(json!({ 49 | "message": if is_new_user { 50 | "User created successfully" 51 | } else { 52 | "User logged in successfully" 53 | }, 54 | "userId": user_id, 55 | "sessionToken": session_token, 56 | "success": true 57 | })) 58 | .into_response(), 59 | )) 60 | } 61 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-3673e672daeb6b4656b07cbe50b07208faa9e1f650f87cd7cb36942ab17a503d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO \"polymarket\".\"users\" (\n google_id,\n email,\n name,\n avatar,\n public_key, \n private_key\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n ) RETURNING * \n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "google_id", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "email", 19 | "type_info": "Varchar" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "name", 24 | "type_info": "Varchar" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "avatar", 29 | "type_info": "Varchar" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "last_login", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "public_key", 39 | "type_info": "Varchar" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "private_key", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "balance", 49 | "type_info": "Numeric" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "created_at", 54 | "type_info": "Timestamp" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "updated_at", 59 | "type_info": "Timestamp" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Varchar", 65 | "Varchar", 66 | "Varchar", 67 | "Varchar", 68 | "Varchar", 69 | "Text" 70 | ] 71 | }, 72 | "nullable": [ 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false, 79 | false, 80 | false, 81 | false, 82 | false, 83 | false 84 | ] 85 | }, 86 | "hash": "3673e672daeb6b4656b07cbe50b07208faa9e1f650f87cd7cb36942ab17a503d" 87 | } 88 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-abb9570ad6d4cc6dd8d0e55ac167c30430643edb5d2aca74f66ac0ac61e29cb9.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO polymarket.user_holdings (user_id, market_id, shares, outcome)\n VALUES ($1, $2, $3, $4)\n RETURNING \n id, \n user_id, \n market_id, \n shares, \n created_at, \n updated_at, \n outcome as \"outcome: Outcome\";\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "user_id", 14 | "type_info": "Uuid" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "market_id", 19 | "type_info": "Uuid" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "shares", 24 | "type_info": "Numeric" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "created_at", 29 | "type_info": "Timestamp" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "updated_at", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "outcome: Outcome", 39 | "type_info": { 40 | "Custom": { 41 | "name": "polymarket.outcome", 42 | "kind": { 43 | "Enum": [ 44 | "yes", 45 | "no", 46 | "unspecified" 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | ], 53 | "parameters": { 54 | "Left": [ 55 | "Uuid", 56 | "Uuid", 57 | "Numeric", 58 | { 59 | "Custom": { 60 | "name": "polymarket.outcome", 61 | "kind": { 62 | "Enum": [ 63 | "yes", 64 | "no", 65 | "unspecified" 66 | ] 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | "nullable": [ 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false, 79 | false 80 | ] 81 | }, 82 | "hash": "abb9570ad6d4cc6dd8d0e55ac167c30430643edb5d2aca74f66ac0ac61e29cb9" 83 | } 84 | -------------------------------------------------------------------------------- /websocket-service/src/nats_handler/mod.rs: -------------------------------------------------------------------------------- 1 | use async_nats::jetstream; 2 | use futures::StreamExt; 3 | use utility_helpers::{ 4 | log_info, 5 | message_pack_helper::deserialize_from_message_pack, 6 | nats_helper::{NatsSubjects, types::OrderBookUpdateData}, 7 | }; 8 | 9 | use crate::{SafeAppState, nats_handler::handle_market_book_update::handle_market_book_update}; 10 | 11 | pub mod handle_market_book_update; 12 | 13 | pub async fn nats_handler(state: SafeAppState) -> Result<(), Box> { 14 | log_info!("NATS handler started for order book service"); 15 | 16 | let stream_guard = state.jetstream.clone(); 17 | 18 | let stream = stream_guard 19 | .get_or_create_stream(jetstream::stream::Config { 20 | name: "ORDER".to_string(), 21 | subjects: vec!["order.>".to_string()], 22 | ..Default::default() 23 | }) 24 | .await?; 25 | 26 | let consumer = stream 27 | .create_consumer(jetstream::consumer::pull::Config { 28 | durable_name: Some("order_ws".to_string()), 29 | ..Default::default() 30 | }) 31 | .await?; 32 | 33 | let mut messages = consumer.messages().await?; 34 | 35 | while let Some(Ok(message)) = messages.next().await { 36 | let subject = message.subject.clone(); 37 | let subject_str = subject.as_str(); 38 | let subject = NatsSubjects::from_string(subject_str) 39 | .ok_or_else(|| format!("Invalid subject: {}", subject))?; 40 | 41 | match subject { 42 | NatsSubjects::MarketBookUpdate(market_id) => { 43 | log_info!("Received market book update for market ID: {}", market_id); 44 | let data_buff = message.payload.to_vec(); 45 | let data = 46 | deserialize_from_message_pack::(&data_buff.as_slice())?; 47 | let market_book_handler_state = state.clone(); 48 | handle_market_book_update(market_book_handler_state, data).await?; 49 | } 50 | _ => { 51 | log_info!("Received message on unsupported subject: {}", subject); 52 | } 53 | } 54 | // Acknowledge the message 55 | message 56 | .ack() 57 | .await 58 | .map_err(|_| "Failed to acknowledge message".to_string())?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /queries/pg_queries/metadata.sql: -------------------------------------------------------------------------------- 1 | WITH 2 | holdings AS ( 3 | SELECT 4 | uh.market_id, 5 | uh.outcome, 6 | uh.shares 7 | FROM polymarket.user_holdings uh 8 | WHERE uh.user_id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid 9 | ), 10 | 11 | orders AS ( 12 | SELECT 13 | COUNT(*) FILTER (WHERE status = 'open') AS open_orders, 14 | COUNT(*) FILTER (WHERE status = 'partial_fill') AS partial_orders, 15 | COUNT(*) AS total_orders, 16 | AVG(filled_quantity / NULLIF(quantity, 0)) AS avg_fill_ratio 17 | FROM polymarket.orders 18 | WHERE user_id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid 19 | ), 20 | 21 | trades AS ( 22 | SELECT 23 | COUNT(*) AS total_trades, 24 | SUM(quantity) AS total_volume, 25 | AVG(price) AS avg_trade_price, 26 | MAX(quantity) AS max_trade_qty, 27 | MIN(created_at) AS first_trade_at, 28 | MAX(created_at) AS last_trade_at, 29 | COUNT(DISTINCT market_id) AS markets_traded 30 | FROM polymarket.user_trades 31 | WHERE user_id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid 32 | ), 33 | 34 | txns AS ( 35 | SELECT 36 | SUM(amount) FILTER (WHERE transaction_type = 'deposit') AS total_deposit, 37 | SUM(amount) FILTER (WHERE transaction_type = 'withdrawal') AS total_withdraw, 38 | MAX(created_at) FILTER (WHERE transaction_type = 'deposit') AS last_deposit, 39 | MAX(created_at) FILTER (WHERE transaction_type = 'withdrawal') AS last_withdraw 40 | FROM polymarket.user_transactions 41 | WHERE user_id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid 42 | ) 43 | 44 | SELECT 45 | u.id, 46 | u.name, 47 | u.email, 48 | u.avatar, 49 | u.public_key, 50 | u.balance, 51 | u.last_login, 52 | u.created_at, 53 | 54 | -- Orders 55 | o.open_orders, 56 | o.partial_orders, 57 | o.total_orders, 58 | o.avg_fill_ratio, 59 | 60 | -- Trades 61 | t.total_trades, 62 | t.total_volume, 63 | t.avg_trade_price, 64 | t.max_trade_qty, 65 | t.first_trade_at, 66 | t.last_trade_at, 67 | t.markets_traded, 68 | 69 | -- Txns 70 | x.total_deposit, 71 | x.total_withdraw, 72 | x.last_deposit, 73 | x.last_withdraw 74 | 75 | FROM polymarket.users u 76 | LEFT JOIN orders o ON true 77 | LEFT JOIN trades t ON true 78 | LEFT JOIN txns x ON true 79 | WHERE u.id = 'cf2f0f54-f66e-4a61-bc85-9a26653e77e9'::uuid; -------------------------------------------------------------------------------- /grpc-service/src/generated/common.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(serde::Serialize, serde::Deserialize)] 3 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 4 | pub struct PageInfo { 5 | #[prost(uint64, tag = "1")] 6 | pub page: u64, 7 | #[prost(uint64, tag = "2")] 8 | pub page_size: u64, 9 | #[prost(uint64, tag = "3")] 10 | pub total_items: u64, 11 | #[prost(uint64, tag = "4")] 12 | pub total_pages: u64, 13 | } 14 | #[derive(serde::Serialize, serde::Deserialize)] 15 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 16 | pub struct PageRequest { 17 | #[prost(uint64, tag = "1")] 18 | pub page: u64, 19 | #[prost(uint64, tag = "2")] 20 | pub page_size: u64, 21 | } 22 | #[derive(serde::Serialize, serde::Deserialize)] 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] 24 | #[repr(i32)] 25 | pub enum Timeframe { 26 | Unspecified = 0, 27 | OneHour = 1, 28 | SixHour = 2, 29 | OneDay = 3, 30 | OneWeek = 4, 31 | OneMonth = 5, 32 | All = 6, 33 | } 34 | impl Timeframe { 35 | /// String value of the enum field names used in the ProtoBuf definition. 36 | /// 37 | /// The values are not transformed in any way and thus are considered stable 38 | /// (if the ProtoBuf definition does not change) and safe for programmatic use. 39 | pub fn as_str_name(&self) -> &'static str { 40 | match self { 41 | Self::Unspecified => "TIMEFRAME_UNSPECIFIED", 42 | Self::OneHour => "TIMEFRAME_ONE_HOUR", 43 | Self::SixHour => "TIMEFRAME_SIX_HOUR", 44 | Self::OneDay => "TIMEFRAME_ONE_DAY", 45 | Self::OneWeek => "TIMEFRAME_ONE_WEEK", 46 | Self::OneMonth => "TIMEFRAME_ONE_MONTH", 47 | Self::All => "TIMEFRAME_ALL", 48 | } 49 | } 50 | /// Creates an enum from field names used in the ProtoBuf definition. 51 | pub fn from_str_name(value: &str) -> ::core::option::Option { 52 | match value { 53 | "TIMEFRAME_UNSPECIFIED" => Some(Self::Unspecified), 54 | "TIMEFRAME_ONE_HOUR" => Some(Self::OneHour), 55 | "TIMEFRAME_SIX_HOUR" => Some(Self::SixHour), 56 | "TIMEFRAME_ONE_DAY" => Some(Self::OneDay), 57 | "TIMEFRAME_ONE_WEEK" => Some(Self::OneWeek), 58 | "TIMEFRAME_ONE_MONTH" => Some(Self::OneMonth), 59 | "TIMEFRAME_ALL" => Some(Self::All), 60 | _ => None, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-de3559807340810a2a40bd545b2a99c3b432643a8a526d0805630f8456b198c7.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO polymarket.user_holdings (user_id, market_id, shares, outcome)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (user_id, market_id, outcome)\n DO UPDATE SET shares = polymarket.user_holdings.shares + $3,\n updated_at = NOW()\n RETURNING \n id, \n user_id, \n market_id, \n shares, \n created_at, \n updated_at, \n outcome as \"outcome: Outcome\";\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "user_id", 14 | "type_info": "Uuid" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "market_id", 19 | "type_info": "Uuid" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "shares", 24 | "type_info": "Numeric" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "created_at", 29 | "type_info": "Timestamp" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "updated_at", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "outcome: Outcome", 39 | "type_info": { 40 | "Custom": { 41 | "name": "polymarket.outcome", 42 | "kind": { 43 | "Enum": [ 44 | "yes", 45 | "no", 46 | "unspecified" 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | ], 53 | "parameters": { 54 | "Left": [ 55 | "Uuid", 56 | "Uuid", 57 | "Numeric", 58 | { 59 | "Custom": { 60 | "name": "polymarket.outcome", 61 | "kind": { 62 | "Enum": [ 63 | "yes", 64 | "no", 65 | "unspecified" 66 | ] 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | "nullable": [ 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false, 79 | false 80 | ] 81 | }, 82 | "hash": "de3559807340810a2a40bd545b2a99c3b432643a8a526d0805630f8456b198c7" 83 | } 84 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-90d29bab84a5b59c50aad52bdc43c81be05a816f93471ee0b17db5e40153c773.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO polymarket.user_holdings (user_id, market_id, shares, outcome)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (user_id, market_id, outcome)\n DO UPDATE SET shares = polymarket.user_holdings.shares + $3,\n updated_at = NOW() \n RETURNING \n id, \n user_id, \n market_id, \n shares, \n created_at, \n updated_at, \n outcome as \"outcome: Outcome\";\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "user_id", 14 | "type_info": "Uuid" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "market_id", 19 | "type_info": "Uuid" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "shares", 24 | "type_info": "Numeric" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "created_at", 29 | "type_info": "Timestamp" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "updated_at", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "outcome: Outcome", 39 | "type_info": { 40 | "Custom": { 41 | "name": "polymarket.outcome", 42 | "kind": { 43 | "Enum": [ 44 | "yes", 45 | "no", 46 | "unspecified" 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | ], 53 | "parameters": { 54 | "Left": [ 55 | "Uuid", 56 | "Uuid", 57 | "Numeric", 58 | { 59 | "Custom": { 60 | "name": "polymarket.outcome", 61 | "kind": { 62 | "Enum": [ 63 | "yes", 64 | "no", 65 | "unspecified" 66 | ] 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | "nullable": [ 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false, 79 | false 80 | ] 81 | }, 82 | "hash": "90d29bab84a5b59c50aad52bdc43c81be05a816f93471ee0b17db5e40153c773" 83 | } 84 | -------------------------------------------------------------------------------- /utility-helpers/src/nats_helper/types.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains types which are going to serialize using message pack pack and send to nats 3 | */ 4 | 5 | use proto_defs::proto_types::order_book::{MarketBook, OrderBook, OrderLevel}; 6 | use rust_decimal::Decimal; 7 | use serde::{Deserialize, Serialize}; 8 | use uuid::Uuid; 9 | 10 | use crate::{ 11 | to_f64, 12 | types::{OrderBookDataStruct, OrderLevel as OrderLevelStruct}, 13 | }; 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | pub struct OrderBookUpdateData { 17 | pub yes_book: OrderBookDataStruct, 18 | pub no_book: OrderBookDataStruct, 19 | pub market_id: Uuid, 20 | pub timestamp: String, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub struct UpdateOrderMessage { 25 | pub order_id: Uuid, 26 | pub new_quantity: Decimal, 27 | pub new_price: Decimal, 28 | } 29 | 30 | #[derive(Debug, Serialize, Deserialize)] 31 | pub struct MarketOrderCreateMessage { 32 | pub order_id: Uuid, 33 | pub budget: Decimal, 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize)] 37 | #[serde(bound( 38 | serialize = "T: Serialize", 39 | deserialize = "T: serde::de::DeserializeOwned" 40 | ))] 41 | pub struct InitializeOrderBookMessage { 42 | pub liquidity_b: Decimal, 43 | pub orders: Vec, 44 | } 45 | 46 | impl OrderBookUpdateData { 47 | pub fn get_prost_market_book(self, market_id: Uuid) -> MarketBook { 48 | let yes_book_bids = Self::get_order_level(&self.yes_book.bids); 49 | 50 | let yes_book_asks = Self::get_order_level(&self.yes_book.asks); 51 | let no_book_bids = Self::get_order_level(&self.no_book.bids); 52 | let no_book_asks = Self::get_order_level(&self.no_book.asks); 53 | 54 | let yes_book = OrderBook { 55 | bids: yes_book_bids, 56 | asks: yes_book_asks, 57 | }; 58 | let no_book = OrderBook { 59 | bids: no_book_bids, 60 | asks: no_book_asks, 61 | }; 62 | 63 | MarketBook { 64 | market_id: market_id.to_string(), 65 | yes_book: Some(yes_book), 66 | no_book: Some(no_book), 67 | } 68 | } 69 | 70 | fn get_order_level(order_level: &Vec) -> Vec { 71 | order_level 72 | .iter() 73 | .map(|level| OrderLevel { 74 | price: to_f64(level.price).unwrap_or_default(), 75 | shares: to_f64(level.shares).unwrap_or_default(), 76 | users: level.users as u32, 77 | }) 78 | .collect() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/utils/types/api.ts: -------------------------------------------------------------------------------- 1 | import { Order, PageInfoServiceAPi } from "."; 2 | 3 | export interface BaseResponse { 4 | message: string; 5 | success: boolean; 6 | } 7 | export interface ErrorResponse { 8 | error: string; 9 | } 10 | 11 | export interface LoginResponse extends BaseResponse { 12 | userId: string; 13 | sessionToken: string; 14 | } 15 | 16 | export interface GetUserResponse { 17 | avatar: string; 18 | balance: number; 19 | email: string; 20 | name: string; 21 | public_key: string; 22 | } 23 | 24 | export interface GetUserOrdersPaginatedResponse { 25 | orders: Order[]; 26 | page: number; 27 | page_size: number; 28 | total_pages: number; 29 | holdings: { 30 | no: string; 31 | yes: string; 32 | }; 33 | } 34 | 35 | export interface GetUserMetadataResponse { 36 | profile_insight: { 37 | avatar: string; 38 | avg_fill_ratio: string; 39 | avg_trade_price: string; 40 | balance: string; 41 | created_at: string; 42 | email: string; 43 | first_trade_at: string; 44 | id: string; 45 | last_deposit: null; 46 | last_login: string; 47 | last_trade_at: string; 48 | last_withdraw: null; 49 | markets_traded: number; 50 | max_trade_qty: string; 51 | name: string; 52 | open_orders: number; 53 | partial_orders: number; 54 | public_key: string; 55 | total_deposit: null; 56 | total_orders: number; 57 | total_trades: number; 58 | total_volume: string; 59 | total_withdraw: null; 60 | }; 61 | user_id: string; 62 | } 63 | 64 | export interface Trade { 65 | market_final_outcome: string; 66 | market_logo: string; 67 | market_name: string; 68 | market_status: string; 69 | trade_outcome: string; 70 | trade_price: string; 71 | trade_quantity: string; 72 | trade_type: string; 73 | } 74 | 75 | export interface GetUserTradesResponse { 76 | data: { 77 | page_info: PageInfoServiceAPi; 78 | trades: Trade[]; 79 | }; 80 | } 81 | 82 | // Holdings interface based on your data structure 83 | interface Holding { 84 | final_outcome: string; 85 | market_created_at: string; 86 | market_description: string; 87 | market_expiry: string; 88 | market_id: string; 89 | market_logo: string; 90 | market_name: string; 91 | market_status: string; 92 | market_updated_at: string; 93 | outcome: string; 94 | shares: string; 95 | } 96 | 97 | export interface GetUserHoldingsResponse { 98 | data: { 99 | holdings: Holding[]; 100 | page_info: PageInfoServiceAPi; 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /service-api/src/routes/user/orders/get_all_users_orders.rs: -------------------------------------------------------------------------------- 1 | use auth_service::types::SessionTokenClaims; 2 | use axum::{ 3 | Extension, Json, 4 | extract::{Query, State}, 5 | http::StatusCode, 6 | response::{IntoResponse, Response}, 7 | }; 8 | use db_service::schema::{enums::OrderStatus, orders::Order}; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_json::json; 11 | use utility_helpers::log_error; 12 | 13 | use crate::{require_field, state::AppState, validate_paginated_fields}; 14 | 15 | #[derive(Deserialize, Serialize, Debug)] 16 | pub struct QueryParams { 17 | page: Option, 18 | page_size: Option, 19 | status: Option, // Optional field to filter by order status 20 | } 21 | 22 | pub async fn get_all_users_orders( 23 | State(app_state): State, 24 | Query(params): Query, 25 | Extension(claims): Extension, 26 | ) -> Result { 27 | let user_id = claims.user_id; 28 | let status = params.status.as_deref().unwrap_or("open"); 29 | 30 | require_field!(params.page); 31 | require_field!(params.page_size); 32 | 33 | let page = params.page.unwrap(); 34 | let page_size = params.page_size.unwrap(); 35 | let order_status = match status.to_lowercase().as_str() { 36 | "open" => OrderStatus::OPEN, 37 | "cancelled" => OrderStatus::CANCELLED, 38 | "filled" => OrderStatus::FILLED, 39 | "expired" => OrderStatus::EXPIRED, 40 | "pending_update" => OrderStatus::PendingUpdate, 41 | "pending_cancel" => OrderStatus::PendingCancel, 42 | _ => { 43 | return Err(( 44 | StatusCode::BAD_REQUEST, 45 | Json(json!({"message": "Invalid order status"})).into_response(), 46 | )); 47 | } 48 | }; 49 | 50 | validate_paginated_fields!(page, page_size); 51 | 52 | let (user_orders, total_page) = Order::get_user_orders_by_paginated( 53 | &app_state.pg_pool, 54 | user_id, 55 | order_status, 56 | page, 57 | page_size, 58 | ) 59 | .await 60 | .map_err(|e| { 61 | log_error!("Failed to fetch user orders {e:?}"); 62 | ( 63 | StatusCode::INTERNAL_SERVER_ERROR, 64 | Json(json!({"message": "Failed to fetch user orders"})).into_response(), 65 | ) 66 | })?; 67 | 68 | Ok(Json(json!({ 69 | "orders": user_orders, 70 | "page": page, 71 | "page_size": page_size, 72 | "total_pages": total_page, 73 | })) 74 | .into_response()) 75 | } 76 | -------------------------------------------------------------------------------- /utility-helpers/src/ws/types.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] 7 | #[serde(rename_all = "snake_case")] 8 | pub enum ChannelType { 9 | PriceUpdate(Uuid), 10 | PricePoster, 11 | OrderBookUpdate(Uuid), 12 | OrderBookPoster, 13 | } 14 | 15 | impl ChannelType { 16 | pub fn from_str(s: &str) -> Option { 17 | if s.starts_with("price_update:") { 18 | let uuid_str = s.strip_prefix("price_update:"); 19 | let uuid = Self::get_uuid_from_str(uuid_str); 20 | return match uuid { 21 | Some(uuid) => Some(ChannelType::PriceUpdate(uuid)), 22 | _ => None, 23 | }; 24 | } else if s.starts_with("order_book_update:") { 25 | let uuid_str = s.strip_prefix("order_book_update:"); 26 | let uuid = Self::get_uuid_from_str(uuid_str); 27 | return match uuid { 28 | Some(uuid) => Some(ChannelType::OrderBookUpdate(uuid)), 29 | _ => None, 30 | }; 31 | } else if s.starts_with("price_poster") { 32 | return Some(ChannelType::PricePoster); 33 | } else if s.starts_with("order_book_poster") { 34 | return Some(ChannelType::OrderBookPoster); 35 | } 36 | None 37 | } 38 | 39 | pub fn to_str(&self) -> String { 40 | match self { 41 | ChannelType::PriceUpdate(uuid) => format!("price_update:{uuid}"), 42 | ChannelType::OrderBookUpdate(uuid) => format!("order_book_update:{uuid}"), 43 | ChannelType::PricePoster => "price_poster".to_string(), 44 | ChannelType::OrderBookPoster => "order_book_poster".to_string(), 45 | } 46 | } 47 | 48 | fn get_uuid_from_str(st: Option<&str>) -> Option { 49 | if let Some(uuid_str) = st { 50 | let uuid = Uuid::from_str(uuid_str); 51 | return match uuid { 52 | Ok(uuid) => Some(uuid), 53 | _ => None, 54 | }; 55 | } 56 | None 57 | } 58 | } 59 | 60 | #[derive(Debug, Clone, Serialize, Deserialize)] 61 | #[serde(tag = "type", content = "data")] 62 | pub enum MessagePayload { 63 | Subscribe { channel: String }, 64 | Unsubscribe { channel: String }, 65 | } 66 | 67 | #[derive(Debug, Clone, Serialize, Deserialize)] 68 | pub struct ClientMessage { 69 | pub id: Option, //TODO we can verify the client id with this id (TODO for now) 70 | pub payload: MessagePayload, 71 | } 72 | -------------------------------------------------------------------------------- /db-service/.sqlx/query-7fe1cc922d607079adede58de4d4a9a08ee0b9cb04d5c525c5bc870935e65024.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO \"polymarket\".\"users\" (\n google_id,\n email,\n name,\n avatar,\n public_key, \n private_key,\n balance\n ) VALUES (\n $1, $2, $3, $4, 'no_puk', 'no_prk', $5\n ) ON CONFLICT (google_id) DO UPDATE SET\n email = EXCLUDED.email,\n name = EXCLUDED.name,\n avatar = EXCLUDED.avatar,\n last_login = CURRENT_TIMESTAMP,\n balance = EXCLUDED.balance\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "google_id", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "email", 19 | "type_info": "Varchar" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "name", 24 | "type_info": "Varchar" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "avatar", 29 | "type_info": "Varchar" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "last_login", 34 | "type_info": "Timestamp" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "public_key", 39 | "type_info": "Varchar" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "private_key", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "balance", 49 | "type_info": "Numeric" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "created_at", 54 | "type_info": "Timestamp" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "updated_at", 59 | "type_info": "Timestamp" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Varchar", 65 | "Varchar", 66 | "Varchar", 67 | "Varchar", 68 | "Numeric" 69 | ] 70 | }, 71 | "nullable": [ 72 | false, 73 | false, 74 | false, 75 | false, 76 | false, 77 | false, 78 | false, 79 | false, 80 | false, 81 | false, 82 | false 83 | ] 84 | }, 85 | "hash": "7fe1cc922d607079adede58de4d4a9a08ee0b9cb04d5c525c5bc870935e65024" 86 | } 87 | -------------------------------------------------------------------------------- /service-api/src/routes/user/orders/cancel_order.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | Json, 3 | extract::{Path, State}, 4 | http::StatusCode, 5 | response::{IntoResponse, Response}, 6 | }; 7 | use db_service::schema::{enums::OrderStatus, orders::Order}; 8 | use serde_json::json; 9 | use utility_helpers::{log_error, nats_helper::NatsSubjects}; 10 | use uuid::Uuid; 11 | 12 | use crate::state::AppState; 13 | 14 | pub async fn cancel_order( 15 | Path(id): Path, 16 | State(app_state): State, 17 | ) -> Result { 18 | let order = Order::find_order_by_id_and_status(id, OrderStatus::OPEN, &app_state.pg_pool) 19 | .await 20 | .map_err(|e| { 21 | ( 22 | StatusCode::INTERNAL_SERVER_ERROR, 23 | Json(json!({ 24 | "error": format!("Failed to find order: {}", e) 25 | })) 26 | .into_response(), 27 | ) 28 | })?; 29 | 30 | if order.is_none() { 31 | return Err(( 32 | StatusCode::NOT_FOUND, 33 | Json(json!({ 34 | "error": "Order not found, or it is not open." 35 | })) 36 | .into_response(), 37 | )); 38 | } 39 | 40 | Order::update_order_status(id, OrderStatus::PendingCancel, &app_state.pg_pool) 41 | .await 42 | .map_err(|e| { 43 | ( 44 | StatusCode::INTERNAL_SERVER_ERROR, 45 | Json(json!({ 46 | "error": format!("Failed to update order status: {}", e) 47 | })) 48 | .into_response(), 49 | ) 50 | })?; 51 | 52 | // assertion is not needed, as it's already checked while creating the order 53 | 54 | // publishing the order to the delete order queue 55 | let order_id_str = id.to_string().into_bytes(); 56 | let subject = NatsSubjects::OrderCancel; 57 | 58 | app_state 59 | .jetstream 60 | .publish(subject.to_string(), order_id_str.into()) 61 | .await 62 | .map_err(|e| { 63 | log_error!("Failed to publish order to jetstream - {:?}", e); 64 | ( 65 | StatusCode::INTERNAL_SERVER_ERROR, 66 | Json(json!({ 67 | "error": "Failed to publish order to jetstream, order will be deleted" 68 | })) 69 | .into_response(), 70 | ) 71 | })?; 72 | 73 | Ok(Json(json!({ 74 | "message": "Order cancellation request sent successfully", 75 | "success": true, 76 | }))) 77 | } 78 | -------------------------------------------------------------------------------- /order-service/src/handlers/ws_handler/handle_text_messages.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use utility_helpers::{ 4 | log_info, log_warn, 5 | ws::types::{ChannelType, ClientMessage, MessagePayload}, 6 | }; 7 | 8 | use crate::state::AppState; 9 | 10 | pub(super) async fn handle_text_messages(state: &Arc, message: &ClientMessage) { 11 | match &message.payload { 12 | MessagePayload::Subscribe { channel } => { 13 | let channel = ChannelType::from_str(&channel); 14 | if let Some(channel_type) = channel { 15 | match channel_type { 16 | ChannelType::OrderBookUpdate(market_id) => { 17 | /* 18 | Example payload 19 | { 20 | "payload": { 21 | "type": "Subscribe", 22 | "data": { 23 | "channel": "order_book_update:" 24 | } 25 | } 26 | } 27 | */ 28 | log_info!("Subscribing to order book updates for market: {market_id}"); 29 | { 30 | let mut market_subs = state.market_subs.write(); 31 | market_subs.insert(market_id); 32 | } 33 | 34 | log_info!("Subscribed to order book updates for market: {market_id}"); 35 | } 36 | _ => { 37 | log_warn!( 38 | "Unsupported channel type for subscription: {:?}", 39 | channel_type 40 | ); 41 | } 42 | } 43 | } 44 | } 45 | MessagePayload::Unsubscribe { channel } => { 46 | let channel = ChannelType::from_str(&channel); 47 | if let Some(channel_type) = channel { 48 | match channel_type { 49 | ChannelType::OrderBookUpdate(market_id) => { 50 | log_info!("Unsubscribing from order book updates for market: {market_id}"); 51 | let mut market_subs = state.market_subs.write(); 52 | market_subs.remove(&market_id); 53 | } 54 | _ => { 55 | log_warn!("Unsupported channel type for unsubscription: {channel_type:?}"); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | --------------------------------------------------------------------------------