> {
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 |
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 |
--------------------------------------------------------------------------------