├── backend
├── .env.dev
├── handlers
│ ├── stats
│ │ └── statshandler_test.go
│ ├── users
│ │ ├── userhelpers
│ │ │ ├── encodeemail.go
│ │ │ └── generateapikey.go
│ │ ├── publicuser.go
│ │ ├── listusers.go
│ │ ├── publicuser
│ │ │ ├── publicuser.go
│ │ │ └── publicuser_test.go
│ │ ├── README_USERS.md
│ │ ├── userpositiononmarkethandler.go
│ │ ├── apply_transaction.go
│ │ ├── credit
│ │ │ ├── usercredit.go
│ │ │ └── usercredit_test.go
│ │ ├── apply_transaction_test.go
│ │ ├── changedescription.go
│ │ ├── changedisplayname.go
│ │ ├── changeemoji.go
│ │ └── privateuser
│ │ │ └── privateuser_test.go
│ ├── markets
│ │ ├── getmarkets.go
│ │ └── leaderboard.go
│ ├── bets
│ │ ├── selling
│ │ │ ├── sellingdust.go
│ │ │ └── sellpositionhandler.go
│ │ ├── errors.go
│ │ └── betutils
│ │ │ ├── betutils.go
│ │ │ ├── validatebet.go
│ │ │ ├── feeutils.go
│ │ │ └── betutils_test.go
│ ├── math
│ │ ├── probabilities
│ │ │ └── wpam
│ │ │ │ ├── wpam_current.go
│ │ │ │ └── wpam_current_test.go
│ │ ├── README_MATH.md
│ │ ├── financials
│ │ │ └── workprofits.go
│ │ ├── market
│ │ │ ├── marketvolume.go
│ │ │ └── marketvolume_test.go
│ │ ├── positions
│ │ │ ├── earliest_users.go
│ │ │ └── valuation.go
│ │ └── payout
│ │ │ └── resolvemarketcore.go
│ ├── home.go
│ ├── tradingdata
│ │ ├── getbets.go
│ │ └── getbets_test.go
│ ├── metrics
│ │ ├── getgloballeaderboard.go
│ │ ├── getsystemmetrics.go
│ │ └── getsystemmetrics_test.go
│ ├── cms
│ │ └── homepage
│ │ │ └── repo.go
│ ├── positions
│ │ └── positionshandler.go
│ └── setup
│ │ └── setuphandler.go
├── seed
│ └── home_embed.go
├── util
│ ├── apikeys.go
│ ├── getenv.go
│ ├── modelchecks.go
│ └── postgres.go
├── models
│ ├── README.md
│ ├── homepage.go
│ ├── modelstesting
│ │ ├── economics_helpers.go
│ │ ├── modelstesting_test.go
│ │ ├── bets_helpers.go
│ │ ├── modelstesting.go
│ │ └── users_helpers.go
│ ├── bets.go
│ ├── market.go
│ └── user.go
├── errors
│ ├── normalerror.go
│ ├── httperror.go
│ └── httperror_test.go
├── setup
│ ├── setup.yaml
│ ├── setuptesting
│ │ ├── setuptesting_test.go
│ │ └── setuptesting.go
│ └── setup_test.go
├── logging
│ ├── mocklogging.go
│ └── loggingutils.go
├── middleware
│ ├── authutils.go
│ └── authadmin.go
├── migration
│ └── migrations
│ │ ├── 20251013_080000_core_models_test.go
│ │ ├── 20251013_080000_core_models.go
│ │ └── 20251020_140500_add_market_labels.go
├── main.go
└── go.mod
├── frontend
├── src
│ ├── App.css
│ ├── api
│ │ ├── axios.js
│ │ └── marketsApi.js
│ ├── components
│ │ ├── layouts
│ │ │ ├── admin
│ │ │ │ └── AdminUtils.jsx
│ │ │ └── profile
│ │ │ │ └── ProfileUtility.jsx
│ │ ├── stats
│ │ │ └── Stats.jsx
│ │ ├── register
│ │ │ └── Register.jsx
│ │ ├── utils
│ │ │ ├── Utils.jsx
│ │ │ ├── graphicalTools
│ │ │ │ ├── GraphDateTimeTools.jsx
│ │ │ │ └── GraphToolTips.jsx
│ │ │ ├── userFinanceTools
│ │ │ │ └── FetchUserCredit.jsx
│ │ │ ├── ExpandableLink.jsx
│ │ │ ├── ExpandableText.jsx
│ │ │ └── dateTimeTools
│ │ │ │ └── FormDateTimeTools.jsx
│ │ ├── footer
│ │ │ └── Footer.jsx
│ │ ├── loaders
│ │ │ └── LoadingSpinner.jsx
│ │ ├── logo
│ │ │ └── Logo.jsx
│ │ ├── inputs
│ │ │ └── InputBox.jsx
│ │ ├── profileEdit
│ │ │ └── ProfileEdit.jsx
│ │ ├── buttons
│ │ │ ├── profile
│ │ │ │ ├── ProfileButtons.jsx
│ │ │ │ ├── ProfileModal.jsx
│ │ │ │ ├── DescriptionSelector.jsx
│ │ │ │ └── DisplayNameSelector.jsx
│ │ │ ├── BaseButton.jsx
│ │ │ ├── marketDetails
│ │ │ │ ├── DescriptionButton.jsx
│ │ │ │ └── ActivityButtons.jsx
│ │ │ └── SiteButtons.jsx
│ │ ├── tabs
│ │ │ ├── ActivityTabs.jsx
│ │ │ └── TradeTabs.jsx
│ │ ├── TradeCTA.jsx
│ │ ├── modals
│ │ │ ├── resolution
│ │ │ │ └── ResolveUtils.jsx
│ │ │ ├── bet
│ │ │ │ ├── BetUtils.jsx
│ │ │ │ └── BetModal.jsx
│ │ │ └── login
│ │ │ │ └── LoginModalClick.jsx
│ │ ├── datetimeSelector
│ │ │ └── DatetimeSelector.jsx
│ │ └── resolutions
│ │ │ └── ResolutionAlert.jsx
│ ├── config.js.template
│ ├── assets
│ │ ├── png
│ │ │ └── HomePageLogo.png
│ │ ├── logo
│ │ │ ├── socialpredictlogo.png
│ │ │ └── socialpredictlogo.webp
│ │ └── svg
│ │ │ ├── create.svg
│ │ │ ├── about.svg
│ │ │ ├── profile.svg
│ │ │ ├── menu-grow.svg
│ │ │ ├── menu-shrink.svg
│ │ │ ├── notifications.svg
│ │ │ ├── home.svg
│ │ │ ├── login.svg
│ │ │ ├── logout.svg
│ │ │ ├── lock-password.svg
│ │ │ ├── markets.svg
│ │ │ ├── polls.svg
│ │ │ ├── api-key.svg
│ │ │ ├── coins.svg
│ │ │ └── admin-gear.svg
│ ├── config.js
│ ├── main.jsx
│ ├── helpers
│ │ └── formatResolutionDate.js
│ ├── pages
│ │ ├── polls
│ │ │ └── Polls.jsx
│ │ ├── notifications
│ │ │ └── Notifications.jsx
│ │ ├── changepassword
│ │ │ └── ChangePassword.jsx
│ │ ├── notfound
│ │ │ └── NotFound.jsx
│ │ ├── admin
│ │ │ └── AdminDashboard.jsx
│ │ ├── marketDetails
│ │ │ ├── MarketDetails.jsx
│ │ │ └── marketDetailsUtils.jsx
│ │ └── home
│ │ │ └── Home.jsx
│ ├── utils
│ │ ├── statusMap.js
│ │ └── chartFormatter.js
│ ├── hooks
│ │ ├── usePortfolio.jsx
│ │ ├── useMarketLabels.js
│ │ ├── useMarketDetails.jsx
│ │ └── useUserData.jsx
│ ├── App.css.backup
│ ├── App.jsx
│ └── index.css
├── index.css
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── HomePageLogo.png
│ ├── env-config.js.template
│ └── manifest.json
├── postcss.config.js
├── .gitignore
├── vite.config.mjs
├── README.md
├── index.html
├── package.json
├── tailwind.config.js
└── README_SPA_ROUTING_FIX.md
├── data
├── nginx
│ ├── conf
│ │ └── nginx.conf
│ └── vhosts
│ │ ├── dev
│ │ └── default.conf.template
│ │ └── prod
│ │ └── default.conf.template
└── traefik
│ └── config
│ └── traefik.template
├── README
├── MATH
│ ├── Prediction_Market_Math.pdf
│ ├── Positions_Calculations.xlsx
│ └── README-MATH-POSITIONS.md
├── IMG
│ └── logotype_kenyon-purple_rgb.png
├── DEVELOPMENT
│ └── DEVELOPMENT.md
├── TROUBLESHOOTING.md
├── CHECKPOINTS
│ ├── CHECKPOINT20251105-01.md
│ ├── CHECKPOINT20250724-01.md
│ └── CHECKPOINT20250803-04.md
├── README-CONFIG.md
├── TESTING
│ └── stress_testing
│ │ └── docker_stats.csv
└── PROJECT_MANAGEMENT
│ └── README-ROADMAP.md
├── docker
├── backend
│ ├── entrypoint.dev.sh
│ ├── Dockerfile.dev
│ └── Dockerfile
└── frontend
│ ├── Dockerfile.dev
│ ├── entrypoint.dev.sh
│ ├── entrypoint.sh
│ ├── Dockerfile
│ └── nginx.conf
├── .github
├── go-versions
├── workflows
│ ├── deploy-to-staging.yml
│ └── deploy-to-production.yml
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── scripts
├── lighthouse
│ └── budget.json
├── prod
│ └── build_prod.sh
├── prod.sh
├── go.mod
├── lib
│ └── arch.sh
├── dev
│ └── env_writer_dev.sh
├── docker-compose-dev.yaml
└── seed_markets_go.sh
├── SECURITY.md
├── LICENSE
├── .gitignore
├── .env.example
└── CONTRIBUTING.md
/backend/.env.dev:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/api/axios.js:
--------------------------------------------------------------------------------
1 | // TBD
2 |
--------------------------------------------------------------------------------
/frontend/src/components/layouts/admin/AdminUtils.jsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/handlers/stats/statshandler_test.go:
--------------------------------------------------------------------------------
1 | package statshandlers
2 |
--------------------------------------------------------------------------------
/backend/handlers/users/userhelpers/encodeemail.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
--------------------------------------------------------------------------------
/backend/handlers/users/userhelpers/generateapikey.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
--------------------------------------------------------------------------------
/data/nginx/conf/nginx.conf:
--------------------------------------------------------------------------------
1 | events {
2 | worker_connections 1024;
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/config.js.template:
--------------------------------------------------------------------------------
1 | export const DOMAIN_URL = $API_DOMAIN;
2 | export const API_URL = $API_DOMAIN;
3 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/HomePageLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/public/HomePageLogo.png
--------------------------------------------------------------------------------
/README/MATH/Prediction_Market_Math.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/README/MATH/Prediction_Market_Math.pdf
--------------------------------------------------------------------------------
/backend/handlers/markets/getmarkets.go:
--------------------------------------------------------------------------------
1 | package marketshandlers
2 |
3 | type PublicMarket struct {
4 | MarketID int64 `json:"marketId"`
5 | }
6 |
--------------------------------------------------------------------------------
/README/IMG/logotype_kenyon-purple_rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/README/IMG/logotype_kenyon-purple_rgb.png
--------------------------------------------------------------------------------
/README/MATH/Positions_Calculations.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/README/MATH/Positions_Calculations.xlsx
--------------------------------------------------------------------------------
/docker/backend/entrypoint.dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ ! -f /src/.air.toml ]; then
4 | touch /src/.air.toml
5 | fi
6 |
7 | exec air -c .air.toml
8 |
--------------------------------------------------------------------------------
/frontend/src/assets/png/HomePageLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/src/assets/png/HomePageLogo.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo/socialpredictlogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/src/assets/logo/socialpredictlogo.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo/socialpredictlogo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openpredictionmarkets/socialpredict/HEAD/frontend/src/assets/logo/socialpredictlogo.webp
--------------------------------------------------------------------------------
/frontend/public/env-config.js.template:
--------------------------------------------------------------------------------
1 | window.__ENV__ = {
2 | DOMAIN_URL: "${DOMAIN_URL:-http://localhost}",
3 | API_URL: "${API_URL:-http://localhost:8080}"
4 | };
5 |
--------------------------------------------------------------------------------
/frontend/src/components/stats/Stats.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | function Stats() {
4 | return "Available Polls"
5 | }
6 |
7 | export default Stats;
--------------------------------------------------------------------------------
/backend/handlers/bets/selling/sellingdust.go:
--------------------------------------------------------------------------------
1 | package sellbetshandlers
2 |
3 | func computeSellingDust() {
4 | // dust := redeemRequest.Amount - actualSaleValue // remainder not paid out
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/components/register/Register.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | function Register() {
4 | return "Register Page"
5 | }
6 |
7 | export default Register;
--------------------------------------------------------------------------------
/backend/handlers/math/probabilities/wpam/wpam_current.go:
--------------------------------------------------------------------------------
1 | package wpam
2 |
3 | func GetCurrentProbability(probChanges []ProbabilityChange) float64 {
4 |
5 | return probChanges[len(probChanges)-1].Probability
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/Utils.jsx:
--------------------------------------------------------------------------------
1 | const MenuItems = ({ isLoggedIn, onLogout }) => {
2 | const handleLogoutClick = () => {
3 | if (onLogout) {
4 | onLogout(); // Call the onLogout function
5 | }
6 | };
7 | }
--------------------------------------------------------------------------------
/data/nginx/vhosts/dev/default.conf.template:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | location /api/ {
5 | proxy_pass http://backend:8080/;
6 | }
7 |
8 | location / {
9 | proxy_pass http://frontend:5173/;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = () => {
4 | return (
5 |
7 | );
8 | };
9 |
10 | export default Footer;
11 |
--------------------------------------------------------------------------------
/backend/seed/home_embed.go:
--------------------------------------------------------------------------------
1 | package seed
2 |
3 | import _ "embed"
4 |
5 | // Embed the homepage markdown content at compile time
6 | // This ensures the content is available regardless of filesystem layout
7 | //
8 | //go:embed home.md
9 | var defaultHomeMD []byte
10 |
--------------------------------------------------------------------------------
/frontend/src/components/loaders/LoadingSpinner.jsx:
--------------------------------------------------------------------------------
1 | const LoadingSpinner = () => (
2 |
5 | );
6 |
7 | export default LoadingSpinner;
8 |
--------------------------------------------------------------------------------
/README/DEVELOPMENT/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | ### Recommended Development Setup
2 |
3 | #### IDE
4 |
5 | * We recommend using VSCODE.
6 | * Plugins:
7 |
8 | * [Go for VS Code](https://code.visualstudio.com/docs/languages/go)
9 |
10 | ### Checkint Local Development Documentation
11 |
12 | * [LOCAL_SETUP](README/LOCAL_SETUP.md)
--------------------------------------------------------------------------------
/docker/backend/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM golang:1.25.1-alpine3.22
2 |
3 | WORKDIR /src
4 |
5 | RUN go install github.com/air-verse/air@latest
6 |
7 | COPY backend/go.mod backend/go.sum ./
8 |
9 | RUN go mod download
10 |
11 | COPY docker/backend/entrypoint.dev.sh /entrypoint.sh
12 |
13 | ENTRYPOINT ["/entrypoint.sh"]
14 |
--------------------------------------------------------------------------------
/docker/frontend/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:22.20.0-alpine3.22 AS builder
2 |
3 | WORKDIR /app
4 |
5 | COPY frontend/package.json frontend/package-lock.json ./
6 |
7 | RUN npm install
8 |
9 | COPY frontend/ .
10 |
11 | COPY docker/frontend/entrypoint.dev.sh /entrypoint.sh
12 |
13 | ENTRYPOINT ["/entrypoint.sh"]
14 |
--------------------------------------------------------------------------------
/frontend/src/config.js:
--------------------------------------------------------------------------------
1 | export const DOMAIN_URL = (typeof window !== 'undefined' && window.__ENV__ && window.__ENV__.DOMAIN_URL) ? window.__ENV__.DOMAIN_URL : 'http://localhost';
2 | export const API_URL = (typeof window !== 'undefined' && window.__ENV__ && window.__ENV__.API_URL) ? window.__ENV__.API_URL : 'http://localhost';
3 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 | import './index.css';
5 |
6 | const root = ReactDOM.createRoot(document.getElementById('root'));
7 | root.render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/backend/handlers/math/README_MATH.md:
--------------------------------------------------------------------------------
1 | ### Conventions
2 |
3 | #### Convention 20250619 63F24852-26DA-4816-B494-AB41F622C38E
4 |
5 | * All math should be moved to the math directory, each area separated by package.
6 | * Handlers should become purely ways to interact with the server and API and should contain little to no budiness logic.
--------------------------------------------------------------------------------
/frontend/src/helpers/formatResolutionDate.js:
--------------------------------------------------------------------------------
1 | const formatResolutionDate = (resolutionDateTime) => {
2 | const now = new Date();
3 | const resolutionDate = new Date(resolutionDateTime);
4 |
5 | return resolutionDate < now ? 'Closed' : resolutionDate.toLocaleDateString();
6 | };
7 |
8 | export default formatResolutionDate;
9 |
--------------------------------------------------------------------------------
/.github/go-versions:
--------------------------------------------------------------------------------
1 | # One entry per line. Two formats supported:
2 | # file: -> reads the language version from that go.mod
3 | # -> exact or x-wildcard like 1.26.x
4 | #
5 | # Default (if this file is missing): file:backend/go.mod
6 |
7 | file:backend/go.mod
8 | #1.26.x
9 | #1.25.x
10 |
--------------------------------------------------------------------------------
/backend/util/apikeys.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "github.com/google/uuid"
5 | "gorm.io/gorm"
6 | )
7 |
8 | func GenerateUniqueApiKey(db *gorm.DB) string {
9 | for {
10 | apiKey := uuid.NewString()
11 | if count := CountByField(db, "api_key", apiKey); count == 0 {
12 | return apiKey
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/lighthouse/budget.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "path": "/*",
4 | "resourceSizes": [
5 | {
6 | "resourceType": "document",
7 | "budget": 18
8 | },
9 | {
10 | "resourceType": "total",
11 | "budget": 200
12 | }
13 | ]
14 | }
15 | ]
--------------------------------------------------------------------------------
/frontend/src/components/utils/graphicalTools/GraphDateTimeTools.jsx:
--------------------------------------------------------------------------------
1 | const FormatDateForAxis = (unixTime) => {
2 | const date = new Date(unixTime);
3 | const day = date.getDate();
4 | const month = date.toLocaleString('en-US', { month: 'short' }); // 'short' gives the three-letter abbreviation
5 | return `${day}-${month.toUpperCase()}`; // Formats the date as DD-MMM
6 | };
7 |
8 | export default FormatDateForAxis;
--------------------------------------------------------------------------------
/frontend/src/assets/svg/create.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/about.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/profile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/models/README.md:
--------------------------------------------------------------------------------
1 | ### Conventions
2 |
3 | #### Convention 20250619 7652196C-216A-4CE5-9119-BB59169A767F
4 |
5 | * Any function used to simply extract information from a model should be moved down to the models directory and package.
6 | * For example a function used to simply extract PublicUser information should sit in models within the users directory.
7 | * If there are model specific packages for example usermodel then it should sit there.
--------------------------------------------------------------------------------
/frontend/src/assets/svg/menu-grow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/menu-shrink.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/nginx/vhosts/prod/default.conf.template:
--------------------------------------------------------------------------------
1 | server {
2 | gzip on;
3 | gzip_min_length 1000;
4 | gunzip on;
5 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
6 |
7 | listen 80;
8 | server_name ${DOMAIN};
9 |
10 | location /api/ {
11 | proxy_pass http://backend:8080/;
12 | }
13 |
14 | location / {
15 | proxy_pass http://frontend:80;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/notifications.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/login.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/logout.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/pages/polls/Polls.jsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../../config';
2 | import React from 'react';
3 | import '../../App.css';
4 |
5 | function Polls() {
6 | return (
7 |
8 |
9 |
Polls
10 |
11 |
Coming Soon...
12 |
13 | );
14 | }
15 |
16 | export default Polls;
17 |
--------------------------------------------------------------------------------
/backend/errors/normalerror.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import (
4 | "log"
5 | )
6 |
7 | // ErrorLogger logs an error and returns a boolean indicating whether an error occurred.
8 | func ErrorLogger(err error, errMsg string) bool {
9 | if err != nil {
10 | log.Printf("Error: %s - %s\n", errMsg, err) // Combine your custom message with the error's message.
11 | return true // Indicate that an error was handled.
12 | }
13 | return false // No error to handle.
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/lock-password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/prod/build_prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Make sure the script can only be run via SocialPredict Script
4 | [ -z "$CALLED_FROM_SOCIALPREDICT" ] && { echo "Not called from SocialPredict"; exit 42; }
5 |
6 | # Pull images
7 | echo "Pulling images ..."
8 | $COMPOSE --env-file "$SCRIPT_DIR"/.env --file "$SCRIPT_DIR/scripts/docker-compose-prod.yaml" pull
9 | echo
10 |
11 | echo "Images pulled."
12 | echo
13 | echo "Your admin credentials are:"
14 | echo "Username: admin"
15 | echo "Password: $ADMIN_PASS"
16 |
--------------------------------------------------------------------------------
/backend/handlers/home.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | func HomeHandler(w http.ResponseWriter, r *http.Request) {
9 | if r.Method != http.MethodGet {
10 | http.Error(w, "Method is not supported.", http.StatusNotFound)
11 | return
12 | }
13 |
14 | // Set the Content-Type header to indicate a JSON response
15 | w.Header().Set("Content-Type", "application/json")
16 |
17 | // Send a JSON-formatted response
18 | fmt.Fprint(w, `{"message": "Data From the Backend!"}`)
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/pages/notifications/Notifications.jsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../../config';
2 | import React from 'react';
3 | import '../../App.css';
4 |
5 | function Notifications() {
6 | return (
7 |
8 |
9 |
Notifications
10 |
11 |
Coming soon...
12 |
13 | );
14 | }
15 |
16 | export default Notifications;
--------------------------------------------------------------------------------
/docker/frontend/entrypoint.dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | TEMPLATE="/app/public/env-config.js.template"
5 | TARGET="/app/public/env-config.js"
6 |
7 | # Defaults if not set
8 | : "${DOMAIN_URL:=http://localhost}"
9 | : "${API_URL:=http://localhost:8080}"
10 |
11 | if [ -f "$TEMPLATE" ]; then
12 | cp "$TEMPLATE" "$TARGET"
13 |
14 | sed -i "s|\${DOMAIN_URL:-http://localhost}|${DOMAIN_URL}|g" "$TARGET"
15 | sed -i "s|\${API_URL:-http://localhost:8080}|${DOMAIN_URL}:${BACKEND_PORT}|g" "$TARGET"
16 | fi
17 |
18 | exec npm run start
19 |
--------------------------------------------------------------------------------
/docker/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.25.1-alpine3.22 AS builder
2 |
3 | WORKDIR /src
4 | COPY backend/go.mod backend/go.sum ./
5 | RUN go mod download
6 | COPY backend/ .
7 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /app/server .
8 |
9 | FROM alpine:3.22.1
10 |
11 | RUN addgroup -S app && adduser -S -G app app
12 | COPY --from=builder /app/server /usr/local/bin/server
13 | RUN chown app:app /usr/local/bin/server
14 | USER app
15 | ENV BACKEND_PORT=8080
16 | EXPOSE 8080
17 | ENTRYPOINT ["/usr/local/bin/server"]
18 |
--------------------------------------------------------------------------------
/frontend/src/components/logo/Logo.jsx:
--------------------------------------------------------------------------------
1 | const Logo = () => {
2 | return (
3 |
4 |
9 | SP
10 |
11 |
12 | );
13 | };
14 |
15 | export default Logo;
--------------------------------------------------------------------------------
/frontend/src/assets/svg/markets.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/pages/changepassword/ChangePassword.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ChangePasswordLayout from '../../components/layouts/changepassword/ChangePasswordLayout';
3 |
4 | function ChangePassword() {
5 |
6 |
7 | return (
8 |
15 | );
16 | }
17 |
18 | export default ChangePassword;
19 |
--------------------------------------------------------------------------------
/scripts/prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Make sure the script can only be run via SocialPredict Script
4 | [ -z "$CALLED_FROM_SOCIALPREDICT" ] && { echo "Not called from SocialPredict"; exit 42; }
5 |
6 | # Check for .env file
7 | export CALLED_FROM_SOCIALPREDICT=yes
8 | source "$SCRIPT_DIR/scripts/prod/env_writer_prod.sh"
9 | _main
10 | unset CALLED_FROM_SOCIALPREDICT
11 |
12 | source_env
13 |
14 | sleep 1;
15 |
16 | # Build Docker Images
17 | export CALLED_FROM_SOCIALPREDICT=yes
18 | source "$SCRIPT_DIR/scripts/prod/build_prod.sh"
19 | unset CALLED_FROM_SOCIALPREDICT
20 |
--------------------------------------------------------------------------------
/frontend/src/components/inputs/InputBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const RegularInputBox = ({
4 | value,
5 | onChange,
6 | name,
7 | placeholder,
8 | className,
9 | ...props
10 | }) => {
11 | return (
12 |
20 | );
21 | };
22 |
23 | export default RegularInputBox;
24 |
--------------------------------------------------------------------------------
/backend/setup/setup.yaml:
--------------------------------------------------------------------------------
1 | economics:
2 | marketcreation:
3 | initialMarketProbability: 0.5
4 | initialMarketSubsidization: 10
5 | initialMarketYes: 0
6 | initialMarketNo: 0
7 | minimumFutureHours: 1.0
8 | marketincentives:
9 | createMarketCost: 10
10 | traderBonus: 1
11 | user:
12 | initialAccountBalance: 0
13 | maximumDebtAllowed: 500
14 | betting:
15 | minimumBet: 1
16 | maxDustPerSale: 2
17 | betFees:
18 | initialBetFee: 1
19 | buySharesFee: 0
20 | sellSharesFee: 0
21 |
22 | frontend:
23 | charts:
24 | sigFigs: 4
25 |
--------------------------------------------------------------------------------
/docker/frontend/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | TEMPLATE="/usr/share/nginx/html/env-config.js.template"
5 | TARGET="/usr/share/nginx/html/env-config.js"
6 |
7 | # Defaults if not set
8 | : "${DOMAIN_URL:=http://localhost}"
9 | : "${API_URL:=http://localhost}"
10 |
11 | if [ -f "$TEMPLATE" ]; then
12 | cp "$TEMPLATE" "$TARGET"
13 |
14 | sed -i "s|\${DOMAIN_URL:-http://localhost}|${DOMAIN_URL}|g" "$TARGET"
15 | sed -i "s|\${API_URL:-http://localhost:8080}|${DOMAIN_URL}/api|g" "$TARGET"
16 |
17 | chown nginx:nginx "$TARGET"
18 |
19 | fi
20 |
21 | exec nginx -g "daemon off;"
22 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/polls.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/models/homepage.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type HomepageContent struct {
10 | gorm.Model
11 | ID uint `gorm:"primaryKey"`
12 | Slug string `gorm:"uniqueIndex;size:64"` // always "home"
13 | Title string `gorm:"size:255"`
14 | Format string `gorm:"size:16"` // "markdown" or "html"
15 | Markdown string `gorm:"type:text"`
16 | HTML string `gorm:"type:text"`
17 | Version uint `gorm:"default:1"` // optimistic locking
18 | UpdatedBy string `gorm:"size:64"`
19 | CreatedAt time.Time
20 | UpdatedAt time.Time
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/profileEdit/ProfileEdit.jsx:
--------------------------------------------------------------------------------
1 | // EmojiSelector.js
2 | import React, { useState } from 'react';
3 |
4 | const ProfileEdit = ({ onSelectEmoji }) => {
5 | const emojis = ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇'];
6 |
7 | const handleEmojiSelect = (emoji) => {
8 | onSelectEmoji(emoji);
9 | };
10 |
11 | return (
12 |
13 | {emojis.map((emoji) => (
14 | handleEmojiSelect(emoji)}>
15 | {emoji}
16 |
17 | ))}
18 |
19 | );
20 | };
21 |
22 | export default ProfileEdit;
23 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-staging.yml:
--------------------------------------------------------------------------------
1 | name: Deploy To Staging
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | types: [closed]
8 |
9 | jobs:
10 | deploy:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | contents: write
14 | steps:
15 | - name: Deploy SocialPredict to Staging
16 | if: github.event.pull_request.merged == true
17 | uses: peter-evans/repository-dispatch@v4
18 | with:
19 | token: ${{ secrets.ANSIBLE_PLAYBOOK_TOKEN }}
20 | repository: openpredictionmarkets/ansible_playbooks
21 | event-type: deploy-to-staging
22 |
--------------------------------------------------------------------------------
/frontend/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig(() => {
5 | return {
6 | server: {
7 | allowedHosts: [
8 | 'frontend', // we need this to be able to access the app on localhost
9 | 'localhost'
10 | ]
11 | },
12 | build: {
13 | outDir: 'build',
14 | commonjsOptions: { transformMixedEsModules: true },
15 | chunkSizeWarningLimit: 1000,
16 | },
17 | plugins: [react()],
18 | css: {
19 | target: 'async', // or 'defer' for older browsers
20 | },
21 | };
22 | });
23 |
--------------------------------------------------------------------------------
/backend/logging/mocklogging.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import "fmt"
4 |
5 | // MockLogger is a mock implementation of Logger for testing.
6 | type MockLogger struct {
7 | CalledFatalf bool
8 | MessageFatalf string
9 | CalledPrintf bool
10 | MessagePrintf string
11 | }
12 |
13 | func (m *MockLogger) Fatalf(format string, args ...interface{}) {
14 | m.CalledFatalf = true
15 | m.MessageFatalf = fmt.Sprintf(format, args...)
16 | panic(m.MessageFatalf) // Simulate Fatalf by panicking
17 | }
18 |
19 | func (m *MockLogger) Printf(format string, args ...interface{}) {
20 | m.CalledPrintf = true
21 | m.MessagePrintf = fmt.Sprintf(format, args...)
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/components/layouts/profile/ProfileUtility.jsx:
--------------------------------------------------------------------------------
1 | export const renderPersonalLinks = () => {
2 | const linkKeys = ['personalink1', 'personalink2', 'personalink3', 'personalink4'];
3 | return linkKeys.map(key => {
4 | const link = userData[key];
5 | return link ? (
6 |
15 | ) : null;
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-production.yml:
--------------------------------------------------------------------------------
1 | name: Deploy To Production
2 |
3 | on:
4 | workflow_run:
5 | workflows: ["Create and publish Docker images"]
6 | types: [completed]
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | steps:
14 | - name: Deploy SocialPredict to Production
15 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
16 | uses: peter-evans/repository-dispatch@v4
17 | with:
18 | token: ${{ secrets.ANSIBLE_PLAYBOOK_TOKEN }}
19 | repository: openpredictionmarkets/ansible_playbooks
20 | event-type: deploy-to-production
21 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/profile/ProfileButtons.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Existing Emoji Button
4 | const ProfileEditButton = ({ onClick, children, isSelected }) => {
5 | const initialButtonStyle = "bg-custom-gray-light border-transparent";
6 | const selectedButtonStyle = "bg-primary-pink border-transparent";
7 |
8 | return (
9 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default ProfileEditButton;
19 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/BaseButton.jsx:
--------------------------------------------------------------------------------
1 | export const buttonBaseStyle = "w-full px-4 py-2 text-white border rounded focus:outline-none";
2 | // const buttonBaseStyle = "w-full px-4 py-2 text-white border border-transparent rounded focus:outline-none focus:ring-2 focus:ring-offset-2";
3 | const yesButtonStyle = `bg-green-btn hover:bg-green-btn-hover`;
4 | const yesButtonHoverStyle = `bg-green-btn-hover hover:bg-green-btn`;
5 | const noButtonStyle = `bg-red-btn hover:bg-red-btn`;
6 | const noButtonHoverStyle = `bg-red-btn-hover hover:bg-red-btn`;
7 | const neutralButtonStyle = `bg-neutral-btn hover:bg-neutral-btn-hover`;
8 | const neutralButtonHoverStyle = `bg-neutral-btn-hover hover:bg-neutral-btn`;
9 |
--------------------------------------------------------------------------------
/frontend/src/utils/statusMap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Mapping from UI tab labels to backend status values
3 | * This ensures consistency across all components that need to translate
4 | * between UI tab names and API status parameters
5 | */
6 | export const TAB_TO_STATUS = {
7 | Active: "active",
8 | Closed: "closed",
9 | Resolved: "resolved",
10 | All: "all", // Backend interprets "all" as no filter
11 | };
12 |
13 | /**
14 | * Reverse mapping from status to tab labels
15 | * Useful for determining which tab should be active based on status
16 | */
17 | export const STATUS_TO_TAB = {
18 | active: "Active",
19 | closed: "Closed",
20 | resolved: "Resolved",
21 | all: "All",
22 | };
23 |
--------------------------------------------------------------------------------
/docker/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22.20.0-alpine3.22 AS builder
2 |
3 | WORKDIR /app
4 | COPY frontend/package.json frontend/package-lock.json ./frontend/
5 | WORKDIR /app/frontend
6 | RUN npm ci
7 | COPY frontend/ .
8 | RUN npm run build
9 |
10 | FROM nginx:1.29.1-alpine3.22
11 |
12 | COPY --from=builder /app/frontend/build /usr/share/nginx/html
13 | COPY frontend/public/env-config.js.template /usr/share/nginx/html/env-config.js.template
14 | COPY docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf
15 | COPY docker/frontend/entrypoint.sh /entrypoint.sh
16 | RUN chmod +x /entrypoint.sh
17 | RUN chown -R nginx:nginx /usr/share/nginx/html
18 | EXPOSE 80
19 | ENTRYPOINT ["/entrypoint.sh"]
20 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/api-key.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/coins.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/graphicalTools/GraphToolTips.jsx:
--------------------------------------------------------------------------------
1 | const Tooltip = ({ content, position }) => {
2 | if (!content) return null;
3 |
4 | const style = {
5 | display: 'block',
6 | position: 'absolute',
7 | top: `${position.y}px`,
8 | left: `${position.x}px`,
9 | backgroundColor: 'white',
10 | padding: '10px',
11 | border: '1px solid',
12 | borderRadius: '5px',
13 | pointerEvents: 'none', // this prevents the tooltip from interfering with mouse events
14 | zIndex: 1000 // make sure this is above everything else
15 | };
16 |
17 | return (
18 |
19 | {content}
20 |
21 | );
22 | };
23 |
24 | export default Tooltip;
--------------------------------------------------------------------------------
/backend/handlers/bets/errors.go:
--------------------------------------------------------------------------------
1 | package betshandlers
2 |
3 | import "fmt"
4 |
5 | // ErrDustCapExceeded is returned when a sell transaction would generate dust exceeding the configured cap
6 | type ErrDustCapExceeded struct {
7 | Cap int64 // Maximum allowed dust per sale
8 | Requested int64 // Amount of dust that would be generated
9 | }
10 |
11 | // Error implements the error interface
12 | func (e ErrDustCapExceeded) Error() string {
13 | return fmt.Sprintf("dust cap exceeded: would generate %d dust points (cap: %d)", e.Requested, e.Cap)
14 | }
15 |
16 | // IsBusinessRuleError identifies this as a business rule violation (HTTP 422)
17 | func (e ErrDustCapExceeded) IsBusinessRuleError() bool {
18 | return true
19 | }
20 |
--------------------------------------------------------------------------------
/backend/handlers/math/financials/workprofits.go:
--------------------------------------------------------------------------------
1 | package financials
2 |
3 | import (
4 | "gorm.io/gorm"
5 | )
6 |
7 | // sumWorkProfitsFromTransactions calculates work-based profits from transactions
8 | // Currently returns 0 since no separate transaction system exists yet (only bets)
9 | // This maintains API structure for future extensibility when work rewards are added
10 | func sumWorkProfitsFromTransactions(db *gorm.DB, username string) (int64, error) {
11 | // No separate transaction system exists yet - only models.Bet
12 | // Work rewards like "WorkReward" and "Bounty" types referenced in checkpoint don't exist
13 | // Return 0 to maintain financial snapshot structure for future extensibility
14 | return 0, nil
15 | }
16 |
--------------------------------------------------------------------------------
/backend/handlers/tradingdata/getbets.go:
--------------------------------------------------------------------------------
1 | package tradingdata
2 |
3 | import (
4 | "socialpredict/models"
5 | "time"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | type PublicBet struct {
11 | ID uint `json:"betId"`
12 | Username string `json:"username"`
13 | MarketID uint `json:"marketId"`
14 | Amount int64 `json:"amount"`
15 | PlacedAt time.Time `json:"placedAt"`
16 | Outcome string `json:"outcome,omitempty"`
17 | }
18 |
19 | func GetBetsForMarket(db *gorm.DB, marketID uint) []models.Bet {
20 | var bets []models.Bet
21 |
22 | if err := db.
23 | Where("market_id = ?", marketID).
24 | Order("placed_at ASC").
25 | Find(&bets).Error; err != nil {
26 | return nil
27 | }
28 |
29 | return bets
30 | }
31 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | At the time of authoring this security policy, we will only support the master branch for the time being. Sorry for any inconvenience this may cause.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | To report a vulnerability, simply create an issue or a pull request.
10 |
11 | If the vulnerability is serious, we will try to fix it within a week or a few days. If it's not that serious, we'll try to cover it in a month or so cycle.
12 |
13 | We may comment and decline the security vulnerability through discussion and mutual agreement with you if it's found to be not worth the effort.
14 |
15 | That being said we appreciate any messages about potential threats because you never know if they could be bigger than you think.
16 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/profile/ProfileModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ProfileModal = ({ isOpen, onClose, children, title }) => {
4 | if (!isOpen) return null;
5 |
6 | return (
7 |
8 |
9 |
{title}
10 | {children}
11 |
12 | ✕
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default ProfileModal;
20 |
--------------------------------------------------------------------------------
/README/TROUBLESHOOTING.md:
--------------------------------------------------------------------------------
1 | This is a non-exhaustive list of issues that users have encountered during local setup. If you encounter issues, please contact the contributors.
2 |
3 | ## Ubuntu
4 |
5 | **ERROR: failed to solve: process "/bin/bash -euo pipefail -c -apt-get update && apt-get install -y inotify-tools=3.22.6.0-4 openssl=3.0.11~deb12u2" did not complete successfully: exit code: 100**
6 | - Your internet connection could be slow. Alternatively, debian.org could be down temporarily. Wait 15 minutes and try again.
7 |
8 | ## macOS
9 |
10 | Nothing here yet.
11 |
12 | ## Windows (WSL2)
13 | **ERROR: unable to locate `docker-compose-plugin`**
14 | - Many solutions, but one solution that has been verified to work is running VSCode inside WSL2 and installing the Docker plugin from there.
15 |
--------------------------------------------------------------------------------
/backend/handlers/metrics/getgloballeaderboard.go:
--------------------------------------------------------------------------------
1 | package metricshandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | positionsmath "socialpredict/handlers/math/positions"
7 | "socialpredict/util"
8 | )
9 |
10 | func GetGlobalLeaderboardHandler(w http.ResponseWriter, r *http.Request) {
11 | db := util.GetDB()
12 |
13 | leaderboard, err := positionsmath.CalculateGlobalLeaderboard(db)
14 | if err != nil {
15 | http.Error(w, "failed to compute global leaderboard: "+err.Error(), http.StatusInternalServerError)
16 | return
17 | }
18 |
19 | w.Header().Set("Content-Type", "application/json")
20 | if err := json.NewEncoder(w).Encode(leaderboard); err != nil {
21 | http.Error(w, "Failed to encode leaderboard response: "+err.Error(), http.StatusInternalServerError)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/notfound/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const NotFound = () => {
5 | return (
6 |
7 |
8 |
404 - Page Not Found
9 |
10 | Oops! The page you're looking for doesn't exist.
11 |
12 |
16 | Go Home
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default NotFound;
24 |
--------------------------------------------------------------------------------
/backend/models/modelstesting/economics_helpers.go:
--------------------------------------------------------------------------------
1 | package modelstesting
2 |
3 | import (
4 | "testing"
5 |
6 | "socialpredict/setup"
7 | )
8 |
9 | // UseStandardTestEconomics replaces the global economics configuration with the standard
10 | // testing values for the duration of the test. The original configuration is restored
11 | // automatically via t.Cleanup.
12 | func UseStandardTestEconomics(t *testing.T) (*setup.EconomicConfig, func() *setup.EconomicConfig) {
13 | t.Helper()
14 |
15 | econConfig := setup.EconomicsConfig()
16 | original := econConfig.Economics
17 | econConfig.Economics = GenerateEconomicConfig().Economics
18 |
19 | t.Cleanup(func() {
20 | econConfig.Economics = original
21 | })
22 |
23 | return econConfig, func() *setup.EconomicConfig {
24 | return econConfig
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/data/traefik/config/traefik.template:
--------------------------------------------------------------------------------
1 | log:
2 | filePath: "/logs/error.log"
3 | format: json
4 | level: WARN
5 |
6 | entryPoints:
7 | web:
8 | address: ":80"
9 | http:
10 | redirections:
11 | entryPoint:
12 | to: websecure
13 | scheme: https
14 | websecure:
15 | address: ":443"
16 | asDefault: true
17 | http:
18 | tls:
19 | certresolver: letsencrypt
20 |
21 | providers:
22 | docker:
23 | endpoint: "unix:///var/run/docker.sock"
24 | exposedByDefault: false
25 | network: socialpredict_external_network
26 |
27 | certificatesResolvers:
28 | letsencrypt:
29 | acme:
30 | email: ${EMAIL}
31 | storage: /acme.json
32 | caServer: https://acme-v02.api.letsencrypt.org/directory
33 | httpChallenge:
34 | entrypoint: web
35 |
--------------------------------------------------------------------------------
/backend/handlers/cms/homepage/repo.go:
--------------------------------------------------------------------------------
1 | package homepage
2 |
3 | import (
4 | "socialpredict/models"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Repository interface {
10 | GetBySlug(slug string) (*models.HomepageContent, error)
11 | Save(item *models.HomepageContent) error
12 | }
13 |
14 | type GormRepository struct {
15 | db *gorm.DB
16 | }
17 |
18 | func NewGormRepository(db *gorm.DB) *GormRepository {
19 | return &GormRepository{db: db}
20 | }
21 |
22 | func (r *GormRepository) GetBySlug(slug string) (*models.HomepageContent, error) {
23 | var item models.HomepageContent
24 | if err := r.db.Where("slug = ?", slug).First(&item).Error; err != nil {
25 | return nil, err
26 | }
27 | return &item, nil
28 | }
29 |
30 | func (r *GormRepository) Save(item *models.HomepageContent) error {
31 | return r.db.Save(item).Error
32 | }
33 |
--------------------------------------------------------------------------------
/backend/handlers/metrics/getsystemmetrics.go:
--------------------------------------------------------------------------------
1 | package metricshandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/handlers/math/financials"
7 | "socialpredict/setup"
8 | "socialpredict/util"
9 | )
10 |
11 | func GetSystemMetricsHandler(w http.ResponseWriter, r *http.Request) {
12 | db := util.GetDB()
13 | load := setup.EconomicsConfig // matches EconConfigLoader (func() *EconomicConfig)
14 |
15 | res, err := financials.ComputeSystemMetrics(db, load)
16 | if err != nil {
17 | http.Error(w, "failed to compute metrics: "+err.Error(), http.StatusInternalServerError)
18 | return
19 | }
20 |
21 | w.Header().Set("Content-Type", "application/json")
22 | if err := json.NewEncoder(w).Encode(res); err != nil {
23 | http.Error(w, "Failed to encode metrics response: "+err.Error(), http.StatusInternalServerError)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/handlers/users/publicuser.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/models"
7 | "socialpredict/util"
8 |
9 | "github.com/gorilla/mux"
10 | "gorm.io/gorm"
11 | )
12 |
13 | func GetPublicUserResponse(w http.ResponseWriter, r *http.Request) {
14 | // Extract the username from the URL
15 | vars := mux.Vars(r)
16 | username := vars["username"]
17 |
18 | db := util.GetDB()
19 |
20 | response := GetPublicUserInfo(db, username)
21 |
22 | w.Header().Set("Content-Type", "application/json")
23 | json.NewEncoder(w).Encode(response)
24 | }
25 |
26 | // Function to get the users public info From the Database
27 | func GetPublicUserInfo(db *gorm.DB, username string) models.PublicUser {
28 | var user models.User
29 | db.Where("username = ?", username).First(&user)
30 |
31 | return user.PublicUser
32 | }
33 |
--------------------------------------------------------------------------------
/backend/handlers/users/listusers.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "log"
5 | "socialpredict/models"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | // ListUserMarkets lists all markets that a specific user is betting in, ordered by the date of the last bet.
11 | func ListUserMarkets(db *gorm.DB, userID int64) ([]models.Market, error) {
12 | var markets []models.Market
13 |
14 | // Query to find all markets where the user has bets, ordered by the date of the last bet
15 | query := db.Table("markets").
16 | Joins("join bets on bets.market_id = markets.id").
17 | Where("bets.user_id = ?", userID).
18 | Order("bets.created_at DESC").
19 | Distinct("markets.*").
20 | Find(&markets)
21 |
22 | if query.Error != nil {
23 | log.Printf("Error fetching user's markets: %v", query.Error)
24 | return nil, query.Error
25 | }
26 |
27 | return markets, nil
28 | }
29 |
--------------------------------------------------------------------------------
/backend/handlers/users/publicuser/publicuser.go:
--------------------------------------------------------------------------------
1 | package publicuser
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/models"
7 | "socialpredict/util"
8 |
9 | "github.com/gorilla/mux"
10 | "gorm.io/gorm"
11 | )
12 |
13 | func GetPublicUserResponse(w http.ResponseWriter, r *http.Request) {
14 | // Extract the username from the URL
15 | vars := mux.Vars(r)
16 | username := vars["username"]
17 |
18 | db := util.GetDB()
19 |
20 | response := GetPublicUserInfo(db, username)
21 |
22 | w.Header().Set("Content-Type", "application/json")
23 | json.NewEncoder(w).Encode(response)
24 | }
25 |
26 | // Function to get the users public info From the Database
27 | func GetPublicUserInfo(db *gorm.DB, username string) models.PublicUser {
28 | var user models.User
29 | db.Where("username = ?", username).First(&user)
30 |
31 | return user.PublicUser
32 | }
33 |
--------------------------------------------------------------------------------
/docker/frontend/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name _;
4 |
5 | root /usr/share/nginx/html;
6 | index index.html;
7 |
8 | # Serve SPA with client-side routing
9 | location / {
10 | try_files $uri $uri/ /index.html;
11 | }
12 |
13 | # Prevent caching of the runtime env file
14 | location = /env-config.js {
15 | add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
16 | try_files $uri =404;
17 | }
18 |
19 | # Also avoid caching index.html
20 | location = /index.html {
21 | add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
22 | try_files $uri =404;
23 | }
24 |
25 | # Basic compression
26 | gzip on;
27 | gzip_types text/plain text/css application/javascript application/json application/xml application/xml+rss image/svg+xml;
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/marketDetails/DescriptionButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { buttonBaseStyle } from '../BaseButton';
3 |
4 | const DescriptionButton = ({ onClick, children }) => {
5 | const [isSelected, setIsSelected] = useState(false);
6 | const initialButtonStyle = "bg-custom-gray-light border-transparent";
7 | const selectedButtonStyle = "bg-primary-pink border-transparent";
8 |
9 | const handleButtonClick = () => {
10 | setIsSelected(!isSelected);
11 | if (onClick) onClick();
12 | };
13 |
14 | return (
15 |
19 | {children || 'SELECT'}
20 |
21 | );
22 | };
23 |
24 | export default DescriptionButton;
--------------------------------------------------------------------------------
/frontend/src/pages/admin/AdminDashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AdminAddUser from '../../components/layouts/admin/AddUser';
3 | import HomeEditor from './HomeEditor';
4 | import SiteTabs from '../../components/tabs/SiteTabs';
5 |
6 | function AdminDashboard() {
7 | const tabsData = [
8 | {
9 | label: 'Add User',
10 | content:
11 | },
12 | {
13 | label: 'Homepage Editor',
14 | content:
15 | }
16 | ];
17 |
18 | return (
19 |
26 | );
27 | }
28 |
29 | export default AdminDashboard;
30 |
--------------------------------------------------------------------------------
/backend/errors/httperror.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 | )
8 |
9 | // HTTPErrorResponse represents a structured error response.
10 | type HTTPErrorResponse struct {
11 | Error string `json:"error"`
12 | }
13 |
14 | // HandleHTTPError checks for an error and handles it by sending an appropriate HTTP response.
15 | // It logs the error and writes a corresponding HTTP status code and error message to the response writer.
16 | // Returns true if there was an error handled, false otherwise.
17 | func HandleHTTPError(w http.ResponseWriter, err error, statusCode int, userMessage string) bool {
18 | if err != nil {
19 | log.Printf("Error: %v", err) // Log the actual error for server-side diagnostics.
20 | w.WriteHeader(statusCode)
21 | json.NewEncoder(w).Encode(HTTPErrorResponse{Error: userMessage})
22 | return true
23 | }
24 | return false
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/assets/svg/admin-gear.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/handlers/bets/betutils/betutils.go:
--------------------------------------------------------------------------------
1 | package betutils
2 |
3 | import (
4 | "errors"
5 | "socialpredict/models"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | // CheckMarketStatus checks if the market is resolved or closed.
12 | // It returns an error if the market is not suitable for placing a bet.
13 | func CheckMarketStatus(db *gorm.DB, marketID uint) error {
14 | var market models.Market
15 | if result := db.First(&market, marketID); result.Error != nil {
16 | if errors.Is(result.Error, gorm.ErrRecordNotFound) {
17 | return errors.New("market not found")
18 | }
19 | return errors.New("error fetching market")
20 | }
21 |
22 | if market.IsResolved {
23 | return errors.New("cannot place a bet on a resolved market")
24 | }
25 |
26 | if time.Now().After(market.ResolutionDateTime) {
27 | return errors.New("cannot place a bet on a closed market")
28 | }
29 |
30 | return nil
31 | }
32 |
--------------------------------------------------------------------------------
/backend/models/modelstesting/modelstesting_test.go:
--------------------------------------------------------------------------------
1 | package modelstesting
2 |
3 | import (
4 | "socialpredict/models"
5 | "testing"
6 | )
7 |
8 | func TestNewFakeDB(t *testing.T) {
9 | db := NewFakeDB(t)
10 | if db == nil {
11 | t.Fatal("Failed to create fake db")
12 | }
13 | tests := []struct {
14 | Name string
15 | Table interface{}
16 | WantRows int
17 | }{
18 | {
19 | Name: "QueryUsers0",
20 | Table: &models.User{},
21 | WantRows: 0,
22 | },
23 | {
24 | Name: "QueryMarkets0",
25 | Table: &models.Market{},
26 | WantRows: 0,
27 | },
28 | {
29 | Name: "QueryBets0",
30 | Table: &models.Bet{},
31 | WantRows: 0,
32 | },
33 | }
34 | for _, test := range tests {
35 | t.Run(test.Name, func(t *testing.T) {
36 | result := db.Find(test.Table)
37 | if int(result.RowsAffected) != test.WantRows {
38 | t.Fail()
39 | }
40 | })
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/SiteButtons.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { buttonBaseStyle } from './BaseButton';
3 |
4 | const SiteButton = ({ onClick, children }) => {
5 | const [isSelected, setIsSelected] = useState(false);
6 | const initialButtonStyle = "bg-custom-gray-light border-transparent";
7 | const selectedButtonStyle = "bg-primary-pink border-transparent";
8 |
9 | const handleClick = () => {
10 | setIsSelected(!isSelected);
11 | if (onClick) { // Only call onClick if it is provided
12 | onClick();
13 | }
14 | };
15 |
16 | return (
17 |
21 | {children || 'SELECT'}
22 |
23 | );
24 | };
25 |
26 | export default SiteButton;
27 |
--------------------------------------------------------------------------------
/backend/middleware/authutils.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/golang-jwt/jwt/v4"
9 | )
10 |
11 | type HTTPError struct {
12 | StatusCode int
13 | Message string
14 | }
15 |
16 | func (e *HTTPError) Error() string {
17 | return e.Message
18 | }
19 |
20 | // ExtractTokenFromHeader extracts the JWT token from the Authorization header of the request
21 | func extractTokenFromHeader(r *http.Request) (string, error) {
22 | authHeader := r.Header.Get("Authorization")
23 | if authHeader == "" {
24 | return "", errors.New("authorization header is required")
25 | }
26 | return strings.TrimPrefix(authHeader, "Bearer "), nil
27 | }
28 |
29 | // ParseToken parses the JWT token and returns the claims
30 | func parseToken(tokenString string, keyFunc jwt.Keyfunc) (*jwt.Token, error) {
31 | return jwt.ParseWithClaims(tokenString, &UserClaims{}, keyFunc)
32 | }
33 |
--------------------------------------------------------------------------------
/backend/util/getenv.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/joho/godotenv"
8 | )
9 |
10 | // GetEnv loads environment variables from .env.dev if it exists.
11 | // It is non-fatal when the file is missing so containers can rely on
12 | // real environment variables supplied at runtime.
13 | func GetEnv() error {
14 | // If the file does not exist, skip loading silently.
15 | if _, err := os.Stat("./.env.dev"); os.IsNotExist(err) {
16 | log.Printf(".env.dev not found; skipping dotenv load")
17 | return nil
18 | } else if err != nil {
19 | // Unexpected stat error, log and attempt to continue
20 | log.Printf("Warning checking .env.dev: %v", err)
21 | }
22 | // Try to load the file; on failure, log a warning but do not return an error.
23 | if err := godotenv.Load("./.env.dev"); err != nil {
24 | log.Printf("Warning: failed to load .env.dev: %v", err)
25 | }
26 | return nil
27 | }
28 |
--------------------------------------------------------------------------------
/backend/migration/migrations/20251013_080000_core_models_test.go:
--------------------------------------------------------------------------------
1 | package migrations_test
2 |
3 | import (
4 | "testing"
5 |
6 | "socialpredict/models"
7 | "socialpredict/models/modelstesting"
8 | )
9 |
10 | func TestCoreModelsMigration_CreatesTablesAndColumns(t *testing.T) {
11 | db := modelstesting.NewFakeDB(t) // runs global migrations incl. this file's init-registered one
12 |
13 | m := db.Migrator()
14 |
15 | // Tables
16 | for _, tbl := range []any{&models.User{}, &models.Market{}, &models.Bet{}, &models.HomepageContent{}} {
17 | if !m.HasTable(tbl) {
18 | t.Fatalf("expected table for %T to exist", tbl)
19 | }
20 | }
21 |
22 | // Critical market columns (v2 change)
23 | if !m.HasColumn(&models.Market{}, "YesLabel") {
24 | t.Fatalf("expected markets.yes_label column to exist")
25 | }
26 | if !m.HasColumn(&models.Market{}, "NoLabel") {
27 | t.Fatalf("expected markets.no_label column to exist")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/models/modelstesting/bets_helpers.go:
--------------------------------------------------------------------------------
1 | package modelstesting
2 |
3 | import (
4 | "socialpredict/models"
5 | "socialpredict/setup"
6 | )
7 |
8 | type betKey struct {
9 | marketID uint
10 | username string
11 | }
12 |
13 | // CalculateParticipationFees determines the total participation fees owed for the provided
14 | // bets according to the configured economics rules. Only the first positive bet per user
15 | // per market incurs the fee.
16 | func CalculateParticipationFees(cfg *setup.EconomicConfig, bets []models.Bet) int64 {
17 | var total int64
18 | seen := make(map[betKey]bool)
19 | initialFee := cfg.Economics.Betting.BetFees.InitialBetFee
20 |
21 | for _, bet := range bets {
22 | if bet.Amount <= 0 {
23 | continue
24 | }
25 |
26 | key := betKey{marketID: bet.MarketID, username: bet.Username}
27 | if !seen[key] {
28 | seen[key] = true
29 | total += initialFee
30 | }
31 | }
32 |
33 | return total
34 | }
35 |
--------------------------------------------------------------------------------
/backend/handlers/users/README_USERS.md:
--------------------------------------------------------------------------------
1 | ### Conventions
2 |
3 | #### Convention 20250619 241D9EDE-9C76-4CBB-B08F-D397A74642C5
4 |
5 | * Any time an amount is deducted or added to a user balance, we should use apply_transaction.
6 | * There should be consistency in terms of how a user's wallet is changed since it is fundamental to the operation of the entire system to maintain the integrity of number of credits created or destroyed.
7 | * This should apply to payouts, refunds, betting, selling shares, creating markets and so on.
8 |
9 | #### Convention 20250619 050E2C8D-3D58-49C5-AEA4-E140FC055A1A
10 |
11 | * There should be a uniform way to check user balances prior to performing another operation, similar to 241D9EDE-9C76-4CBB-B08F-D397A74642C5.
12 |
13 | #### Convention 20250619 88897BB8-6845-46E4-B43B-0F6652951622
14 |
15 | * We should always use the PublicUser model to interact with users for any business logic operation to reduce the risk of exposing user info.
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # Like our work? Sponsor us!
2 |
3 | github: [openpredictionmarkets]
4 |
5 | # We don't have any of these yet; may be added in future
6 |
7 | patreon: # Replace with a single Patreon username
8 | open_collective: # Replace with a single Open Collective username
9 | ko_fi: # Replace with a single Ko-fi username
10 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
11 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
12 | liberapay: # Replace with a single Liberapay username
13 | issuehunt: # Replace with a single IssueHunt username
14 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
15 | polar: # Replace with a single Polar username
16 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
17 | thanks_dev: # Replace with a single thanks.dev username
18 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
19 |
--------------------------------------------------------------------------------
/README/CHECKPOINTS/CHECKPOINT20251105-01.md:
--------------------------------------------------------------------------------
1 | ## The goal:
2 |
3 | - Have users able to change the number of significant figures displayed on a stepped chart by editing setup.yaml (between 2 and 9 significant figures)
4 | - Prediction market charts to display a variable number of significant figures (2-9) based on the user input in setup.yaml
5 |
6 | ## Step by step implementation
7 |
8 | 1) Modify setup.yaml so that users can set the number of significant figures in the market charts
9 |
10 | 2) Create a tiny helper that:
11 |
12 | - fetches the yaml file
13 |
14 | - parses it with our SocialPredict tools
15 |
16 | - clamps the number of significant figures to the allowed range (2–9)
17 |
18 | - provides a ready-to-use number formatter
19 |
20 | 3) Build a single formatting utility
21 |
22 | 4) Wire the formatter into your Chart.js options
23 |
24 | Apply it to:
25 |
26 | - Y-axis ticks (most common)
27 |
28 | - Tooltip label (so hover values match the tick style)
--------------------------------------------------------------------------------
/scripts/go.mod:
--------------------------------------------------------------------------------
1 | module socialpredict-scripts
2 |
3 | go 1.23.1
4 |
5 | require github.com/joho/godotenv v1.5.1
6 |
7 | // Use local socialpredict backend module
8 | replace socialpredict => ../backend
9 |
10 | require (
11 | gorm.io/gorm v1.25.12
12 | socialpredict v0.0.0-00010101000000-000000000000
13 | )
14 |
15 | require (
16 | github.com/brianvoe/gofakeit v3.18.0+incompatible // indirect
17 | github.com/google/uuid v1.6.0 // indirect
18 | github.com/jackc/pgpassfile v1.0.0 // indirect
19 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
20 | github.com/jackc/pgx/v5 v5.7.1 // indirect
21 | github.com/jackc/puddle/v2 v2.2.2 // indirect
22 | github.com/jinzhu/inflection v1.0.0 // indirect
23 | github.com/jinzhu/now v1.1.5 // indirect
24 | golang.org/x/crypto v0.36.0 // indirect
25 | golang.org/x/sync v0.12.0 // indirect
26 | golang.org/x/text v0.23.0 // indirect
27 | gopkg.in/yaml.v3 v3.0.1 // indirect
28 | gorm.io/driver/postgres v1.5.9 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/frontend/src/components/tabs/ActivityTabs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SiteTabs from './SiteTabs';
3 | import BetsActivityLayout from '../layouts/activity/bets/BetsActivity';
4 | import PositionsActivityLayout from '../layouts/activity/positions/PositionsActivity';
5 | import LeaderboardActivity from '../layouts/activity/leaderboard/LeaderboardActivity';
6 |
7 | const ActivityTabs = ({ marketId, market, refreshTrigger }) => {
8 | const tabsData = [
9 | { label: 'Positions', content: },
10 | { label: 'Bets', content: },
11 | { label: 'Leaderboard', content: },
12 | { label: 'Comments', content: Comments Go here...
},
13 | ];
14 |
15 | return ;
16 | };
17 |
18 | export default ActivityTabs;
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/backend/handlers/math/probabilities/wpam/wpam_current_test.go:
--------------------------------------------------------------------------------
1 | package wpam
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestGetCurrentProbability(t *testing.T) {
9 | now := time.Now()
10 |
11 | t.Run("returns last probability from multiple entries", func(t *testing.T) {
12 | probChanges := []ProbabilityChange{
13 | {Probability: 0.5, Timestamp: now},
14 | {Probability: 0.6, Timestamp: now.Add(time.Minute)},
15 | {Probability: 0.7, Timestamp: now.Add(2 * time.Minute)},
16 | }
17 |
18 | prob := GetCurrentProbability(probChanges)
19 | if prob != 0.7 {
20 | t.Errorf("expected 0.7, got %f", prob)
21 | }
22 | })
23 |
24 | t.Run("returns only probability in single entry", func(t *testing.T) {
25 | probChanges := []ProbabilityChange{
26 | {Probability: 0.42, Timestamp: now},
27 | }
28 |
29 | prob := GetCurrentProbability(probChanges)
30 | if prob != 0.42 {
31 | t.Errorf("expected 0.42, got %f", prob)
32 | }
33 | })
34 |
35 | // 🚫 No test for empty input — we are okay with panics in this case
36 | }
37 |
--------------------------------------------------------------------------------
/backend/handlers/markets/leaderboard.go:
--------------------------------------------------------------------------------
1 | package marketshandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/errors"
7 | positionsmath "socialpredict/handlers/math/positions"
8 | "socialpredict/util"
9 |
10 | "github.com/gorilla/mux"
11 | )
12 |
13 | // MarketLeaderboardHandler handles requests for market profitability leaderboards
14 | func MarketLeaderboardHandler(w http.ResponseWriter, r *http.Request) {
15 | vars := mux.Vars(r)
16 | marketIdStr := vars["marketId"]
17 |
18 | // Set content type header early to ensure it's always set
19 | w.Header().Set("Content-Type", "application/json")
20 |
21 | // Open up database to utilize connection pooling
22 | db := util.GetDB()
23 |
24 | leaderboard, err := positionsmath.CalculateMarketLeaderboard(db, marketIdStr)
25 | if errors.HandleHTTPError(w, err, http.StatusBadRequest, "Invalid request or data processing error.") {
26 | return // Stop execution if there was an error.
27 | }
28 |
29 | // Respond with the leaderboard information
30 | json.NewEncoder(w).Encode(leaderboard)
31 | }
32 |
--------------------------------------------------------------------------------
/backend/handlers/users/userpositiononmarkethandler.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | positionsmath "socialpredict/handlers/math/positions"
7 | "socialpredict/middleware"
8 | "socialpredict/util"
9 |
10 | "github.com/gorilla/mux"
11 | )
12 |
13 | func UserMarketPositionHandler(w http.ResponseWriter, r *http.Request) {
14 | vars := mux.Vars(r)
15 | marketId := vars["marketId"]
16 |
17 | // Open up database to utilize connection pooling
18 | db := util.GetDB()
19 | user, httperr := middleware.ValidateTokenAndGetUser(r, db)
20 | if httperr != nil {
21 | http.Error(w, "Invalid token: "+httperr.Error(), http.StatusUnauthorized)
22 | return
23 | }
24 |
25 | userPosition, err := positionsmath.CalculateMarketPositionForUser_WPAM_DBPM(db, marketId, user.Username)
26 | if err != nil {
27 | http.Error(w, "Error calculating user market position: "+err.Error(), http.StatusInternalServerError)
28 | return
29 | }
30 |
31 | w.Header().Set("Content-Type", "application/json")
32 | json.NewEncoder(w).Encode(userPosition)
33 | }
34 |
--------------------------------------------------------------------------------
/backend/models/modelstesting/modelstesting.go:
--------------------------------------------------------------------------------
1 | // package modelstesting provides support for the
2 | package modelstesting
3 |
4 | import (
5 | "testing"
6 |
7 | "socialpredict/migration"
8 | // Import migrations to ensure init() functions run during tests
9 | _ "socialpredict/migration/migrations"
10 |
11 | "github.com/glebarez/sqlite"
12 | "gorm.io/gorm"
13 | )
14 |
15 | // NewFakeDB returns a sqlite db running in memory as a gorm.DB
16 | func NewFakeDB(t *testing.T) *gorm.DB {
17 | t.Helper()
18 | db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
19 | if err != nil {
20 | t.Fatalf("Failed to connect to the database: %v", err)
21 | }
22 | if err := migration.MigrateDB(db); err != nil {
23 | t.Fatalf("Failed to run migrations: %v", err)
24 | }
25 | return db
26 | }
27 |
28 | // Returns a Completely New DB and does not migrate. Used for testing migrations.
29 | func NewTestDB(t *testing.T) *gorm.DB {
30 | t.Helper()
31 | db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
32 | if err != nil {
33 | t.Fatalf("open sqlite: %v", err)
34 | }
35 | return db
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/components/TradeCTA.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | export default function TradeCTA({ onClick, disabled }) {
4 | const [isSelected, setIsSelected] = useState(false);
5 | const initialButtonStyle = "bg-custom-gray-light";
6 | const selectedButtonStyle = "bg-neutral-btn";
7 | const buttonBaseStyle = "w-full px-4 py-2 text-white border rounded focus:outline-none";
8 |
9 | const handleClick = () => {
10 | setIsSelected(!isSelected);
11 | onClick && onClick();
12 | };
13 |
14 | return (
15 |
20 |
26 | TRADE
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/components/tabs/TradeTabs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SiteTabs from './SiteTabs';
3 | import BuySharesLayout from '../layouts/trade/BuySharesLayout'
4 | import SellSharesLayout from '../layouts/trade/SellSharesLayout'
5 |
6 | const TradeTabs = ({ marketId, market, token, onTransactionSuccess }) => {
7 | const tabsData = [
8 | {
9 | label: 'Purchase Shares',
10 | content:
16 | },
17 | {
18 | label: 'Sell Shares',
19 | content:
25 | },
26 | ];
27 |
28 | return ;
29 | };
30 |
31 | export default TradeTabs;
--------------------------------------------------------------------------------
/backend/migration/migrations/20251013_080000_core_models.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "log"
5 |
6 | "socialpredict/logger"
7 | "socialpredict/migration"
8 | "socialpredict/models"
9 |
10 | "gorm.io/gorm"
11 | )
12 |
13 | func init() {
14 | err := migration.Register("20251013080000", func(db *gorm.DB) error {
15 | // Migrate the User models first
16 | if err := db.AutoMigrate(&models.User{}); err != nil {
17 | return err
18 | }
19 |
20 | // Then, migrate the Market model
21 | if err := db.AutoMigrate(&models.Market{}); err != nil {
22 | return err
23 | }
24 |
25 | // Then, migrate the Bet model
26 | if err := db.AutoMigrate(&models.Bet{}); err != nil {
27 | return err
28 | }
29 |
30 | // Then, migrate the HomepageContent model
31 | if err := db.AutoMigrate(&models.HomepageContent{}); err != nil {
32 | return err
33 | }
34 |
35 | return nil
36 | })
37 |
38 | // In init() functions, registration failure is a critical startup error
39 | if err != nil {
40 | logger.LogError("migrations", "init", err)
41 | log.Fatalf("Failed to register migration 20251013080000: %v", err)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Open Prediction Markets
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/backend/handlers/math/market/marketvolume.go:
--------------------------------------------------------------------------------
1 | package marketmath
2 |
3 | import (
4 | "log"
5 | "socialpredict/models"
6 | "socialpredict/setup"
7 | )
8 |
9 | // appConfig holds the loaded application configuration accessible within the package
10 | var appConfig *setup.EconomicConfig
11 |
12 | func init() {
13 | // Load configuration
14 | var err error
15 | appConfig, err = setup.LoadEconomicsConfig()
16 | if err != nil {
17 | log.Fatalf("Failed to load configuration: %v", err)
18 | }
19 | }
20 |
21 | // getMarketVolume returns the total volume of trades for a given market
22 | func GetMarketVolume(bets []models.Bet) int64 {
23 |
24 | var totalVolume int64
25 | for _, bet := range bets {
26 | totalVolume += bet.Amount
27 | }
28 |
29 | totalVolumeUint := int64(totalVolume)
30 |
31 | return totalVolumeUint
32 | }
33 |
34 | // returns the market volume + subsidization added into pool,
35 | // subsidzation in pool could be paid out after resolution but not sold mid-market
36 | func GetEndMarketVolume(bets []models.Bet) int64 {
37 |
38 | return GetMarketVolume(bets) + appConfig.Economics.MarketCreation.InitialMarketSubsidization
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/backend/models/bets.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Bet struct {
10 | gorm.Model
11 | Action string `json:"action"`
12 | ID uint `json:"id" gorm:"primary_key"`
13 | Username string `json:"username"`
14 | User User `gorm:"foreignKey:Username;references:Username"`
15 | MarketID uint `json:"marketId"`
16 | Market Market `gorm:"foreignKey:ID;references:MarketID"`
17 | Amount int64 `json:"amount"`
18 | PlacedAt time.Time `json:"placedAt"`
19 | Outcome string `json:"outcome,omitempty"`
20 | }
21 |
22 | type Bets []Bet
23 |
24 | // getMarketUsers returns the number of unique users for a given market
25 | func GetNumMarketUsers(bets []Bet) int {
26 | userMap := make(map[string]bool)
27 | for _, bet := range bets {
28 | userMap[bet.Username] = true
29 | }
30 |
31 | return len(userMap)
32 | }
33 |
34 | func CreateBet(username string, marketID uint, amount int64, outcome string) Bet {
35 | return Bet{
36 | Username: username,
37 | MarketID: marketID,
38 | Amount: amount,
39 | PlacedAt: time.Now(),
40 | Outcome: outcome,
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/lib/arch.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # scripts/lib/arch.sh
3 | set -euo pipefail
4 |
5 | # Detect host architecture
6 | HOST_ARCH="$(uname -m || true)"
7 |
8 | is_arm() {
9 | case "${HOST_ARCH}" in
10 | arm64|aarch64) return 0 ;;
11 | *) return 1 ;;
12 | esac
13 | }
14 |
15 | # Normalize env defaults (in case .env didn’t define them)
16 | : "${BUILD_NATIVE_ARM64:=false}"
17 | : "${FORCE_PLATFORM:=}"
18 |
19 | if is_arm; then
20 | # On Apple Silicon:
21 | if [ "${APP_ENV:-}" = "development" ]; then
22 | if [ "${BUILD_NATIVE_ARM64}" = "true" ]; then
23 | # We’ll build native images; do not force emulation.
24 | export FORCE_PLATFORM="linux/arm64"
25 | else
26 | # Default: run amd64 images under emulation for max compatibility
27 | export FORCE_PLATFORM="linux/amd64"
28 | export DOCKER_DEFAULT_PLATFORM="linux/amd64"
29 | fi
30 | else
31 | # localhost/prod pull from registry; prefer emulation unless registry is multi-arch
32 | if [ -z "${FORCE_PLATFORM}" ]; then
33 | export FORCE_PLATFORM="linux/amd64"
34 | export DOCKER_DEFAULT_PLATFORM="linux/amd64"
35 | fi
36 | fi
37 | fi
38 |
--------------------------------------------------------------------------------
/backend/models/market.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | type Market struct {
10 | gorm.Model
11 | ID int64 `json:"id" gorm:"primary_key"`
12 | QuestionTitle string `json:"questionTitle" gorm:"not null"`
13 | Description string `json:"description" gorm:"not null"`
14 | OutcomeType string `json:"outcomeType" gorm:"not null"`
15 | ResolutionDateTime time.Time `json:"resolutionDateTime" gorm:"not null"`
16 | FinalResolutionDateTime time.Time `json:"finalResolutionDateTime"`
17 | UTCOffset int `json:"utcOffset"`
18 | IsResolved bool `json:"isResolved"`
19 | ResolutionResult string `json:"resolutionResult"`
20 | InitialProbability float64 `json:"initialProbability" gorm:"not null"`
21 | YesLabel string `json:"yesLabel" gorm:"default:YES"`
22 | NoLabel string `json:"noLabel" gorm:"default:NO"`
23 | CreatorUsername string `json:"creatorUsername" gorm:"not null"`
24 | Creator User `gorm:"foreignKey:CreatorUsername;references:Username"`
25 | }
26 |
--------------------------------------------------------------------------------
/backend/middleware/authadmin.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "socialpredict/models"
8 |
9 | "github.com/golang-jwt/jwt/v4"
10 | "gorm.io/gorm"
11 | )
12 |
13 | // ValidateAdminToken checks if the authenticated user is an admin
14 | // It returns error if not an admin or if any validation fails
15 | func ValidateAdminToken(r *http.Request, db *gorm.DB) error {
16 | tokenString, err := extractTokenFromHeader(r)
17 | if err != nil {
18 | return err
19 | }
20 |
21 | keyFunc := func(token *jwt.Token) (interface{}, error) {
22 | return getJWTKey(), nil
23 | }
24 |
25 | token, err := parseToken(tokenString, keyFunc)
26 | if err != nil {
27 | return errors.New("invalid token")
28 | }
29 |
30 | if claims, ok := token.Claims.(*UserClaims); ok && token.Valid {
31 | var user models.User
32 | result := db.Where("username = ?", claims.Username).First(&user)
33 | if result.Error != nil {
34 | return fmt.Errorf("user not found")
35 | }
36 | if user.UserType != "ADMIN" {
37 | return fmt.Errorf("access denied for non-ADMIN users")
38 | }
39 |
40 | return nil
41 | }
42 |
43 | return errors.New("invalid token")
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/components/modals/resolution/ResolveUtils.jsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../../../config';
2 |
3 | export const resolveMarket = (marketId, token, selectedResolution) => {
4 | const resolutionData = {
5 | outcome: selectedResolution,
6 | };
7 |
8 | const requestOptions = {
9 | method: 'POST',
10 | headers: {
11 | 'Content-Type': 'application/json',
12 | Authorization: `Bearer ${token}`,
13 | },
14 | body: JSON.stringify(resolutionData),
15 | };
16 |
17 | // Returning fetch promise to allow handling of the response in the component
18 | return fetch(`${API_URL}/v0/resolve/${marketId}`, requestOptions)
19 | .then(response => {
20 | if (!response.ok) {
21 | throw new Error('Network response was not ok');
22 | }
23 | return response.json();
24 | })
25 | .then(data => {
26 | console.log('Market resolved successfully:', data);
27 | return data;
28 | })
29 | .catch(error => {
30 | console.error('Error resolving market:', error);
31 | throw error;
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/backend/handlers/users/apply_transaction.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "fmt"
5 | "socialpredict/models"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | const (
11 | TransactionWin = "WIN"
12 | TransactionRefund = "REFUND"
13 | TransactionSale = "SALE"
14 | TransactionBuy = "BUY"
15 | TransactionFee = "FEE"
16 | )
17 |
18 | // ApplyTransactionToUser credits the user's balance for a specific transaction type (WIN, REFUND, etc.)
19 | func ApplyTransactionToUser(username string, amount int64, db *gorm.DB, transactionType string) error {
20 | var user models.User
21 |
22 | if err := db.Where("username = ?", username).First(&user).Error; err != nil {
23 | return fmt.Errorf("user lookup failed: %w", err)
24 | }
25 |
26 | switch transactionType {
27 | case TransactionWin, TransactionRefund, TransactionSale:
28 | user.AccountBalance += amount
29 | case TransactionBuy, TransactionFee:
30 | user.AccountBalance -= amount
31 | default:
32 | return fmt.Errorf("unknown transaction type: %s", transactionType)
33 | }
34 |
35 | if err := db.Save(&user).Error; err != nil {
36 | return fmt.Errorf("failed to update user balance: %w", err)
37 | }
38 |
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/README/README-CONFIG.md:
--------------------------------------------------------------------------------
1 | # Economics Configuration
2 |
3 | * The economics of SocialPredict can be customized upon set up based upon entries in a YAML file.
4 | * Elements such as the cost to create a market, what the initial probability of a market is set to, how much a market is subsidized by the computer, trader bonuses and so on can be set up within this file.
5 | * These are global variables which effect the operation of the entire software suite and for now are meant to be set up permanently for the entirety of the run of a particular instance of SocialPredict.
6 |
7 | ```
8 | economics:
9 | marketcreation:
10 | initialMarketProbability: 0.5
11 | initialMarketSubsidization: 10
12 | initialMarketYes: 0
13 | initialMarketNo: 0
14 | marketincentives:
15 | createMarketCost: 1
16 | traderBonus: 2
17 | user:
18 | initialAccountBalance: 0
19 | maximumDebtAllowed: 500
20 | betting:
21 | minimumBet: 1
22 | betFee: 0
23 | sellSharesFee: 0
24 | ```
25 |
26 | * We may implement variable economics in the future, however this might need to come along with transparency metrics, which show how the economics were changed to users, which requires another level of data table to be added.
--------------------------------------------------------------------------------
/frontend/src/components/utils/userFinanceTools/FetchUserCredit.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { API_URL } from '../../../config';
3 |
4 | const useUserCredit = (username) => {
5 | const [userCredit, setUserCredit] = useState(null);
6 | const [loading, setLoading] = useState(true); // Make sure this state is named correctly
7 | const [error, setError] = useState(null);
8 |
9 | useEffect(() => {
10 | const fetchCredit = async () => {
11 | try {
12 | const response = await fetch(`${API_URL}/v0/usercredit/${username}`);
13 | if (!response.ok) {
14 | throw new Error('Failed to fetch user credit');
15 | }
16 | const data = await response.json();;
17 | setUserCredit(data.credit);
18 | } catch (error) {
19 | console.error('Error fetching user credit:', error);
20 | setError(error.toString());
21 | } finally {
22 | setLoading(false);
23 | }
24 | };
25 |
26 | if (username) {
27 | fetchCredit();
28 | }
29 | }, [username]);
30 |
31 | return { userCredit, loading, error };
32 | };
33 |
34 | export default useUserCredit;
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool
15 | *.out
16 | backend/**/cover.html
17 |
18 | # Dependency directories (remove the comment below to include it)
19 | # vendor/
20 |
21 | # Go workspace file
22 | go.work
23 |
24 | # Mac Stuff
25 | .DS_Store
26 |
27 | # Env File
28 | .env
29 | .env-e
30 |
31 | # Data files
32 | **/data/certbot
33 | **/data/postgres
34 | **/data/traefik/logs
35 | **/data/traefik/config/acme.json
36 | **/data/traefik/config/traefik.yaml
37 |
38 | .first_run
39 |
40 | # Keys
41 | .playground_key_pointer
42 |
43 | # Local temporary files
44 | tmp/
45 |
46 | # JetBrains files
47 | /.idea
48 |
49 | # Frontend Files that are generated from their template
50 | /frontend/src/config.js
51 | /frontend/vite.config.mjs
52 | /frontend/public/env-config.js
53 |
54 | /backend/.air.toml
55 |
56 | # Apple Silicon Docker Compose Override
57 | docker-compose.override.yml
58 |
--------------------------------------------------------------------------------
/backend/util/modelchecks.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "socialpredict/models"
7 |
8 | "github.com/brianvoe/gofakeit"
9 | "gorm.io/gorm"
10 | )
11 |
12 | func CheckUserIsReal(db *gorm.DB, username string) error {
13 | var user models.User
14 | result := db.Where("username = ?", username).First(&user)
15 | if result.Error != nil {
16 | if errors.Is(result.Error, gorm.ErrRecordNotFound) {
17 | return errors.New("creator user not found")
18 | }
19 | return result.Error
20 | }
21 | return nil
22 | }
23 |
24 | func CountByField(db *gorm.DB, field, value string) int64 {
25 | var count int64
26 | db.Model(&models.User{}).Where(fmt.Sprintf("%s = ?", field), value).Count(&count)
27 | return count
28 | }
29 |
30 | func UniqueDisplayName(db *gorm.DB) string {
31 | // Generate display name and check for uniqueness
32 | for {
33 | name := gofakeit.Name()
34 | if count := CountByField(db, "display_name", name); count == 0 {
35 | return name
36 | }
37 | }
38 | }
39 |
40 | func UniqueEmail(db *gorm.DB) string {
41 | // Generate email and check for uniqueness
42 | for {
43 | email := gofakeit.Email()
44 | if count := CountByField(db, "email", email); count == 0 {
45 | return email
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/README/TESTING/stress_testing/docker_stats.csv:
--------------------------------------------------------------------------------
1 | Timestamp,Container ID,Name,CPU %,Mem Usage (MiB),Mem Limit (GiB),Mem %,Net I/O Received (KB),Net I/O Transmitted (KB),Block I/O Read (KB),Block I/O Write (KB),PIDs
2 | 2024-05-31 09:57:23,2bb3bf37755b,socialpredict-nginx-dev,0.00%,0.00859082,0.00950195,0.09%,0.00113281,0,0.00874023,0.00400391,3
3 | 2024-05-31 09:57:23,1dd9948512e1,socialpredict-frontend-dev,0.22%,0.0897949,0.00950195,0.92%,0.316406,0.0327148,0.0182617,0.00700195,41
4 | 2024-05-31 09:57:23,eb6549a97489,socialpredict-backend-container-dev,0.00%,0.0512207,0.00950195,0.53%,0.0132812,0.0132812,0.0625977,0.224609,8
5 | 2024-05-31 09:57:23,fc3a4bfdef5a,db-postgres-dev,0.01%,0.0453027,0.00950195,0.47%,0.0147461,0.0119141,0.0173828,0.0538086,7
6 | 2024-05-31 09:57:36,2bb3bf37755b,socialpredict-nginx-dev,0.00%,0.00859082,0.00950195,0.09%,0.00113281,0,0.00874023,0.00400391,3
7 | 2024-05-31 09:57:36,1dd9948512e1,socialpredict-frontend-dev,0.32%,0.089834,0.00950195,0.92%,0.316406,0.0327148,0.0182617,0.00700195,41
8 | 2024-05-31 09:57:36,eb6549a97489,socialpredict-backend-container-dev,0.00%,0.0512207,0.00950195,0.53%,0.0132812,0.0132812,0.0625977,0.224609,8
9 | 2024-05-31 09:57:37,fc3a4bfdef5a,db-postgres-dev,0.01%,0.0453027,0.00950195,0.47%,0.0148437,0.0119141,0.0173828,0.0538086,7
10 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/ExpandableLink.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const ExpandableLink = ({
5 | text,
6 | maxLength = 50,
7 | to,
8 | className = '',
9 | buttonClassName = '',
10 | expandIcon = '📐',
11 | linkClassName = ''
12 | }) => {
13 | const [isExpanded, setIsExpanded] = useState(false);
14 |
15 | if (!text || text.length <= maxLength) {
16 | return (
17 |
18 | {text}
19 |
20 | );
21 | }
22 |
23 | const truncatedText = text.slice(0, maxLength) + '...';
24 |
25 | return (
26 |
27 |
28 | {isExpanded ? text : truncatedText}
29 |
30 | {
32 | e.preventDefault();
33 | e.stopPropagation();
34 | setIsExpanded(!isExpanded);
35 | }}
36 | className={`inline-block ${buttonClassName}`}
37 | title={isExpanded ? 'Collapse' : 'Expand'}
38 | type="button"
39 | >
40 | {expandIcon} {isExpanded ? 'Less' : 'More'}
41 |
42 |
43 | );
44 | };
45 |
46 | export default ExpandableLink;
47 |
--------------------------------------------------------------------------------
/backend/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "socialpredict/middleware"
8 | "socialpredict/migration"
9 | _ "socialpredict/migration/migrations" // <-- side-effect import: registers migrations via init()
10 | "socialpredict/seed"
11 | "socialpredict/server"
12 | "socialpredict/util"
13 | )
14 |
15 | func main() {
16 | // Secure endpoint example
17 | http.Handle("/secure", middleware.Authenticate(http.HandlerFunc(secureEndpoint)))
18 |
19 | // Load env (.env, .env.dev)
20 | if err := util.GetEnv(); err != nil {
21 | log.Printf("env: warning loading environment: %v", err)
22 | }
23 |
24 | util.InitDB()
25 | db := util.GetDB()
26 |
27 | const MAX_ATTEMPTS = 20
28 | if err := seed.EnsureDBReady(db, MAX_ATTEMPTS); err != nil {
29 | log.Fatalf("database readiness check failed: %v", err)
30 | }
31 |
32 | if err := migration.MigrateDB(db); err != nil {
33 | log.Printf("migration: warning: %v", err)
34 | }
35 |
36 | seed.SeedUsers(db)
37 | if err := seed.SeedHomepage(db, "."); err != nil {
38 | log.Printf("seed homepage: warning: %v", err)
39 | }
40 |
41 | server.Start()
42 | }
43 |
44 | func secureEndpoint(w http.ResponseWriter, r *http.Request) {
45 | // This is a secure endpoint, only accessible if Authenticate middleware passes
46 | }
47 |
--------------------------------------------------------------------------------
/backend/handlers/bets/selling/sellpositionhandler.go:
--------------------------------------------------------------------------------
1 | package sellbetshandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/middleware"
7 | "socialpredict/models"
8 | "socialpredict/setup"
9 | "socialpredict/util"
10 | )
11 |
12 | func SellPositionHandler(loadEconConfig setup.EconConfigLoader) func(w http.ResponseWriter, r *http.Request) {
13 | return func(w http.ResponseWriter, r *http.Request) {
14 | db := util.GetDB()
15 | user, httperr := middleware.ValidateUserAndEnforcePasswordChangeGetUser(r, db)
16 | if httperr != nil {
17 | http.Error(w, httperr.Error(), httperr.StatusCode)
18 | return
19 | }
20 |
21 | var redeemRequest models.Bet
22 | err := json.NewDecoder(r.Body).Decode(&redeemRequest)
23 | if err != nil {
24 | http.Error(w, err.Error(), http.StatusBadRequest)
25 | return
26 | }
27 |
28 | // Load economic configuration
29 | cfg := loadEconConfig()
30 | if cfg == nil {
31 | http.Error(w, "failed to load economic configuration", http.StatusInternalServerError)
32 | return
33 | }
34 |
35 | if err := ProcessSellRequest(db, &redeemRequest, user, cfg); err != nil {
36 | http.Error(w, err.Error(), http.StatusBadRequest)
37 | return
38 | }
39 |
40 | w.WriteHeader(http.StatusCreated)
41 | json.NewEncoder(w).Encode(redeemRequest)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/components/modals/bet/BetUtils.jsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../../../config';
2 |
3 | export const submitBet = (betData, token, onSuccess, onError) => {
4 |
5 | if (!token) {
6 | alert('Please log in to place a bet.');
7 | return;
8 | }
9 |
10 | console.log('Sending bet data:', betData);
11 |
12 | fetch(`${API_URL}/v0/bet`, {
13 | method: 'POST',
14 | headers: {
15 | 'Content-Type': 'application/json',
16 | Authorization: `Bearer ${token}`,
17 | },
18 | body: JSON.stringify(betData),
19 | })
20 | .then(response => {
21 | if (!response.ok) {
22 | return response.json().then(err => {
23 | // Construct a new error object and throw it
24 | throw new Error(`Error placing bet: ${err.error || 'Unknown error'}`);
25 | });
26 | }
27 | return response.json();
28 | })
29 | .then(data => {
30 | console.log('Success:', data);
31 | onSuccess(data); // Handle success outside this utility function
32 | })
33 | .catch(error => {
34 | console.error('Error:', error);
35 | alert(error.message); // Use error.message to display the custom error message
36 | onError(error); // Handle error outside this utility function
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/src/pages/marketDetails/MarketDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MarketDetailsTable from '../../components/marketDetails/MarketDetailsLayout';
3 | import { useMarketDetails } from '../../hooks/useMarketDetails';
4 | import { useAuth } from '../../helpers/AuthContent';
5 | import LoadingSpinner from '../../components/loaders/LoadingSpinner';
6 |
7 | const MarketDetails = () => {
8 | const { username } = useAuth();
9 | const { details, isLoggedIn, token, refetchData, currentProbability } =
10 | useMarketDetails();
11 |
12 | if (!details) {
13 | return ;
14 | }
15 |
16 | return (
17 |
35 | );
36 | };
37 |
38 | export default MarketDetails;
39 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Set the environment to either 'development', 'localhost' or 'production'
2 | APP_ENV=production
3 |
4 | # Apple Silicon support knobs
5 | # - If true on arm64: build native arm64 images locally in development
6 | # - If false on arm64: use amd64 images under emulation (default safer)
7 | BUILD_NATIVE_ARM64=false
8 | # Force a docker platform for pulls/builds (auto-set by arch.sh; leave blank here)
9 | FORCE_PLATFORM=
10 |
11 | ADMIN_PASSWORD='password'
12 |
13 | BACKEND_CONTAINER_NAME=socialpredict-backend-container
14 | BACKEND_IMAGE_NAME=ghcr.io/openpredictionmarkets/socialpredict-backend
15 | # External Backend Port
16 | BACKEND_PORT=8080
17 |
18 | POSTGRES_CONTAINER_NAME=socialpredict-postgres-container
19 | DB_HOST=db
20 | # External Postgres Port
21 | DB_PORT=5432
22 | POSTGRES_USER='user'
23 | POSTGRES_PASSWORD='password'
24 | POSTGRES_DATABASE='socialpredict_db'
25 |
26 | FRONTEND_CONTAINER_NAME=socialpredict-frontend-container
27 | FRONTEND_IMAGE_NAME=ghcr.io/openpredictionmarkets/socialpredict-frontend
28 | # External Frontend Port
29 | FRONTEND_PORT=5173
30 |
31 | NGINX_IMAGE_NAME=socialpredict-nginx
32 | NGINX_CONTAINER_NAME=socialpredict-nginx-container
33 | # External Nginx Port
34 | NGINX_PORT=80
35 |
36 | # Domain name you wish to use
37 | DOMAIN='domain.com'
38 | DOMAIN_URL='domain.com'
39 | API_URL='domain.com'
40 | EMAIL='john@doe.com'
41 |
42 | TRAEFIK_CONTAINER_NAME=socialpredict-traefik-container
43 |
--------------------------------------------------------------------------------
/backend/models/modelstesting/users_helpers.go:
--------------------------------------------------------------------------------
1 | package modelstesting
2 |
3 | import (
4 | "socialpredict/models"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // AdjustUserBalance applies a delta to the specified user's account balance in a transactional manner.
10 | func AdjustUserBalance(db *gorm.DB, username string, delta int64) error {
11 | return db.Transaction(func(tx *gorm.DB) error {
12 | var user models.User
13 | if err := tx.Where("username = ?", username).First(&user).Error; err != nil {
14 | return err
15 | }
16 | user.AccountBalance += delta
17 | return tx.Save(&user).Error
18 | })
19 | }
20 |
21 | // SumAllUserBalances returns the aggregate account balance across all users in the database.
22 | func SumAllUserBalances(db *gorm.DB) (int64, error) {
23 | var users []models.User
24 | if err := db.Find(&users).Error; err != nil {
25 | return 0, err
26 | }
27 |
28 | var total int64
29 | for _, user := range users {
30 | total += user.AccountBalance
31 | }
32 | return total, nil
33 | }
34 |
35 | // LoadUserBalances returns a map of username to account balance for every user in the database.
36 | func LoadUserBalances(db *gorm.DB) (map[string]int64, error) {
37 | var users []models.User
38 | if err := db.Find(&users).Error; err != nil {
39 | return nil, err
40 | }
41 |
42 | result := make(map[string]int64, len(users))
43 | for _, user := range users {
44 | result[user.Username] = user.AccountBalance
45 | }
46 | return result, nil
47 | }
48 |
--------------------------------------------------------------------------------
/backend/handlers/tradingdata/getbets_test.go:
--------------------------------------------------------------------------------
1 | package tradingdata
2 |
3 | import (
4 | "socialpredict/models"
5 | "socialpredict/models/modelstesting"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestGetBetsForMarket(t *testing.T) {
11 | // Set up in-memory SQLite database
12 | db := modelstesting.NewFakeDB(t)
13 |
14 | // Auto-migrate the Bet model
15 | if err := db.AutoMigrate(&models.Bet{}); err != nil {
16 | t.Fatalf("Failed to auto-migrate Bet model: %v", err)
17 | }
18 |
19 | // Create some test data
20 | bets := []models.Bet{
21 | {Username: "user1", MarketID: 1, Amount: 100, PlacedAt: time.Now(), Outcome: "YES"},
22 | {Username: "user2", MarketID: 1, Amount: 200, PlacedAt: time.Now(), Outcome: "NO"},
23 | {Username: "user3", MarketID: 2, Amount: 150, PlacedAt: time.Now(), Outcome: "YES"},
24 | }
25 | if err := db.Create(&bets).Error; err != nil {
26 | t.Fatalf("Failed to create bets: %v", err)
27 | }
28 |
29 | // Test the function
30 | retrievedBets := GetBetsForMarket(db, 1)
31 |
32 | // Verify the number of bets retrieved
33 | if got, want := len(retrievedBets), 2; got != want {
34 | t.Errorf("GetBetsForMarket(db, 1) = %d bets, want %d bets", got, want)
35 | }
36 |
37 | // Check if the returned bets match the expected ones
38 | for _, bet := range retrievedBets {
39 | if got, want := int(bet.MarketID), 1; got != want {
40 | t.Errorf("GetBetsForMarket(db, 1) - retrieved bet with MarketID = %d, want %d", got, want)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/backend/migration/migrations/20251020_140500_add_market_labels.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "socialpredict/migration"
5 | "socialpredict/models"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | // migrateAddMarketLabels contains the core logic so we can unit test it directly.
11 | func MigrateAddMarketLabels(db *gorm.DB) error {
12 | m := db.Migrator()
13 |
14 | // 1) Add columns if missing (snake_cased by GORM -> yes_label / no_label)
15 | if !m.HasColumn(&models.Market{}, "YesLabel") {
16 | if err := m.AddColumn(&models.Market{}, "YesLabel"); err != nil {
17 | return err
18 | }
19 | }
20 | if !m.HasColumn(&models.Market{}, "NoLabel") {
21 | if err := m.AddColumn(&models.Market{}, "NoLabel"); err != nil {
22 | return err
23 | }
24 | }
25 |
26 | // 2) Backfill defaults for existing rows (empty or NULL -> "YES"/"NO")
27 | if err := db.Model(&models.Market{}).
28 | Where("yes_label IS NULL OR yes_label = ''").
29 | Update("yes_label", "YES").Error; err != nil {
30 | return err
31 | }
32 | if err := db.Model(&models.Market{}).
33 | Where("no_label IS NULL OR no_label = ''").
34 | Update("no_label", "NO").Error; err != nil {
35 | return err
36 | }
37 |
38 | return nil
39 | }
40 |
41 | // Register the migration with a timestamp. Adjust the timestamp if you need strict ordering vs. other migrations.
42 | func init() {
43 | migration.Register("20251020140500", func(db *gorm.DB) error {
44 | return MigrateAddMarketLabels(db)
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/scripts/dev/env_writer_dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Make sure the script can only be run via SocialPredict Script
4 | [ -z "$CALLED_FROM_SOCIALPREDICT" ] && { echo "Not called from SocialPredict"; exit 42; }
5 |
6 | # Function to create and update .env file
7 | # Updated to be compatible with MacOS Sonoma.
8 | # Uses POSTGRES_VOLUME to deal with MacOS xattrs provenence.
9 | init_env() {
10 | # Create .env file
11 | cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
12 |
13 | # Update APP_ENV
14 | sed -i -e "s/APP_ENV=.*/APP_ENV='development'/g" "$SCRIPT_DIR/.env"
15 | echo "ENV_PATH=$SCRIPT_DIR/.env" >> "$SCRIPT_DIR/.env"
16 |
17 | # Add OS-specific POSTGRES_VOLUME
18 | OS=$(uname -s)
19 | if [[ "$OS" == "Darwin" ]]; then
20 | echo "POSTGRES_VOLUME=pgdata:/var/lib/postgresql/data" >> "$SCRIPT_DIR/.env"
21 | else
22 | echo "POSTGRES_VOLUME=../data/postgres:/var/lib/postgresql/data" >> "$SCRIPT_DIR/.env"
23 | fi
24 | }
25 |
26 | if [[ ! -f "$SCRIPT_DIR/.env" ]]; then
27 | echo "### First time running the script ..."
28 | echo "Let's initialize the application ..."
29 | sleep 1
30 | init_env
31 | echo "Application initialized successfully."
32 | else
33 | read -p ".env file found. Do you want to re-create it? (y/N) " DECISION
34 | if [ "$DECISION" != "Y" ] && [ "$DECISION" != "y" ]; then
35 | :
36 | else
37 | sleep 1
38 | echo "Re-creating env file ..."
39 | sleep 1
40 | init_env
41 | echo ".env file re-created successfully."
42 | fi
43 | fi
44 |
45 | echo
46 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/marketDetails/ActivityButtons.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { buttonBaseStyle } from '../BaseButton';
3 |
4 | const ActivityModal = ({ activeTab, changeTab, bets, toggleModal }) => {
5 | return (
6 |
7 |
8 |
9 | changeTab('Comments')} className={activeTab === 'Comments' ? 'active' : ''}>Comments
10 | changeTab('Bets')} className={activeTab === 'Bets' ? 'active' : ''}>Bets
11 | changeTab('Positions')} className={activeTab === 'Positions' ? 'active' : ''}>Positions
12 |
13 | {activeTab === 'Bets' && (
14 |
15 | {bets.map(bet => {bet.description} )}
16 |
17 | )}
18 | {/* Implement similar structures for Comments and Positions */}
19 |
✕
20 |
21 |
22 | );
23 | };
24 |
25 | export default ActivityModal;
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/ExpandableText.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const ExpandableText = ({
4 | text,
5 | maxLength = 50,
6 | className = '',
7 | expandedClassName = 'mt-2 p-2 bg-gray-700 rounded border border-gray-600',
8 | buttonClassName = 'text-xs text-blue-400 hover:text-blue-300 transition-colors ml-1',
9 | showFullTextInExpanded = true,
10 | expandIcon = '📐'
11 | }) => {
12 | const [isExpanded, setIsExpanded] = useState(false);
13 |
14 | // If text is shorter than maxLength, no need for expansion
15 | if (text.length <= maxLength) {
16 | return {text} ;
17 | }
18 |
19 | const truncatedText = text.slice(0, maxLength) + '...';
20 |
21 | return (
22 |
23 | {isExpanded ? text : truncatedText}
24 | {
26 | e.preventDefault(); // Prevent navigation if inside a Link
27 | e.stopPropagation(); // Prevent event bubbling
28 | setIsExpanded(!isExpanded);
29 | }}
30 | className={buttonClassName}
31 | title={isExpanded ? "Show less" : "Show full text"}
32 | >
33 | {expandIcon}
34 |
35 | {isExpanded && showFullTextInExpanded && (
36 |
37 | {text}
38 |
39 | )}
40 |
41 | );
42 | };
43 |
44 | export default ExpandableText;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/utils/dateTimeTools/FormDateTimeTools.jsx:
--------------------------------------------------------------------------------
1 | export function getEndofDayDateTime() {
2 | const now = new Date();
3 | now.setHours(23, 59); // Set time to 11:59 PM
4 | // Format for datetime-local input, which requires 'YYYY-MM-DDTHH:MM'
5 | return now.toISOString().slice(0, 16);
6 | };
7 |
8 | // format the time for the main display grid, showing resolution time
9 | export function formatDateTimeForGrid(dateTimeString) {
10 | const date = new Date(dateTimeString);
11 |
12 | // Check if there are any seconds
13 | if (date.getSeconds() > 0) {
14 | // Add one minute
15 | date.setMinutes(date.getMinutes() + 1);
16 | // Reset seconds to zero
17 | date.setSeconds(0);
18 | }
19 |
20 | // Extracting date components
21 | const year = date.getFullYear();
22 | const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Months are 0-based
23 | const day = date.getDate().toString().padStart(2, '0');
24 |
25 | // Convert 24-hour time to 12-hour time and determine AM/PM
26 | let hour = date.getHours();
27 | const amPm = hour >= 12 ? 'PM' : 'AM';
28 | hour = hour % 12;
29 | hour = hour ? hour : 12; // the hour '0' should be '12'
30 | const formattedHour = hour.toString().padStart(2, '0');
31 |
32 | const minute = date.getMinutes().toString().padStart(2, '0');
33 |
34 | // Format to YYYY.MM.DD and append time with AM/PM
35 | return `${year}.${month}.${day} ${formattedHour}:${minute} ${amPm}`;
36 | }
--------------------------------------------------------------------------------
/backend/setup/setuptesting/setuptesting_test.go:
--------------------------------------------------------------------------------
1 | package setuptesting
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestBuildInitialMarketAppConfig(t *testing.T) {
8 | cfg := BuildInitialMarketAppConfig(t, 0.7, 15, 5, 3)
9 |
10 | if cfg.Economics.MarketCreation.InitialMarketProbability != 0.7 {
11 | t.Fatalf("expected probability 0.7, got %f", cfg.Economics.MarketCreation.InitialMarketProbability)
12 | }
13 | if cfg.Economics.MarketCreation.InitialMarketSubsidization != 15 {
14 | t.Fatalf("expected subsidization 15, got %d", cfg.Economics.MarketCreation.InitialMarketSubsidization)
15 | }
16 | if cfg.Economics.MarketCreation.InitialMarketYes != 5 {
17 | t.Fatalf("expected initial yes 5, got %d", cfg.Economics.MarketCreation.InitialMarketYes)
18 | }
19 | if cfg.Economics.MarketCreation.InitialMarketNo != 3 {
20 | t.Fatalf("expected initial no 3, got %d", cfg.Economics.MarketCreation.InitialMarketNo)
21 | }
22 | }
23 |
24 | func TestMockEconomicConfig(t *testing.T) {
25 | cfg := MockEconomicConfig()
26 |
27 | if cfg.Economics.User.MaximumDebtAllowed != 500 {
28 | t.Fatalf("expected max debt 500, got %d", cfg.Economics.User.MaximumDebtAllowed)
29 | }
30 |
31 | if cfg.Economics.MarketIncentives.CreateMarketCost != 10 {
32 | t.Fatalf("expected create market cost 10, got %d", cfg.Economics.MarketIncentives.CreateMarketCost)
33 | }
34 |
35 | if cfg.Economics.Betting.BetFees.InitialBetFee != 1 {
36 | t.Fatalf("expected initial bet fee 1, got %d", cfg.Economics.Betting.BetFees.InitialBetFee)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/hooks/usePortfolio.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { API_URL } from '../config';
3 | import { useAuth } from '../helpers/AuthContent';
4 |
5 | const usePortfolio = (username) => {
6 | const [portfolio, setPortfolio] = useState({ portfolioItems: [] });
7 | const [portfolioLoading, setPortfolioLoading] = useState(true);
8 | const [portfolioError, setPortfolioError] = useState(null);
9 | const { token } = useAuth();
10 |
11 | useEffect(() => {
12 | const fetchPortfolio = async () => {
13 | try {
14 | const headers = {};
15 | if (token) {
16 | headers['Authorization'] = `Bearer ${token}`;
17 | headers['Content-Type'] = 'application/json';
18 | }
19 |
20 | const response = await fetch(`${API_URL}/api/v0/portfolio/${username}`, { headers });
21 | if (!response.ok) {
22 | throw new Error('Failed to fetch portfolio');
23 | }
24 | const data = await response.json();
25 | setPortfolio({ ...data, portfolioItems: data.portfolioItems || [] });
26 | } catch (error) {
27 | console.error('Error fetching portfolio:', error);
28 | setPortfolioError(error.toString());
29 | } finally {
30 | setPortfolioLoading(false);
31 | }
32 | };
33 |
34 | if (username && token) {
35 | fetchPortfolio();
36 | }
37 | }, [username, token]);
38 |
39 | return { portfolio, portfolioLoading, portfolioError };
40 | };
41 |
42 | export default usePortfolio;
43 |
--------------------------------------------------------------------------------
/backend/setup/setuptesting/setuptesting.go:
--------------------------------------------------------------------------------
1 | // package setuptesting provides testing helpers and support for testing doubles
2 | package setuptesting
3 |
4 | import (
5 | "testing"
6 |
7 | "socialpredict/setup"
8 | )
9 |
10 | // BuildInitialMarketAppConfig builds the MarketCreation portion of the app config for use in tests that require an EconomicConfig
11 | func BuildInitialMarketAppConfig(t *testing.T, probability float64, subsidization, yes, no int64) *setup.EconomicConfig {
12 | t.Helper()
13 | return &setup.EconomicConfig{
14 | Economics: setup.Economics{
15 | MarketCreation: setup.MarketCreation{
16 | InitialMarketProbability: probability,
17 | InitialMarketSubsidization: subsidization,
18 | InitialMarketYes: yes,
19 | InitialMarketNo: no,
20 | },
21 | },
22 | }
23 | }
24 |
25 | func MockEconomicConfig() *setup.EconomicConfig {
26 | return &setup.EconomicConfig{
27 | Economics: setup.Economics{
28 | MarketCreation: setup.MarketCreation{
29 | InitialMarketProbability: 0.5,
30 | InitialMarketSubsidization: 10,
31 | InitialMarketYes: 0,
32 | InitialMarketNo: 0,
33 | },
34 | MarketIncentives: setup.MarketIncentives{
35 | CreateMarketCost: 10,
36 | TraderBonus: 1,
37 | },
38 | User: setup.User{
39 | InitialAccountBalance: 1000,
40 | MaximumDebtAllowed: 500,
41 | },
42 | Betting: setup.Betting{
43 | MinimumBet: 1,
44 | BetFees: setup.BetFees{
45 | InitialBetFee: 1,
46 | BuySharesFee: 1,
47 | SellSharesFee: 0,
48 | },
49 | },
50 | },
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/backend/handlers/metrics/getsystemmetrics_test.go:
--------------------------------------------------------------------------------
1 | package metricshandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "socialpredict/models/modelstesting"
9 | "socialpredict/util"
10 | )
11 |
12 | func TestGetSystemMetricsHandler_Success(t *testing.T) {
13 | db := modelstesting.NewFakeDB(t)
14 | orig := util.DB
15 | util.DB = db
16 | t.Cleanup(func() {
17 | util.DB = orig
18 | })
19 |
20 | _, _ = modelstesting.UseStandardTestEconomics(t)
21 |
22 | user := modelstesting.GenerateUser("alice", 0)
23 | if err := db.Create(&user).Error; err != nil {
24 | t.Fatalf("create user: %v", err)
25 | }
26 |
27 | req := httptest.NewRequest("GET", "/v0/system/metrics", nil)
28 | rec := httptest.NewRecorder()
29 |
30 | GetSystemMetricsHandler(rec, req)
31 |
32 | if rec.Code != 200 {
33 | t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
34 | }
35 |
36 | var payload map[string]interface{}
37 | if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
38 | t.Fatalf("unmarshal response: %v", err)
39 | }
40 |
41 | if payload["moneyCreated"] == nil {
42 | t.Fatalf("expected moneyCreated section in response: %+v", payload)
43 | }
44 | }
45 |
46 | func TestGetSystemMetricsHandler_Error(t *testing.T) {
47 | orig := util.DB
48 | util.DB = nil
49 | defer func() { util.DB = orig }()
50 |
51 | req := httptest.NewRequest("GET", "/v0/system/metrics", nil)
52 | rec := httptest.NewRecorder()
53 |
54 | GetSystemMetricsHandler(rec, req)
55 |
56 | if rec.Code != 500 {
57 | t.Fatalf("expected status 500, got %d", rec.Code)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/backend/util/postgres.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "gorm.io/driver/postgres"
9 | "gorm.io/gorm"
10 | )
11 |
12 | var DB *gorm.DB
13 | var err error
14 |
15 | // InitDB initializes the database connection.
16 | // It supports both canonical POSTGRES_* variables and legacy DB_* fallbacks.
17 | func InitDB() {
18 | dbHost := os.Getenv("DB_HOST")
19 | if dbHost == "" {
20 | dbHost = os.Getenv("DBHOST")
21 | }
22 |
23 | dbUser := os.Getenv("POSTGRES_USER")
24 | if dbUser == "" {
25 | dbUser = os.Getenv("DB_USER")
26 | }
27 |
28 | dbPassword := os.Getenv("POSTGRES_PASSWORD")
29 | if dbPassword == "" {
30 | dbPassword = os.Getenv("DB_PASS")
31 | }
32 | if dbPassword == "" {
33 | dbPassword = os.Getenv("DB_PASSWORD")
34 | }
35 |
36 | dbName := os.Getenv("POSTGRES_DATABASE")
37 | if dbName == "" {
38 | dbName = os.Getenv("POSTGRES_DB")
39 | }
40 | if dbName == "" {
41 | dbName = os.Getenv("DB_NAME")
42 | }
43 |
44 | dbPort := os.Getenv("POSTGRES_PORT")
45 | if dbPort == "" {
46 | dbPort = os.Getenv("DB_PORT")
47 | }
48 | if dbPort == "" {
49 | dbPort = "5432"
50 | }
51 |
52 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC",
53 | dbHost, dbUser, dbPassword, dbName, dbPort)
54 |
55 | DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
56 | if err != nil {
57 | log.Fatalf("Error opening database: %v", err)
58 | }
59 |
60 | log.Println("Successfully connected to the database.")
61 | }
62 |
63 | // GetDB returns the database connection
64 | func GetDB() *gorm.DB {
65 | return DB
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/src/utils/chartFormatter.js:
--------------------------------------------------------------------------------
1 | export const MIN_SIG_FIGS = 2;
2 | export const MAX_SIG_FIGS = 9;
3 | export const DEFAULT_SIG_FIGS = 4;
4 |
5 | export const clampSigFigs = (value) => {
6 | if (!Number.isFinite(value)) {
7 | return DEFAULT_SIG_FIGS;
8 | }
9 |
10 | if (value < MIN_SIG_FIGS) {
11 | return MIN_SIG_FIGS;
12 | }
13 |
14 | if (value > MAX_SIG_FIGS) {
15 | return MAX_SIG_FIGS;
16 | }
17 |
18 | return Math.round(value);
19 | };
20 |
21 | export const createFormatter = (sigFigs) => (rawValue) => {
22 | if (rawValue === null || rawValue === undefined) {
23 | return '';
24 | }
25 |
26 | const numericValue = Number(rawValue);
27 | if (!Number.isFinite(numericValue)) {
28 | return '';
29 | }
30 |
31 | return numericValue.toPrecision(sigFigs);
32 | };
33 |
34 | export const loadChartFormatter = async () => {
35 | let requestedSigFigs = DEFAULT_SIG_FIGS;
36 |
37 | try {
38 | const response = await fetch('/v0/setup/frontend', {
39 | headers: {
40 | 'Content-Type': 'application/json',
41 | },
42 | });
43 |
44 | if (response.ok) {
45 | const data = await response.json();
46 | requestedSigFigs = Number(data?.charts?.sigFigs ?? DEFAULT_SIG_FIGS);
47 | }
48 | } catch (error) {
49 | console.error('Failed to load chart formatter config', error);
50 | }
51 |
52 | const sigFigs = clampSigFigs(requestedSigFigs);
53 | return {
54 | sigFigs,
55 | format: createFormatter(sigFigs),
56 | };
57 | };
58 |
59 | export const SocialPredictChartTools = {
60 | clampSigFigs,
61 | createFormatter,
62 | loadChartFormatter,
63 | };
64 |
--------------------------------------------------------------------------------
/backend/handlers/users/credit/usercredit.go:
--------------------------------------------------------------------------------
1 | package usercredit
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 | "socialpredict/handlers/users/publicuser"
8 | "socialpredict/setup"
9 | "socialpredict/util"
10 |
11 | "github.com/gorilla/mux"
12 | "gorm.io/gorm"
13 | )
14 |
15 | // appConfig holds the loaded application configuration accessible within the package
16 | var appConfig *setup.EconomicConfig
17 |
18 | func init() {
19 | var err error
20 | appConfig, err = setup.LoadEconomicsConfig()
21 | if err != nil {
22 | log.Fatalf("Failed to load configuration: %v", err)
23 | }
24 | }
25 |
26 | type UserCredit struct {
27 | Credit int64 `json:"credit"`
28 | }
29 |
30 | // gets the user's available credits for display
31 | func GetUserCreditHandler(w http.ResponseWriter, r *http.Request) {
32 |
33 | if r.Method != http.MethodGet {
34 | http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
35 | return
36 | }
37 |
38 | vars := mux.Vars(r)
39 | username := vars["username"]
40 |
41 | db := util.GetDB()
42 |
43 | userCredit := calculateUserCredit(
44 | db,
45 | username,
46 | appConfig.Economics.User.MaximumDebtAllowed,
47 | )
48 |
49 | response := UserCredit{
50 | Credit: userCredit,
51 | }
52 |
53 | w.Header().Set("Content-Type", "application/json")
54 | json.NewEncoder(w).Encode(response)
55 |
56 | }
57 |
58 | func calculateUserCredit(db *gorm.DB, username string, maximumdebt int64) int64 {
59 |
60 | userPublicInfo := publicuser.GetPublicUserInfo(db, username)
61 |
62 | userCredit := maximumdebt + userPublicInfo.AccountBalance
63 |
64 | return int64(userCredit)
65 | }
66 |
--------------------------------------------------------------------------------
/README/MATH/README-MATH-POSITIONS.md:
--------------------------------------------------------------------------------
1 | ### Different Types of Positions
2 |
3 | * There are two different types of positions in our codebase, which might be confusing.
4 | * The positions are meant to seperate the core math from what get served to the outside world and combined with other math.
5 | * The idea is to keep the functions in the core math as pure and simple as possible, so that they can be combined together in a pipeline of functions that results in a final output.
6 |
7 | #### DBPMMarketPosition
8 |
9 | Purpose: Internal, math-only.
10 |
11 | Usage: Output of your core pipeline (including NetAggregateMarketPositions), and used for payout resolution logic.
12 |
13 | Fields: Only Username, YesSharesOwned, NoSharesOwned.
14 |
15 | #### MarketPosition
16 |
17 | Purpose: API-facing, informational.
18 |
19 | Usage: Takes the result of the internal pipeline (DBPMMarketPosition slice), augments it (for example, by calculating Value using current probability and market volume), and presents it to the frontend/consumer.
20 |
21 | Fields: Adds Value (mark-to-market estimate) for display, but does not affect core math.
22 |
23 | #### Flowchart of How Each Position Type Used
24 |
25 | ```
26 | [Raw Bets/Market]
27 | |
28 | V
29 | [ Core math functions: Aggregate, Normalize, etc. ]
30 | |
31 | V
32 | []dbpm.DBPMMarketPosition <-- (internal math output, result of last in a pipeline of pure functions)
33 | |
34 | V
35 | []positions.MarketPosition <-- (Run all of core functions and enrich []dbpm.DBPMMarketPosition, adding more information)
36 | |
37 | V
38 | [ API response layer: append Value, format for HTTP ]
39 |
40 | ```
--------------------------------------------------------------------------------
/frontend/src/components/modals/login/LoginModalClick.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import LoginModal from './LoginModal';
3 | import { useAuth } from '../../../helpers/AuthContent';
4 | import { useHistory } from 'react-router-dom';
5 | import { LoginSVG } from '../../../assets/components/SvgIcons';
6 |
7 | const LoginModalButton = ({ iconOnly = false }) => {
8 | const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
9 | const { login } = useAuth();
10 | const [redirectAfterLogin, setRedirectAfterLogin] = useState('/');
11 | const history = useHistory();
12 |
13 | const handleOpenModal = () => {
14 | setRedirectAfterLogin(history.location.pathname);
15 | setIsLoginModalOpen(true);
16 | };
17 |
18 | return (
19 | <>
20 |
26 |
31 | {!iconOnly && Login }
32 |
33 | {isLoginModalOpen && (
34 | setIsLoginModalOpen(false)}
37 | onLogin={login}
38 | redirectAfterLogin={redirectAfterLogin}
39 | />
40 | )}
41 | >
42 | );
43 | };
44 |
45 | export default LoginModalButton;
46 |
--------------------------------------------------------------------------------
/backend/handlers/positions/positionshandler.go:
--------------------------------------------------------------------------------
1 | package positions
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/errors"
7 | positionsmath "socialpredict/handlers/math/positions"
8 | "socialpredict/util"
9 |
10 | "github.com/gorilla/mux"
11 | )
12 |
13 | func MarketDBPMPositionsHandler(w http.ResponseWriter, r *http.Request) {
14 | vars := mux.Vars(r)
15 | marketIdStr := vars["marketId"]
16 |
17 | // open up database to utilize connection pooling
18 | db := util.GetDB()
19 |
20 | marketDBPMPositions, err := positionsmath.CalculateMarketPositions_WPAM_DBPM(db, marketIdStr)
21 | if errors.HandleHTTPError(w, err, http.StatusBadRequest, "Invalid request or data processing error.") {
22 | return // Stop execution if there was an error.
23 | }
24 |
25 | // Respond with the bets display information
26 | w.Header().Set("Content-Type", "application/json")
27 | json.NewEncoder(w).Encode(marketDBPMPositions)
28 | }
29 |
30 | func MarketDBPMUserPositionsHandler(w http.ResponseWriter, r *http.Request) {
31 | vars := mux.Vars(r)
32 | marketIdStr := vars["marketId"]
33 | userNameStr := vars["username"]
34 |
35 | // open up database to utilize connection pooling
36 | db := util.GetDB()
37 |
38 | marketDBPMPositions, err := positionsmath.CalculateMarketPositionForUser_WPAM_DBPM(db, marketIdStr, userNameStr)
39 | if errors.HandleHTTPError(w, err, http.StatusBadRequest, "Invalid request or data processing error.") {
40 | return // Stop execution if there was an error.
41 | }
42 |
43 | // Respond with the bets display information
44 | w.Header().Set("Content-Type", "application/json")
45 | json.NewEncoder(w).Encode(marketDBPMPositions)
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/src/components/modals/bet/BetModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { BetButton } from '../../buttons/trade/BetButtons';
3 | import TradeTabs from '../../tabs/TradeTabs';
4 | import { submitBet } from './BetUtils'
5 |
6 | const BetModalButton = ({ marketId, token, onTransactionSuccess }) => {
7 | const [showBetModal, setShowBetModal] = useState(false);
8 | const toggleBetModal = () => setShowBetModal(prev => !prev);
9 |
10 | const handleTransactionSuccess = () => {
11 | setShowBetModal(false); // Close modal
12 | if (onTransactionSuccess) {
13 | onTransactionSuccess(); // Trigger data refresh
14 | }
15 | };
16 |
17 | return (
18 |
19 |
20 | {showBetModal && (
21 |
22 |
23 |
24 |
29 |
30 |
31 | ✕
32 |
33 |
34 |
35 | )}
36 |
37 | );
38 | };
39 |
40 | export default BetModalButton;
41 |
--------------------------------------------------------------------------------
/backend/handlers/users/apply_transaction_test.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "testing"
5 |
6 | "socialpredict/models"
7 | "socialpredict/models/modelstesting"
8 | )
9 |
10 | func TestApplyTransactionToUser(t *testing.T) {
11 | db := modelstesting.NewFakeDB(t)
12 |
13 | startingBalance := int64(100)
14 | user := modelstesting.GenerateUser("testuser", startingBalance)
15 | if err := db.Create(&user).Error; err != nil {
16 | t.Fatalf("failed to create user: %v", err)
17 | }
18 |
19 | type testCase struct {
20 | txType string
21 | amount int64
22 | expectBalance int64
23 | expectErr bool
24 | }
25 |
26 | testCases := []testCase{
27 | {TransactionWin, 50, 150, false},
28 | {TransactionRefund, 25, 175, false},
29 | {TransactionSale, 20, 195, false},
30 | {TransactionBuy, 40, 155, false},
31 | {TransactionFee, 10, 145, false},
32 | {"UNKNOWN", 10, 145, true}, // balance should not change
33 | }
34 |
35 | for _, tc := range testCases {
36 | err := ApplyTransactionToUser(user.Username, tc.amount, db, tc.txType)
37 | var updated models.User
38 | if err := db.Where("username = ?", user.Username).First(&updated).Error; err != nil {
39 | t.Fatalf("failed to fetch user after update: %v", err)
40 | }
41 | if tc.expectErr {
42 | if err == nil {
43 | t.Errorf("expected error for type %s but got nil", tc.txType)
44 | }
45 | continue
46 | }
47 | if err != nil {
48 | t.Errorf("unexpected error for type %s: %v", tc.txType, err)
49 | }
50 | if updated.AccountBalance != tc.expectBalance {
51 | t.Errorf("after %s, expected balance %d, got %d", tc.txType, tc.expectBalance, updated.AccountBalance)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/backend/setup/setup_test.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import "testing"
4 |
5 | func TestLoadEconomicsConfigSingleton(t *testing.T) {
6 | cfg1, err := LoadEconomicsConfig()
7 | if err != nil {
8 | t.Fatalf("unexpected error loading config: %v", err)
9 | }
10 | if cfg1 == nil {
11 | t.Fatalf("expected non-nil config")
12 | }
13 |
14 | cfg2, err := LoadEconomicsConfig()
15 | if err != nil {
16 | t.Fatalf("unexpected error on second load: %v", err)
17 | }
18 |
19 | if cfg1 != cfg2 {
20 | t.Fatalf("expected LoadEconomicsConfig to return singleton instance")
21 | }
22 |
23 | if cfg1.Economics.User.MaximumDebtAllowed <= 0 {
24 | t.Fatalf("expected maximum debt to be populated, got %d", cfg1.Economics.User.MaximumDebtAllowed)
25 | }
26 |
27 | if EconomicsConfig() != cfg1 {
28 | t.Fatalf("EconomicsConfig should return the singleton instance")
29 | }
30 | }
31 |
32 | func TestChartSigFigsClamping(t *testing.T) {
33 | cfg, err := LoadEconomicsConfig()
34 | if err != nil {
35 | t.Fatalf("unexpected error loading config: %v", err)
36 | }
37 |
38 | original := cfg.Frontend.Charts.SigFigs
39 | t.Cleanup(func() {
40 | cfg.Frontend.Charts.SigFigs = original
41 | })
42 |
43 | cfg.Frontend.Charts.SigFigs = 1
44 | if got := ChartSigFigs(); got != minChartSigFigs {
45 | t.Fatalf("expected minChartSigFigs (%d), got %d", minChartSigFigs, got)
46 | }
47 |
48 | cfg.Frontend.Charts.SigFigs = 99
49 | if got := ChartSigFigs(); got != maxChartSigFigs {
50 | t.Fatalf("expected maxChartSigFigs (%d), got %d", maxChartSigFigs, got)
51 | }
52 |
53 | cfg.Frontend.Charts.SigFigs = 6
54 | if got := ChartSigFigs(); got != 6 {
55 | t.Fatalf("expected unclamped value 6, got %d", got)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/backend/handlers/setup/setuphandler.go:
--------------------------------------------------------------------------------
1 | package setup
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/setup"
7 | )
8 |
9 | func GetSetupHandler(loadEconomicsConfig func() (*setup.EconomicConfig, error)) func(w http.ResponseWriter, r *http.Request) {
10 | return func(w http.ResponseWriter, r *http.Request) {
11 | appConfig, err := loadEconomicsConfig()
12 | if err != nil {
13 | http.Error(w, "Failed to load economic config", http.StatusInternalServerError)
14 | return
15 | }
16 |
17 | w.Header().Set("Content-Type", "application/json")
18 | err = json.NewEncoder(w).Encode(appConfig.Economics)
19 | if err != nil {
20 | http.Error(w, "Failed to encode response", http.StatusInternalServerError)
21 | }
22 | }
23 | }
24 |
25 | type frontendChartsResponse struct {
26 | SigFigs int `json:"sigFigs"`
27 | }
28 |
29 | type frontendConfigResponse struct {
30 | Charts frontendChartsResponse `json:"charts"`
31 | }
32 |
33 | func GetFrontendSetupHandler(loadEconomicsConfig func() (*setup.EconomicConfig, error)) func(w http.ResponseWriter, r *http.Request) {
34 | return func(w http.ResponseWriter, r *http.Request) {
35 | _, err := loadEconomicsConfig()
36 | if err != nil {
37 | http.Error(w, "Failed to load frontend config", http.StatusInternalServerError)
38 | return
39 | }
40 |
41 | response := frontendConfigResponse{
42 | Charts: frontendChartsResponse{
43 | SigFigs: setup.ChartSigFigs(),
44 | },
45 | }
46 |
47 | w.Header().Set("Content-Type", "application/json")
48 | if err := json.NewEncoder(w).Encode(response); err != nil {
49 | http.Error(w, "Failed to encode frontend setup response", http.StatusInternalServerError)
50 | return
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/components/datetimeSelector/DatetimeSelector.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | const DatetimeSelector = ({ value, onChange }) => {
4 | const [internalValue, setInternalValue] = useState(value);
5 |
6 | // Set internalValue when component mounts
7 | useEffect(() => {
8 | if (!value) { // Only set default if no value is provided
9 | const now = new Date();
10 | const year = now.getFullYear();
11 | const month = now.getMonth() + 1;
12 | const day = now.getDate();
13 | const formattedMonth = month < 10 ? `0${month}` : `${month}`;
14 | const formattedDay = day < 10 ? `0${day}` : `${day}`;
15 | const defaultDateTime = `${year}-${formattedMonth}-${formattedDay}T23:59`;
16 | setInternalValue(defaultDateTime);
17 | }
18 | }, [value]);
19 |
20 | const handleChange = (event) => {
21 | setInternalValue(event.target.value);
22 | onChange(event); // Propagate changes to parent
23 | };
24 |
25 | return (
26 |
27 |
28 | Select Date and Time:
29 |
30 |
37 |
38 | );
39 | };
40 |
41 | export default DatetimeSelector;
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # SocialPredict Frontend
2 |
3 | This is the frontend application for SocialPredict, built with React and Vite.
4 |
5 | This gets you started with running only the frontend using Docker.
6 |
7 | #### Prerequisites
8 |
9 | - **Docker**: Version 20.10 or higher
10 | - **Docker Compose**: Version 2.0 or higher
11 |
12 | #### Running with Docker
13 |
14 | 1. **Build the frontend Docker image:**
15 |
16 | ```bash
17 | cd /path/to/socialpredict
18 | cd frontend
19 | docker build -t socialpredict .
20 | ```
21 |
22 | 2. **Run the frontend container:**
23 |
24 | ```bash
25 | docker run -d -p 5173:5173 socialpredict
26 | ```
27 |
28 | This will start the Vite development server with the following features:
29 |
30 | - **Port**: Typically runs on `http://localhost:5173` (Vite's default port)
31 | - **Host**: Accessible from other devices on your network
32 | - **Hot reload**: Automatic reloading when files change
33 | - **Fast builds**: Vite's lightning-fast development experience
34 |
35 | 3. **Access the application:**
36 | Navigate to `http://localhost:5173` on your browser to view the application.
37 |
38 | 4. **When ready, stop the container:**
39 | ```bash
40 | docker stop $(docker ps -n 1 -a -q)
41 | ```
42 |
43 | ### Mobile Responsiveness
44 |
45 | The application is fully responsive and includes:
46 |
47 | - Mobile-optimized navigation with hamburger menu
48 | - Touch-friendly interface
49 | - Responsive design for all screen sizes
50 |
51 | ### Tech Stack
52 |
53 | - **React 18**: UI framework
54 | - **Vite**: Build tool and development server
55 | - **Tailwind CSS**: Styling
56 | - **React Router**: Client-side routing
57 | - **Chart.js & Recharts**: Data visualization
58 |
--------------------------------------------------------------------------------
/README/PROJECT_MANAGEMENT/README-ROADMAP.md:
--------------------------------------------------------------------------------
1 | # Non-Binding Software Roadmap
2 |
3 | Here is a general list of bullet points without promise dates. This is a general non-binding road map showing where the software will go in the future.
4 |
5 | This road map is as up to date as the last time the file was committed on Github.
6 |
7 | ### Roadmap For 2024
8 |
9 | * Comments
10 | * Market Search
11 | * View Market by Status, Resolved, Closed, Otherwise
12 | * Leaderboards
13 |
14 | ### Project Boards
15 |
16 | We have a few project boards on Github which describe features and issues, including both bugs and enhancements, which serve as a more dynamic way to track smaller tasks and releases.
17 |
18 | * [Frontend Design Discussion](https://github.com/orgs/openpredictionmarkets/projects/3)
19 | * [v0.0.4 - Security and Admin Improvements](https://github.com/orgs/openpredictionmarkets/projects/4)
20 | * [v0.0.5 - Long Term Improvements Going Into 2025](https://github.com/orgs/openpredictionmarkets/projects/5)
21 |
22 | ### Readme Project Management
23 |
24 | Much of the devops related project management is held here:
25 |
26 | - [SocialPredict Script To Do](/README/PROJECT_MANAGEMENT/SocialPredict_Script_To_do.md)
27 |
28 |
29 | ### Already Accomplished
30 |
31 | #### Summer 2024
32 |
33 | * Improved Frontend User Interface
34 | * Capability to View Own Profile, Trades, Markets and Change Secure Information
35 | * Positions Displayable on Markets
36 | * One-Command to Build All Dockerfiles
37 | * Mobile Compatibility
38 | * Production Version Separated from Development Version
39 | * Capability to Sell Shares
40 |
41 | #### More Or Less Finished Projects
42 |
43 | * [v0.0.3 Consolidated Codebase, Frontend Improvements](https://github.com/orgs/openpredictionmarkets/projects/1)
44 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useMarketLabels.js:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import {
3 | getMarketLabels,
4 | mapInternalToDisplay,
5 | mapDisplayToInternal,
6 | getResultCssClass,
7 | getResolvedText,
8 | hasValidLabels,
9 | getFallbackLabels
10 | } from '../utils/labelMapping';
11 |
12 | /**
13 | * React hook for market label mapping
14 | * Provides easy access to label mapping utilities with memoization
15 | * @param {Object} market - Market object with label fields
16 | * @returns {Object} Object with label mapping functions and values
17 | */
18 | export const useMarketLabels = (market) => {
19 | const labels = useMemo(() => getFallbackLabels(market), [market]);
20 |
21 | const mapToDisplay = useMemo(
22 | () => (internalValue) => mapInternalToDisplay(internalValue, market),
23 | [market]
24 | );
25 |
26 | const mapToInternal = useMemo(
27 | () => (displayValue) => mapDisplayToInternal(displayValue, market),
28 | [market]
29 | );
30 |
31 | const getResultClass = useMemo(
32 | () => (internalValue) => getResultCssClass(internalValue),
33 | []
34 | );
35 |
36 | const getResolvedLabel = useMemo(
37 | () => (internalValue) => getResolvedText(internalValue, market),
38 | [market]
39 | );
40 |
41 | const isValidLabels = useMemo(() => hasValidLabels(market), [market]);
42 |
43 | return {
44 | // Labels for display
45 | yesLabel: labels.yes,
46 | noLabel: labels.no,
47 |
48 | // Mapping functions
49 | mapToDisplay,
50 | mapToInternal,
51 | getResultClass,
52 | getResolvedLabel,
53 |
54 | // Validation
55 | isValidLabels,
56 |
57 | // Original market for reference
58 | market
59 | };
60 | };
61 |
62 | export default useMarketLabels;
--------------------------------------------------------------------------------
/backend/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "golang.org/x/crypto/bcrypt"
5 | "gorm.io/gorm"
6 | )
7 |
8 | type User struct {
9 | gorm.Model
10 | ID int64 `json:"id" gorm:"primary_key"`
11 | PublicUser
12 | PrivateUser
13 | MustChangePassword bool `json:"mustChangePassword" gorm:"default:true"`
14 | }
15 |
16 | type PublicUser struct {
17 | Username string `json:"username" gorm:"unique;not null"`
18 | DisplayName string `json:"displayname" gorm:"unique;not null"`
19 | UserType string `json:"usertype" gorm:"not null"`
20 | InitialAccountBalance int64 `json:"initialAccountBalance"`
21 | AccountBalance int64 `json:"accountBalance"`
22 | PersonalEmoji string `json:"personalEmoji,omitempty"`
23 | Description string `json:"description,omitempty"`
24 | PersonalLink1 string `json:"personalink1,omitempty"`
25 | PersonalLink2 string `json:"personalink2,omitempty"`
26 | PersonalLink3 string `json:"personalink3,omitempty"`
27 | PersonalLink4 string `json:"personalink4,omitempty"`
28 | }
29 |
30 | type PrivateUser struct {
31 | Email string `json:"email" gorm:"unique;not null"`
32 | APIKey string `json:"apiKey,omitempty" gorm:"unique"`
33 | Password string `json:"password,omitempty" gorm:"not null"`
34 | }
35 |
36 | // HashPassword hashes given password
37 | func (u *User) HashPassword(password string) error {
38 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
39 | u.Password = string(bytes)
40 | return err
41 | }
42 |
43 | // CheckPasswordHash checks if provided password is correct
44 | func (u *User) CheckPasswordHash(password string) bool {
45 | err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
46 | return err == nil
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/components/resolutions/ResolutionAlert.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getResolvedText, getResultCssClass } from '../../utils/labelMapping';
3 |
4 | const ResolutionAlert = ({ isResolved, resolutionResult, market }) => {
5 | if (!isResolved) return null;
6 |
7 | const isYes = resolutionResult.toUpperCase() === 'YES';
8 | const bgColor = isYes ? 'bg-blue-900/30' : 'bg-red-900/30';
9 | const borderColor = isYes ? 'border-blue-500/50' : 'border-red-500/50';
10 | const textColor = isYes ? 'text-blue-200' : 'text-red-200';
11 | const iconColor = isYes ? 'text-blue-400' : 'text-red-400';
12 | const strongColor = isYes ? 'text-green-300' : 'text-red-300';
13 |
14 | return (
15 |
18 |
19 |
25 |
31 |
32 |
33 |
Market Resolved
34 |
35 | This market has been resolved as{' '}
36 |
37 | {market ? getResolvedText(resolutionResult, market).replace('Resolved ', '') : resolutionResult}
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default ResolutionAlert;
47 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
22 | SocialPredict
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/src/App.css.backup:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | flex-grow: 1; /* Grow to fill available space */
4 | padding: 0px;
5 | }
6 |
7 | .App-logo {
8 | height: 40vmin;
9 | pointer-events: none;
10 | }
11 |
12 | @media (prefers-reduced-motion: no-preference) {
13 | .App-logo {
14 | animation: App-logo-spin infinite 20s linear;
15 | }
16 | }
17 |
18 | .App-header {
19 | background-color: #280000;
20 | min-height: 100vh;
21 | display: flex;
22 | flex-direction: column;
23 | align-items: center;
24 | justify-content: flex-start;
25 | font-size: calc(10px + 2vmin);
26 | color: white;
27 | }
28 |
29 | .App-link {
30 | color: #61dafb;
31 | }
32 |
33 | .Center-content {
34 | margin-left: 10px; /* Space for the left navbar */
35 | margin-right: 100px; /* Space for the right navbar */
36 | max-width: 1000px;
37 | margin-top: 10px;
38 | margin-bottom: 20px;
39 | padding: 10px;
40 | background-color: #280000; /* Optional, for contrast */
41 | }
42 |
43 | .Center-content-header {
44 | text-align: center;
45 | }
46 |
47 | .Center-content-table {
48 | text-align: left;
49 | font-size: 0.5rem;
50 | margin-top: 30px;
51 | }
52 |
53 | .Center-content-table a {
54 | color: #00ffff;
55 | }
56 |
57 | .Center-content-table a:hover {
58 | color: white;
59 | background-color: #DE7C5A;
60 | }
61 |
62 | .Center-content-table a:active {
63 | /* Styles for active state */
64 | }
65 |
66 | @keyframes App-logo-spin {
67 | from {
68 | transform: rotate(0deg);
69 | }
70 | to {
71 | transform: rotate(360deg);
72 | }
73 | }
74 |
75 | .container {
76 | display: flex;
77 | justify-content: center;
78 | }
79 |
80 | .footer {
81 | padding: 20px;
82 | text-align: center;
83 | background-color: #280000;
84 | bottom: 0;
85 | width: 100%;
86 | }
87 |
--------------------------------------------------------------------------------
/README/CHECKPOINTS/CHECKPOINT20250724-01.md:
--------------------------------------------------------------------------------
1 | * The problem we're trying to fix here is that when deployed to production, on the front end, if we visit an endpoint such as:
2 |
3 | https://domain.com/markets
4 |
5 | It gives a 404 error.
6 |
7 | However if we go to:
8 |
9 | https://domain.com/
10 |
11 | ...and then navigate to /markets, it's fine, no 404 error.
12 |
13 | Also, if we go to a page such as domain.com/markets/1 and then click on the market creator in the upper left of the page, it also gives a 404.
14 |
15 | However no other navigation buttons seem to give this anywhere else.
16 |
17 | Where this button should lead is, for example:
18 |
19 | domain.com/user/{USERNAME}
20 |
21 | Now, if we visit that directly, e.g, domain.com/user/{USERNAME}, it gives the user's public profile as expected.
22 |
23 | Also, if we visit this from below under the bets or positions module, it successfully navigates with no 402 Error.
24 |
25 | Again, this is a problem under the production build, which can be found under scripts/
26 |
27 | To be a bit more clear, all of this runs on docker.
28 |
29 | I suspect there is something wrong with how react is serving things and this has nothing to do with the back end.
30 |
31 | For the purposes of this task, it would be nice to just code it up and then hand it back over to a tester, rather than trying to test the code written.
32 |
33 | There are lots of complicated things going on and the fact that this has to be deployed to prod to really test out makes executing commands on local inadaquate for testing and finalizing this code.
34 |
35 | As far as writing react based tests, we haven't gotten into that so it's not necessary to follow all of the testing concerns mentioned in the conventions and testing conventions. We have been following that for our backend/ not our frontend/ for the timg being.
--------------------------------------------------------------------------------
/backend/handlers/users/changedescription.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/middleware"
7 | "socialpredict/security"
8 | "socialpredict/util"
9 | )
10 |
11 | type ChangeDescriptionRequest struct {
12 | Description string `json:"description"`
13 | }
14 |
15 | func ChangeDescription(w http.ResponseWriter, r *http.Request) {
16 | if r.Method != http.MethodPost {
17 | http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
18 | return
19 | }
20 |
21 | // Initialize security service
22 | securityService := security.NewSecurityService()
23 |
24 | db := util.GetDB()
25 | user, httperr := middleware.ValidateTokenAndGetUser(r, db)
26 | if httperr != nil {
27 | http.Error(w, "Invalid token: "+httperr.Error(), http.StatusUnauthorized)
28 | return
29 | }
30 |
31 | var request ChangeDescriptionRequest
32 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
33 | http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
34 | return
35 | }
36 |
37 | // Validate description length and content
38 | if len(request.Description) > 2000 {
39 | http.Error(w, "Description exceeds maximum length of 2000 characters", http.StatusBadRequest)
40 | return
41 | }
42 |
43 | // Sanitize the description to prevent XSS
44 | sanitizedDescription, err := securityService.Sanitizer.SanitizeDescription(request.Description)
45 | if err != nil {
46 | http.Error(w, "Invalid description: "+err.Error(), http.StatusBadRequest)
47 | return
48 | }
49 |
50 | user.Description = sanitizedDescription
51 | if err := db.Save(&user).Error; err != nil {
52 | http.Error(w, "Failed to update description: "+err.Error(), http.StatusInternalServerError)
53 | return
54 | }
55 |
56 | w.WriteHeader(http.StatusOK)
57 | json.NewEncoder(w).Encode(user)
58 | }
59 |
--------------------------------------------------------------------------------
/backend/handlers/users/credit/usercredit_test.go:
--------------------------------------------------------------------------------
1 | package usercredit
2 |
3 | import (
4 | "fmt"
5 | "socialpredict/models"
6 | "socialpredict/models/modelstesting"
7 | "testing"
8 | )
9 |
10 | func TestCalculateUserCredit(t *testing.T) {
11 | db := modelstesting.NewFakeDB(t)
12 |
13 | testCases := []struct {
14 | username string
15 | displayName string
16 | accountBalance int64
17 | maximumDebt int64
18 | expectedCredit int64
19 | }{
20 | {"user1", "Test User 1", -100, 500, 400},
21 | {"user2", "Test User 2", 0, 500, 500},
22 | {"user3", "Test User 3", 100, 500, 600},
23 | {"user4", "Test User 4", -100, 5000, 4900},
24 | {"user5", "Test User 5", 0, 5000, 5000},
25 | {"user6", "Test User 6", 100, 5000, 5100},
26 | }
27 |
28 | for _, tc := range testCases {
29 | user := models.User{
30 | PublicUser: models.PublicUser{
31 | Username: tc.username,
32 | DisplayName: tc.displayName,
33 | UserType: "REGULAR",
34 | AccountBalance: tc.accountBalance,
35 | },
36 | PrivateUser: models.PrivateUser{
37 | Email: tc.username + "@example.com",
38 | Password: "password123",
39 | APIKey: "apikey-" + tc.username,
40 | },
41 | }
42 |
43 | if err := db.Create(&user).Error; err != nil {
44 | t.Fatalf("Failed to save user %s to database: %v", tc.username, err)
45 | }
46 | }
47 |
48 | for _, tc := range testCases {
49 | t.Run(fmt.Sprintf("Username=%s_AccountBalance=%d_MaximumDebt=%d", tc.username, tc.accountBalance, tc.maximumDebt), func(t *testing.T) {
50 | credit := calculateUserCredit(db, tc.username, tc.maximumDebt)
51 | if credit != tc.expectedCredit {
52 | t.Errorf(
53 | "calculateUserCredit(db, username=%s, maximumDebt=%d) = %d; want %d",
54 | tc.username, tc.maximumDebt, credit, tc.expectedCredit,
55 | )
56 | }
57 | })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/backend/handlers/math/positions/earliest_users.go:
--------------------------------------------------------------------------------
1 | package positionsmath
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | type UserOrdered struct {
11 | Username string
12 | YesShares int64
13 | NoShares int64
14 | EarliestBet string
15 | }
16 |
17 | // GetAllUserEarliestBetsForMarket returns a map of usernames to their earliest bet timestamp in the given market.
18 | func GetAllUserEarliestBetsForMarket(db *gorm.DB, marketID uint) (map[string]time.Time, error) {
19 | var ordered []UserOrdered
20 | err := db.Raw(`
21 | SELECT username,
22 | SUM(CASE WHEN outcome = 'YES' THEN amount ELSE 0 END) as yes_shares,
23 | SUM(CASE WHEN outcome = 'NO' THEN amount ELSE 0 END) as no_shares,
24 | MIN(placed_at) as earliest_bet
25 | FROM bets
26 | WHERE market_id = ?
27 | GROUP BY username
28 | ORDER BY (SUM(CASE WHEN outcome = 'YES' THEN amount ELSE 0 END) +
29 | SUM(CASE WHEN outcome = 'NO' THEN amount ELSE 0 END)) DESC,
30 | earliest_bet ASC,
31 | username ASC
32 | `, marketID).Scan(&ordered).Error
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | m := make(map[string]time.Time)
38 | for _, b := range ordered {
39 | var t time.Time
40 | var err error
41 | layouts := []string{
42 | time.RFC3339Nano, // try RFC first
43 | "2006-01-02 15:04:05.999999999", // sqlite default (no TZ)
44 | "2006-01-02 15:04:05.999999-07:00", // your case!
45 | "2006-01-02 15:04:05", // fallback, no fraction or tz
46 | }
47 | for _, layout := range layouts {
48 | t, err = time.Parse(layout, b.EarliestBet)
49 | if err == nil {
50 | break
51 | }
52 | }
53 | if err != nil {
54 | log.Printf("Could not parse time %q for user %s: %v", b.EarliestBet, b.Username, err)
55 | continue
56 | }
57 | m[b.Username] = t
58 | }
59 | return m, nil
60 | }
61 |
--------------------------------------------------------------------------------
/backend/handlers/users/publicuser/publicuser_test.go:
--------------------------------------------------------------------------------
1 | package publicuser
2 |
3 | import (
4 | "socialpredict/models"
5 | "socialpredict/models/modelstesting"
6 |
7 | "testing"
8 | )
9 |
10 | func TestGetPublicUserInfo(t *testing.T) {
11 |
12 | db := modelstesting.NewFakeDB(t)
13 |
14 | user := models.User{
15 | PublicUser: models.PublicUser{
16 | Username: "testuser",
17 | DisplayName: "Test User",
18 | UserType: "regular",
19 | InitialAccountBalance: 1000,
20 | AccountBalance: 500,
21 | PersonalEmoji: "😊",
22 | Description: "Test description",
23 | PersonalLink1: "http://link1.com",
24 | PersonalLink2: "http://link2.com",
25 | PersonalLink3: "http://link3.com",
26 | PersonalLink4: "http://link4.com",
27 | },
28 | PrivateUser: models.PrivateUser{
29 | Email: "testuser@example.com",
30 | APIKey: "whatever123",
31 | Password: "whatever123",
32 | },
33 | }
34 |
35 | if err := db.Create(&user).Error; err != nil {
36 | t.Fatalf("Failed to save user to database: %v", err)
37 | }
38 |
39 | retrievedUser := GetPublicUserInfo(db, "testuser")
40 |
41 | expectedUser := models.PublicUser{
42 | Username: "testuser",
43 | DisplayName: "Test User",
44 | UserType: "regular",
45 | InitialAccountBalance: 1000,
46 | AccountBalance: 500,
47 | PersonalEmoji: "😊",
48 | Description: "Test description",
49 | PersonalLink1: "http://link1.com",
50 | PersonalLink2: "http://link2.com",
51 | PersonalLink3: "http://link3.com",
52 | PersonalLink4: "http://link4.com",
53 | }
54 |
55 | if retrievedUser != expectedUser {
56 | t.Errorf("GetPublicUserInfo(db, 'testuser') = %+v, want %+v", retrievedUser, expectedUser)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/backend/handlers/users/changedisplayname.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/middleware"
7 | "socialpredict/security"
8 | "socialpredict/util"
9 | )
10 |
11 | type ChangeDisplayNameRequest struct {
12 | DisplayName string `json:"displayName"`
13 | }
14 |
15 | func ChangeDisplayName(w http.ResponseWriter, r *http.Request) {
16 | if r.Method != http.MethodPost {
17 | http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
18 | return
19 | }
20 |
21 | // Initialize security service
22 | securityService := security.NewSecurityService()
23 |
24 | db := util.GetDB()
25 | user, httperr := middleware.ValidateTokenAndGetUser(r, db)
26 | if httperr != nil {
27 | http.Error(w, "Invalid token: "+httperr.Error(), http.StatusUnauthorized)
28 | return
29 | }
30 |
31 | var request ChangeDisplayNameRequest
32 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
33 | http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
34 | return
35 | }
36 |
37 | // Validate display name length and content
38 | if len(request.DisplayName) > 50 || len(request.DisplayName) < 1 {
39 | http.Error(w, "Display name must be between 1 and 50 characters", http.StatusBadRequest)
40 | return
41 | }
42 |
43 | // Sanitize the display name to prevent XSS
44 | sanitizedDisplayName, err := securityService.Sanitizer.SanitizeDisplayName(request.DisplayName)
45 | if err != nil {
46 | http.Error(w, "Invalid display name: "+err.Error(), http.StatusBadRequest)
47 | return
48 | }
49 |
50 | user.DisplayName = sanitizedDisplayName
51 | if err := db.Save(&user).Error; err != nil {
52 | http.Error(w, "Failed to update display name: "+err.Error(), http.StatusInternalServerError)
53 | return
54 | }
55 |
56 | w.WriteHeader(http.StatusOK)
57 | json.NewEncoder(w).Encode(user)
58 | }
59 |
--------------------------------------------------------------------------------
/backend/handlers/users/changeemoji.go:
--------------------------------------------------------------------------------
1 | package usershandlers
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "socialpredict/middleware"
7 | "socialpredict/security"
8 | "socialpredict/util"
9 | )
10 |
11 | type ChangeEmojiRequest struct {
12 | Emoji string `json:"emoji"`
13 | }
14 |
15 | func ChangeEmoji(w http.ResponseWriter, r *http.Request) {
16 | if r.Method != http.MethodPost {
17 | http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
18 | return
19 | }
20 |
21 | // Initialize security service
22 | securityService := security.NewSecurityService()
23 |
24 | db := util.GetDB()
25 | user, httperr := middleware.ValidateTokenAndGetUser(r, db)
26 | if httperr != nil {
27 | http.Error(w, "Invalid token: "+httperr.Error(), http.StatusUnauthorized)
28 | return
29 | }
30 |
31 | var request ChangeEmojiRequest
32 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
33 | http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
34 | return
35 | }
36 |
37 | // Validate emoji length and content
38 | if len(request.Emoji) > 20 {
39 | http.Error(w, "Emoji exceeds maximum length of 20 characters", http.StatusBadRequest)
40 | return
41 | }
42 |
43 | if request.Emoji == "" {
44 | http.Error(w, "Emoji cannot be blank", http.StatusBadRequest)
45 | return
46 | }
47 |
48 | // Sanitize the emoji to prevent XSS
49 | sanitizedEmoji, err := securityService.Sanitizer.SanitizeEmoji(request.Emoji)
50 | if err != nil {
51 | http.Error(w, "Invalid emoji: "+err.Error(), http.StatusBadRequest)
52 | return
53 | }
54 |
55 | user.PersonalEmoji = sanitizedEmoji
56 | if err := db.Save(&user).Error; err != nil {
57 | http.Error(w, "Failed to update emoji: "+err.Error(), http.StatusInternalServerError)
58 | return
59 | }
60 |
61 | w.WriteHeader(http.StatusOK)
62 | json.NewEncoder(w).Encode(user)
63 | }
64 |
--------------------------------------------------------------------------------
/backend/handlers/math/market/marketvolume_test.go:
--------------------------------------------------------------------------------
1 | package marketmath
2 |
3 | import (
4 | "socialpredict/models"
5 | "testing"
6 | )
7 |
8 | // TestGetMarketVolume tests that the total volume of trades is returned correctly
9 | func TestGetMarketVolume(t *testing.T) {
10 | tests := []struct {
11 | Name string
12 | Bets []models.Bet
13 | ExpectedVolume int64
14 | }{
15 | {
16 | Name: "empty slice",
17 | Bets: []models.Bet{},
18 | ExpectedVolume: 0,
19 | },
20 | {
21 | Name: "positive Bets",
22 | Bets: []models.Bet{
23 | {Amount: 100},
24 | {Amount: 200},
25 | {Amount: 300},
26 | },
27 | ExpectedVolume: 600,
28 | },
29 | {
30 | Name: "negative Bets",
31 | Bets: []models.Bet{
32 | {Amount: -100},
33 | {Amount: -200},
34 | {Amount: -300},
35 | },
36 | ExpectedVolume: -600,
37 | },
38 | {
39 | Name: "mixed Bets",
40 | Bets: []models.Bet{
41 | {Amount: 100},
42 | {Amount: -50},
43 | {Amount: 200},
44 | {Amount: -150},
45 | },
46 | ExpectedVolume: 100,
47 | },
48 | {
49 | Name: "large numbers",
50 | Bets: []models.Bet{
51 | {Amount: 9223372036854775807},
52 | {Amount: -9223372036854775806},
53 | },
54 | ExpectedVolume: 1,
55 | },
56 | {
57 | Name: "infinity avoidance",
58 | Bets: []models.Bet{
59 | {Amount: 1, Outcome: "YES"},
60 | {Amount: -1, Outcome: "YES"},
61 | {Amount: 1, Outcome: "NO"},
62 | {Amount: -1, Outcome: "NO"},
63 | {Amount: 1, Outcome: "NO"},
64 | },
65 | ExpectedVolume: 1,
66 | },
67 | }
68 |
69 | for _, test := range tests {
70 | t.Run(test.Name, func(t *testing.T) {
71 | volume := GetMarketVolume(test.Bets)
72 | if volume != test.ExpectedVolume {
73 | t.Errorf("%s: expected %d, got %d", test.Name, test.ExpectedVolume, volume)
74 | }
75 | })
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router } from 'react-router-dom';
3 | import { ErrorBoundary } from 'react-error-boundary';
4 | import { AuthProvider } from './helpers/AuthContent';
5 | import Footer from './components/footer/Footer';
6 | import AppRoutes from './helpers/AppRoutes';
7 | import '../index.css';
8 | import Sidebar from './components/sidebar/Sidebar';
9 |
10 | function ErrorFallback({ error, resetErrorBoundary }) {
11 | return (
12 |
13 |
Oops! Something went wrong.
14 |
15 | We're sorry for the inconvenience. Please try again.
16 |
17 |
{error.message}
18 |
22 | Try again
23 |
24 |
25 | );
26 | }
27 |
28 | function App() {
29 | return (
30 | {
33 | // Reset the state of your app so the error doesn't happen again
34 | }}
35 | >
36 |
37 |
38 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/profile/DescriptionSelector.jsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../../../config';
2 | import React, { useState } from 'react';
3 | import SiteButton from '../SiteButtons';
4 |
5 | const DescriptionSelector = ({ onSave }) => {
6 | const [description, setDescription] = useState('');
7 |
8 | const handleSave = async () => {
9 | if (!description.trim()) {
10 | alert('Please enter a description before saving.');
11 | return;
12 | }
13 | try {
14 | const token = localStorage.getItem('token');
15 | const response = await fetch(`${API_URL}/v0/profilechange/description`, {
16 | method: 'POST',
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | Authorization: `Bearer ${token}`,
20 | },
21 | body: JSON.stringify({ description }),
22 | });
23 | const responseData = await response.json();
24 | if (response.ok) {
25 | console.log('Description updated successfully:', responseData);
26 | onSave(description);
27 | } else {
28 | throw new Error('Failed to update description');
29 | }
30 | } catch (error) {
31 | console.error('Error updating description:', error);
32 | }
33 | };
34 |
35 | return (
36 |
37 |
45 | );
46 | };
47 |
48 | export default DescriptionSelector;
49 |
--------------------------------------------------------------------------------
/frontend/src/pages/marketDetails/marketDetailsUtils.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import { API_URL } from '../../config';
4 |
5 | export const useFetchMarketData = () => {
6 | const [details, setDetails] = useState(null);
7 | const { marketId } = useParams();
8 | const [triggerRefresh, setTriggerRefresh] = useState(false);
9 |
10 | useEffect(() => {
11 | const fetchData = async () => {
12 | try {
13 | const response = await fetch(`${API_URL}/v0/markets/${marketId}`);
14 | const data = await response.json();
15 | setDetails(data);
16 | } catch (error) {
17 | console.error('Error fetching market data:', error);
18 | }
19 | };
20 |
21 | fetchData();
22 | }, [marketId, triggerRefresh]);
23 |
24 | const refetchData = () => {
25 | setTriggerRefresh(prev => !prev);
26 | };
27 |
28 | return { details, refetchData };
29 | };
30 |
31 | export const calculateCurrentProbability = (details) => {
32 | if (!details || !details.probabilityChanges) return 0;
33 |
34 | const currentProbability = details.probabilityChanges.length > 0
35 | ? details.probabilityChanges[details.probabilityChanges.length - 1].probability
36 | : details.market.initialProbability;
37 |
38 | return parseFloat(currentProbability.toFixed(2));
39 | };
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | //const roundedProbability = parseFloat(newCurrentProbability.toFixed(3));
48 |
49 | // Append the current time with the last known probability, converted to Unix time
50 | // const [currentProbability, setCurrentProbability] = useState(null);
51 | // const currentTimeStamp = new Date().getTime();
52 | // chartData.push({ time: currentTimeStamp, P: roundedProbability });
53 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "socialpredict-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@canvasjs/react-charts": "^1.0.2",
7 | "@testing-library/jest-dom": "^5.17.0",
8 | "@testing-library/react": "^13.4.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "chart.js": "^4.4.0",
11 | "d3": "^7.8.5",
12 | "emoji-picker-react": "^4.14.0",
13 | "jwt-decode": "^4.0.0",
14 | "path-to-regexp": ">=8.2.0",
15 | "react": "^18.2.0",
16 | "react-chartjs-2": "^5.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-error-boundary": "^4.0.13",
19 | "react-router-dom": "^5.3.0",
20 | "recharts": "^2.10.3",
21 | "rollup": ">=4.22.4",
22 | "vite-plugin-commonjs": "^0.10.3",
23 | "web-vitals": "^2.1.4"
24 | },
25 | "engines": {
26 | "node": ">=21.0.0"
27 | },
28 | "scripts": {
29 | "start": "vite --host",
30 | "build": "vite build",
31 | "serve": "vite preview"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "react-app/jest"
37 | ]
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
52 | "main": "index.js",
53 | "keywords": [],
54 | "author": "",
55 | "license": "ISC",
56 | "devDependencies": {
57 | "@vitejs/plugin-react": "^4.7.0",
58 | "autoprefixer": "^10.4.17",
59 | "esbuild": "0.25.9",
60 | "path-to-regexp": ">=8.2.0",
61 | "postcss": "^8.4.35",
62 | "rollup": ">=4.22.4",
63 | "tailwindcss": "^3.4.1",
64 | "vite": "^7.1.11"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {
6 | colors: {
7 | 'primary-background': {
8 | DEFAULT: '#0e121d',
9 | },
10 | 'custom-gray-verylight': {
11 | DEFAULT: '#DBD4D3',
12 | },
13 | 'custom-gray-light': {
14 | DEFAULT: '#67697C',
15 | },
16 | 'custom-gray-dark': {
17 | DEFAULT: '#303030',
18 | },
19 | 'beige': {
20 | DEFAULT: '#F9D3A5',
21 | hover: '#F9D3A5',
22 | active: '#F9D3A5'
23 | },
24 | 'green-btn': {
25 | DEFAULT: '#054A29',
26 | hover: '#00cca4',
27 | 'border-default': '#054A29',
28 | 'border-hover': '#00cca4',
29 | },
30 | 'red-btn': {
31 | DEFAULT: '#D00000',
32 | hover: '#FF8484',
33 | 'border-default': '#D00000',
34 | 'border-hover': '#FF8484',
35 | },
36 | 'gold-btn': {
37 | DEFAULT: '#FFC107',
38 | hover: '#FFC107',
39 | active: '#FFC107',
40 | },
41 | 'neutral-btn': {
42 | DEFAULT: '#8A1C7C',
43 | hover: '#8A1C7C',
44 | active: '#8A1C7C',
45 | },
46 | 'primary-pink': {
47 | DEFAULT: '#F72585',
48 | },
49 | 'info-blue': {
50 | DEFAULT: '#17a2b8',
51 | },
52 | 'warning-orange': {
53 | DEFAULT: '#ffc107',
54 | },
55 | },
56 | borderRadius: {
57 | 'badge': '12px'
58 | },
59 | spacing: {
60 | 'sidebar': '8rem', // more rem means sidebar thicker
61 | },
62 | zIndex: {
63 | 'sidebar': 40, // higher number means more on top
64 | },
65 | },
66 | },
67 | plugins: [],
68 | };
69 |
--------------------------------------------------------------------------------
/frontend/src/components/buttons/profile/DisplayNameSelector.jsx:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../../../config';
2 | import React, { useState } from 'react';
3 | import SiteButton from '../SiteButtons';
4 |
5 | const DisplayNameSelector = ({ onSave }) => {
6 | const [displayName, setDisplayName] = useState('');
7 |
8 | const handleSave = async () => {
9 | if (!displayName.trim()) {
10 | alert('Please enter a display name before saving.');
11 | return;
12 | }
13 | try {
14 | const token = localStorage.getItem('token');
15 | const response = await fetch(`${API_URL}/v0/profilechange/displayname`, {
16 | method: 'POST',
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | Authorization: `Bearer ${token}`,
20 | },
21 | body: JSON.stringify({ displayName }),
22 | });
23 | const responseData = await response.json();
24 | if (response.ok) {
25 | console.log('Display name updated successfully:', responseData);
26 | onSave(displayName);
27 | } else {
28 | throw new Error('Failed to update display name');
29 | }
30 | } catch (error) {
31 | console.error('Error updating display name:', error);
32 | }
33 | };
34 |
35 | return (
36 |
37 | setDisplayName(e.target.value)}
41 | placeholder="Enter new display name..."
42 | className="mb-4 px-2 py-1 border rounded text-black"
43 | />
44 | Save Display Name
45 |
46 | );
47 | };
48 |
49 | export default DisplayNameSelector;
50 |
--------------------------------------------------------------------------------
/frontend/README_SPA_ROUTING_FIX.md:
--------------------------------------------------------------------------------
1 | # SPA Routing Fix for Production
2 |
3 | ## Problem
4 | Direct navigation to routes like `/markets` or `/user/{username}` in production resulted in 404 errors, while navigation from the home page worked fine.
5 |
6 | ## Root Cause
7 | The production setup was missing proper Single Page Application (SPA) fallback configuration in nginx. When users directly visited routes like `/markets`, the server tried to find a file at that path instead of serving the React app's `index.html` and letting React Router handle the routing.
8 |
9 | ## Solution
10 | Added a custom nginx configuration (`nginx.conf`) to the frontend container with the essential `try_files` directive:
11 |
12 | ```nginx
13 | location / {
14 | try_files $uri $uri/ /index.html;
15 | }
16 | ```
17 |
18 | This ensures that:
19 | 1. nginx first tries to serve the requested URI as a file
20 | 2. If that fails, tries to serve it as a directory
21 | 3. If that also fails, falls back to serving `/index.html`
22 | 4. React Router then handles the client-side routing
23 |
24 | ## Files Modified
25 | - `frontend/nginx.conf` - New custom nginx configuration for SPA routing
26 | - `frontend/Dockerfile.prod` - Updated to copy the custom nginx config
27 |
28 | ## How It Works
29 | - Static assets (JS, CSS, images) are served directly with caching headers
30 | - All other requests fall back to `index.html`, allowing React Router to handle routing
31 | - API requests are still handled by the main nginx proxy configuration
32 |
33 | ## Testing
34 | After rebuilding and deploying:
35 | - Direct navigation to `/markets` should work
36 | - Direct navigation to `/user/{username}` should work
37 | - Clicking on market creator links should work
38 | - All existing functionality should continue to work
39 |
40 | ## Related Files
41 | - `data/nginx/vhosts/prod/app.conf.template` - Main nginx proxy configuration
42 | - `scripts/prod/build_prod.sh` - Production build script
43 |
--------------------------------------------------------------------------------
/backend/handlers/bets/betutils/validatebet.go:
--------------------------------------------------------------------------------
1 | package betutils
2 |
3 | import (
4 | "errors"
5 | "socialpredict/models"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | func ValidateBuy(db *gorm.DB, bet *models.Bet) error {
11 | var user models.User
12 | var market models.Market
13 |
14 | // Check if username exists
15 | if err := db.First(&user, "username = ?", bet.Username).Error; err != nil {
16 | return errors.New("invalid username")
17 | }
18 |
19 | // Check if market exists and is open
20 | if err := db.First(&market, "id = ? AND is_resolved = false", bet.MarketID).Error; err != nil {
21 | return errors.New("invalid or closed market")
22 | }
23 |
24 | // Check for valid amount: it should be greater than or equal to 1
25 | if bet.Amount < 1 {
26 | return errors.New("Buy amount must be greater than or equal to 1")
27 | }
28 |
29 | // Validate bet outcome: it should be either 'YES' or 'NO'
30 | if bet.Outcome != "YES" && bet.Outcome != "NO" {
31 | return errors.New("bet outcome must be 'YES' or 'NO'")
32 | }
33 |
34 | return nil
35 | }
36 |
37 | func ValidateSale(db *gorm.DB, bet *models.Bet) error {
38 | var user models.User
39 | var market models.Market
40 |
41 | // Check if username exists
42 | if err := db.First(&user, "username = ?", bet.Username).Error; err != nil {
43 | return errors.New("invalid username")
44 | }
45 |
46 | // Check if market exists and is open
47 | if err := db.First(&market, "id = ? AND is_resolved = false", bet.MarketID).Error; err != nil {
48 | return errors.New("invalid or closed market")
49 | }
50 |
51 | // Check for valid amount: it should be less than or equal to -1
52 | if bet.Amount > -1 {
53 | return errors.New("Sale amount must be greater than or equal to 1")
54 | }
55 |
56 | // Validate bet outcome: it should be either 'YES' or 'NO'
57 | if bet.Outcome != "YES" && bet.Outcome != "NO" {
58 | return errors.New("bet outcome must be 'YES' or 'NO'")
59 | }
60 |
61 | return nil
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useMarketDetails.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom/cjs/react-router-dom';
3 | import { API_URL } from '../config';
4 |
5 | const calculateCurrentProbability = (details) => {
6 | if (!details || !details.probabilityChanges) return 0;
7 |
8 | const currentProbability =
9 | details.probabilityChanges.length > 0
10 | ? details.probabilityChanges[details.probabilityChanges.length - 1]
11 | .probability
12 | : details.market.initialProbability;
13 |
14 | return parseFloat(currentProbability.toFixed(2));
15 | };
16 |
17 | export const useMarketDetails = () => {
18 | const [details, setDetails] = useState(null);
19 | const [isLoggedIn, setIsLoggedIn] = useState(false);
20 | const [token, setToken] = useState(null);
21 | const [currentProbability, setCurrentProbability] = useState(0);
22 | const { marketId } = useParams();
23 | const [triggerRefresh, setTriggerRefresh] = useState(false);
24 |
25 | useEffect(() => {
26 | const fetchedToken = localStorage.getItem('token');
27 | setToken(fetchedToken);
28 | setIsLoggedIn(!!fetchedToken);
29 | }, []);
30 |
31 | useEffect(() => {
32 | const fetchData = async () => {
33 | try {
34 | const response = await fetch(`${API_URL}/v0/markets/${marketId}`);
35 | if (!response.ok) {
36 | throw new Error('Failed to fetch market data');
37 | }
38 | const data = await response.json();
39 | setDetails(data);
40 | setCurrentProbability(calculateCurrentProbability(data));
41 | } catch (error) {
42 | console.error('Error fetching market data:', error);
43 | }
44 | };
45 |
46 | fetchData();
47 | }, [marketId, triggerRefresh]);
48 |
49 | const refetchData = () => {
50 | setTriggerRefresh((prev) => !prev);
51 | };
52 |
53 | return { details, isLoggedIn, token, refetchData, currentProbability };
54 | };
55 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import {API_URL} from "../../config";
3 |
4 | function Home() {
5 | const [content, setContent] = useState(null);
6 | const [loading, setLoading] = useState(true);
7 |
8 | useEffect(() => {
9 | fetch(`${API_URL}/v0/content/home`)
10 | .then(response => response.json())
11 | .then(data => {
12 | setContent({
13 | title: data.title,
14 | html: data.html,
15 | version: data.version
16 | });
17 | setLoading(false);
18 | })
19 | .catch(error => {
20 | console.error('Failed to load homepage content:', error);
21 | setLoading(false);
22 | });
23 | }, []);
24 |
25 | if (loading) {
26 | return (
27 |
32 | );
33 | }
34 |
35 | if (!content) {
36 | return (
37 |
38 |
39 |
Failed to load homepage content.
40 |
41 |
42 | );
43 | }
44 |
45 | return (
46 |
54 | );
55 | }
56 |
57 | export default Home;
58 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useUserData.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { API_URL } from '../config';
3 | import { useAuth } from '../helpers/AuthContent';
4 |
5 | const useUserData = (username, usePrivateProfile = false) => {
6 | const [userData, setUserData] = useState(null);
7 | const [userLoading, setUserLoading] = useState(true);
8 | const [userError, setUserError] = useState(null);
9 | const { token } = useAuth();
10 |
11 | useEffect(() => {
12 | const fetchUserData = async () => {
13 | try {
14 | let url, headers = {};
15 |
16 | if (usePrivateProfile) {
17 | // Use private profile endpoint for authenticated user's own profile
18 | url = `${API_URL}/api/v0/privateprofile`;
19 | headers = {
20 | 'Authorization': `Bearer ${token}`,
21 | 'Content-Type': 'application/json'
22 | };
23 | } else {
24 | // Use public user endpoint for viewing other users' profiles
25 | url = `${API_URL}/api/v0/userinfo/${username}`;
26 | if (token) {
27 | headers = {
28 | 'Authorization': `Bearer ${token}`,
29 | 'Content-Type': 'application/json'
30 | };
31 | }
32 | }
33 |
34 | const response = await fetch(url, { headers });
35 | if (!response.ok) {
36 | throw new Error('Failed to fetch user data');
37 | }
38 | const data = await response.json();
39 | setUserData(data);
40 | } catch (error) {
41 | console.error('Error fetching user data:', error);
42 | setUserError(error.toString());
43 | } finally {
44 | setUserLoading(false);
45 | }
46 | };
47 |
48 | if (username || usePrivateProfile) {
49 | fetchUserData();
50 | }
51 | }, [username, usePrivateProfile, token]);
52 |
53 | return { userData, userLoading, userError };
54 | };
55 |
56 | export default useUserData;
57 |
--------------------------------------------------------------------------------
/backend/handlers/bets/betutils/feeutils.go:
--------------------------------------------------------------------------------
1 | package betutils
2 |
3 | import (
4 | "log"
5 | "socialpredict/handlers/tradingdata"
6 | "socialpredict/models"
7 | "socialpredict/setup"
8 |
9 | "gorm.io/gorm"
10 | )
11 |
12 | // appConfig holds the loaded application configuration accessible within the package
13 | var appConfig *setup.EconomicConfig
14 |
15 | func init() {
16 | var err error
17 | appConfig, err = setup.LoadEconomicsConfig()
18 | if err != nil {
19 | log.Fatalf("Failed to load configuration: %v", err)
20 | }
21 | }
22 |
23 | func GetBetFees(db *gorm.DB, user *models.User, betRequest models.Bet) int64 {
24 |
25 | MarketID := betRequest.MarketID
26 |
27 | initialBetFee := getUserInitialBetFee(db, MarketID, user)
28 | transactionFee := getTransactionFee(betRequest)
29 |
30 | sumOfBetFees := initialBetFee + transactionFee
31 |
32 | return sumOfBetFees
33 | }
34 |
35 | // Get initial bet fee, if applicable, for user on market.
36 | // If this is the first bet on this market for the user, apply a fee.
37 | func getUserInitialBetFee(db *gorm.DB, marketID uint, user *models.User) int64 {
38 | // Fetch bets for the market
39 | allBetsOnMarket := tradingdata.GetBetsForMarket(db, marketID)
40 |
41 | // Check if the user has placed any bets on this market
42 | for _, bet := range allBetsOnMarket {
43 | if bet.Username == user.Username {
44 | // User has placed a bet, so no initial fee is applicable
45 | return 0
46 | }
47 | }
48 |
49 | // This is the user's first bet on this market, apply the initial bet fee
50 | return appConfig.Economics.Betting.BetFees.InitialBetFee
51 | }
52 |
53 | func getTransactionFee(betRequest models.Bet) int64 {
54 |
55 | var transactionFee int64
56 |
57 | // if amount > 0, buying share, else selling share
58 | if betRequest.Amount > 0 {
59 | transactionFee = appConfig.Economics.Betting.BetFees.BuySharesFee
60 | } else {
61 | transactionFee = appConfig.Economics.Betting.BetFees.SellSharesFee
62 | }
63 |
64 | return transactionFee
65 | }
66 |
--------------------------------------------------------------------------------
/backend/handlers/bets/betutils/betutils_test.go:
--------------------------------------------------------------------------------
1 | package betutils
2 |
3 | import (
4 | "socialpredict/models"
5 | "socialpredict/models/modelstesting"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestCheckMarketStatus(t *testing.T) {
11 |
12 | db := modelstesting.NewFakeDB(t)
13 |
14 | resolvedMarket := models.Market{
15 | ID: 1,
16 | IsResolved: true,
17 | ResolutionDateTime: time.Now().Add(-time.Hour),
18 | }
19 | closedMarket := models.Market{
20 | ID: 2,
21 | IsResolved: false,
22 | ResolutionDateTime: time.Now().Add(-time.Hour),
23 | }
24 | openMarket := models.Market{
25 | ID: 3,
26 | IsResolved: false,
27 | ResolutionDateTime: time.Now().Add(time.Hour),
28 | }
29 |
30 | db.Create(&resolvedMarket)
31 | db.Create(&closedMarket)
32 | db.Create(&openMarket)
33 |
34 | tests := []struct {
35 | name string
36 | marketID uint
37 | expectsErr bool
38 | errMsg string
39 | }{
40 | {
41 | name: "Market not found",
42 | marketID: 999,
43 | expectsErr: true,
44 | errMsg: "market not found",
45 | },
46 | {
47 | name: "Resolved market",
48 | marketID: 1,
49 | expectsErr: true,
50 | errMsg: "cannot place a bet on a resolved market",
51 | },
52 | {
53 | name: "Closed market",
54 | marketID: 2,
55 | expectsErr: true,
56 | errMsg: "cannot place a bet on a closed market",
57 | },
58 | {
59 | name: "Open market",
60 | marketID: 3,
61 | expectsErr: false,
62 | },
63 | }
64 |
65 | for _, test := range tests {
66 | t.Run(test.name, func(t *testing.T) {
67 | err := CheckMarketStatus(db, test.marketID)
68 | if (err != nil) != test.expectsErr {
69 | t.Errorf("got error = %v, expected error = %v", err, test.expectsErr)
70 | }
71 | if err != nil && test.expectsErr && err.Error() != test.errMsg {
72 | t.Errorf("expected error message %v, got %v", test.errMsg, err.Error())
73 | }
74 | })
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/backend/logging/loggingutils.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "log"
5 | "reflect"
6 | "runtime"
7 | )
8 |
9 | // Logger interface for flexible logging.
10 | type Logger interface {
11 | Fatalf(format string, args ...interface{})
12 | Printf(format string, args ...interface{}) // Add other log levels as needed.
13 | }
14 |
15 | // DefaultLogger provides default implementations for Logger methods.
16 | type DefaultLogger struct{}
17 |
18 | func (DefaultLogger) Fatalf(format string, args ...interface{}) {
19 | log.Fatalf(format, args...)
20 | }
21 |
22 | func (DefaultLogger) Printf(format string, args ...interface{}) {
23 | log.Printf(format, args...)
24 | }
25 |
26 | // LogAnyType logs any type of variable with its type, value, and length (if applicable).
27 | func LogAnyType(variable interface{}, variableName string) {
28 | // Get the variable type using reflection
29 | varType := reflect.TypeOf(variable)
30 | // Get the caller function details for contextual logging
31 | _, file, line, ok := runtime.Caller(1)
32 | location := ""
33 | if ok {
34 | location = file + ":" + string(rune(line))
35 | }
36 |
37 | // Convert the variable to a reflect.Value to access its value
38 | varValue := reflect.ValueOf(variable)
39 |
40 | // Initialize length variable
41 | length := -1 // Default to -1 to indicate non-applicable
42 |
43 | // Check if the variable is of a type that has a length
44 | if varValue.Kind() == reflect.Array || varValue.Kind() == reflect.Slice || varValue.Kind() == reflect.Map || varValue.Kind() == reflect.String {
45 | length = varValue.Len()
46 | }
47 |
48 | // Log the type, value, and length (if applicable) of the variable
49 | if length >= 0 {
50 | log.Printf("[%s] Variable Name: '%s', Type: %s, Length: %d, Value: %v\n", location, variableName, varType, length, varValue)
51 | } else {
52 | log.Printf("[%s] Variable Name: '%s', Type: %s, Value: %v\n", location, variableName, varType, varValue)
53 | }
54 | }
55 |
56 | func LogMsg(input string) {
57 | log.Printf("%s", input)
58 | }
59 |
--------------------------------------------------------------------------------
/backend/errors/httperror_test.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "log"
7 | "net/http"
8 | "net/http/httptest"
9 | "strings"
10 | "testing"
11 | )
12 |
13 | func TestHandleHTTPError(t *testing.T) {
14 | tests := []struct {
15 | name string
16 | w *httptest.ResponseRecorder
17 | err error
18 | statusCode int
19 | userMessage string
20 | output string
21 | wrappedMessage string
22 | res bool
23 | }{
24 | {
25 | name: "Nil",
26 | w: httptest.NewRecorder(),
27 | err: nil,
28 | statusCode: http.StatusOK,
29 | userMessage: "",
30 | output: "",
31 | wrappedMessage: "",
32 | res: false,
33 | },
34 | {
35 | name: "Server Error 500",
36 | w: httptest.NewRecorder(),
37 | err: errors.New("foo"),
38 | statusCode: http.StatusInternalServerError,
39 | userMessage: "bar",
40 | output: "Error: foo",
41 | wrappedMessage: "{\"error\":\"bar\"}",
42 | res: true,
43 | },
44 | }
45 | for _, test := range tests {
46 | t.Run(test.name, func(t *testing.T) {
47 | currentWriter := log.Writer()
48 | var buf bytes.Buffer
49 | log.SetOutput(&buf)
50 | res := HandleHTTPError(test.w, test.err, test.statusCode, test.userMessage)
51 | log.SetOutput(currentWriter)
52 | if test.res != res {
53 | t.Errorf("got %t, want %t", test.res, res)
54 | }
55 | if test.w.Code != test.statusCode {
56 | t.Errorf("got %d, want %d", test.statusCode, test.w.Code)
57 | }
58 | if !strings.Contains(test.w.Body.String(), test.wrappedMessage) {
59 | t.Errorf("got %s, want %s", strings.TrimRight(test.w.Body.String(), "\n"), test.wrappedMessage)
60 | }
61 | if !strings.Contains(buf.String(), test.output) {
62 | t.Errorf("got Error%s, want %s", strings.SplitN(strings.TrimRight(buf.String(), "\n"), "Error", 1)[0], test.output)
63 | }
64 | })
65 |
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/scripts/docker-compose-dev.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | container_name: ${POSTGRES_CONTAINER_NAME}
4 | image: postgres:16.6-alpine
5 | environment:
6 | POSTGRES_USER: ${POSTGRES_USER}
7 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
8 | POSTGRES_DB: ${POSTGRES_DATABASE}
9 | volumes:
10 | - pgdata:/var/lib/postgresql/data
11 | networks:
12 | - socialpredict_dev_network
13 | ports:
14 | - "${DB_PORT:-5432}:5432"
15 | healthcheck:
16 | test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"]
17 | interval: 2s
18 | timeout: 3s
19 | retries: 30
20 |
21 | backend:
22 | container_name: "${BACKEND_CONTAINER_NAME}"
23 | image: socialpredict-dev-backend:latest
24 | environment:
25 | APP_ENV: ${APP_ENV}
26 | CONTAINER_NAME: ${BACKEND_CONTAINER_NAME}
27 | DB_HOST: db
28 | DB_PORT: 5432
29 | DB_USER: ${POSTGRES_USER}
30 | DB_PASSWORD: ${POSTGRES_PASSWORD}
31 | DB_PASS: ${POSTGRES_PASSWORD}
32 | DB_NAME: ${POSTGRES_DATABASE}
33 | env_file:
34 | - ../.env
35 | ports:
36 | - "${BACKEND_PORT}:8080"
37 | volumes:
38 | - ../backend:/src # keep for live dev (air/nodemon etc.)
39 | networks:
40 | - socialpredict_dev_network
41 | depends_on:
42 | db:
43 | condition: service_healthy
44 |
45 | frontend:
46 | container_name: "${FRONTEND_CONTAINER_NAME}"
47 | image: socialpredict-dev-frontend:latest
48 | environment:
49 | APP_ENV: ${APP_ENV}
50 | networks:
51 | - socialpredict_dev_network
52 | env_file:
53 | - ../.env
54 | volumes:
55 | - ../frontend:/app
56 | - socialpredict_dev_node_modules:/app/node_modules
57 | ports:
58 | - "${FRONTEND_PORT}:5173"
59 | depends_on:
60 | - backend
61 |
62 | networks:
63 | socialpredict_dev_network:
64 | name: socialpredict_dev_network
65 |
66 | volumes:
67 | pgdata: {}
68 | socialpredict_dev_node_modules: {}
69 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | @apply bg-primary-background;
7 | margin: 0;
8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10 | sans-serif;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | }
14 |
15 | code {
16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
17 | monospace;
18 | }
19 |
20 | @layer components {
21 | /* Shared cells */
22 | .sp-cell-username {
23 | @apply min-w-0;
24 | }
25 | .sp-cell-num {
26 | @apply text-right tabular-nums;
27 | }
28 | .sp-chip {
29 | @apply px-2 py-0.5 rounded-full text-[10px] sm:text-xs bg-emerald-600/20;
30 | }
31 | .sp-subline {
32 | @apply text-[10px] sm:text-xs text-slate-400;
33 | }
34 | .sp-ellipsis {
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | white-space: nowrap;
38 | }
39 |
40 | /* Bets */
41 | .sp-grid-bets-header {
42 | @apply hidden sm:grid grid-cols-5 gap-3 px-4 py-2 text-xs text-gray-400;
43 | }
44 | .sp-grid-bets-row {
45 | @apply grid grid-cols-3 sm:grid-cols-5 gap-2 sm:gap-3 rounded-xl bg-slate-800 px-3 py-3;
46 | }
47 |
48 | /* Leaderboard */
49 | .sp-grid-leaderboard-header {
50 | @apply hidden sm:grid grid-cols-7 gap-3 px-4 py-2 text-xs text-gray-400;
51 | }
52 | .sp-grid-leaderboard-row {
53 | @apply grid grid-cols-2 sm:grid-cols-7 gap-2 sm:gap-3 rounded-xl bg-slate-800 px-3 py-3;
54 | }
55 |
56 | /* Setup Configuration */
57 | .sp-grid-setup-header {
58 | @apply hidden sm:grid grid-cols-3 gap-3 px-4 py-2 text-xs text-gray-400;
59 | }
60 | .sp-grid-setup-row {
61 | @apply grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-3 rounded-xl bg-slate-800 px-3 py-3;
62 | }
63 |
64 | /* Optional smallest device tweaks (<=390px) */
65 | @media (max-width: 390px) {
66 | .sp-tight { letter-spacing: 0; }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/backend/handlers/users/privateuser/privateuser_test.go:
--------------------------------------------------------------------------------
1 | package privateuser
2 |
3 | import (
4 | "encoding/json"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "socialpredict/models/modelstesting"
9 | "socialpredict/util"
10 | )
11 |
12 | func TestGetPrivateProfileUserResponse_Success(t *testing.T) {
13 | db := modelstesting.NewFakeDB(t)
14 | orig := util.DB
15 | util.DB = db
16 | t.Cleanup(func() {
17 | util.DB = orig
18 | })
19 |
20 | t.Setenv("JWT_SIGNING_KEY", "test-secret-key-for-testing")
21 |
22 | user := modelstesting.GenerateUser("alice", 0)
23 | if err := db.Create(&user).Error; err != nil {
24 | t.Fatalf("failed to create user: %v", err)
25 | }
26 |
27 | token := modelstesting.GenerateValidJWT(user.Username)
28 |
29 | req := httptest.NewRequest("GET", "/v0/privateprofile", nil)
30 | req.Header.Set("Authorization", "Bearer "+token)
31 | rec := httptest.NewRecorder()
32 |
33 | GetPrivateProfileUserResponse(rec, req)
34 |
35 | if rec.Code != 200 {
36 | t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
37 | }
38 |
39 | var resp CombinedUserResponse
40 | if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
41 | t.Fatalf("failed to decode response: %v", err)
42 | }
43 |
44 | if resp.Username != user.Username {
45 | t.Fatalf("expected username %q, got %q", user.Username, resp.Username)
46 | }
47 | if resp.PrivateUser.Email != user.PrivateUser.Email {
48 | t.Fatalf("expected email %q, got %q", user.PrivateUser.Email, resp.PrivateUser.Email)
49 | }
50 | }
51 |
52 | func TestGetPrivateProfileUserResponse_Unauthorized(t *testing.T) {
53 | db := modelstesting.NewFakeDB(t)
54 | orig := util.DB
55 | util.DB = db
56 | t.Cleanup(func() {
57 | util.DB = orig
58 | })
59 |
60 | t.Setenv("JWT_SIGNING_KEY", "test-secret-key-for-testing")
61 |
62 | req := httptest.NewRequest("GET", "/v0/privateprofile", nil)
63 | rec := httptest.NewRecorder()
64 |
65 | GetPrivateProfileUserResponse(rec, req)
66 |
67 | if rec.Code != 401 {
68 | t.Fatalf("expected status 401, got %d", rec.Code)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/backend/handlers/math/payout/resolvemarketcore.go:
--------------------------------------------------------------------------------
1 | package payout
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | positionsmath "socialpredict/handlers/math/positions"
7 | usersHandlers "socialpredict/handlers/users"
8 | "socialpredict/models"
9 | "strconv"
10 |
11 | "gorm.io/gorm"
12 | )
13 |
14 | func DistributePayoutsWithRefund(market *models.Market, db *gorm.DB) error {
15 | if market == nil {
16 | return errors.New("market is nil")
17 | }
18 |
19 | switch market.ResolutionResult {
20 | case "N/A":
21 | return refundAllBets(market, db)
22 | case "YES", "NO":
23 | return calculateAndAllocateProportionalPayouts(market, db)
24 | case "PROB":
25 | return fmt.Errorf("probabilistic resolution is not yet supported")
26 | default:
27 | return fmt.Errorf("unsupported resolution result: %q", market.ResolutionResult)
28 | }
29 | }
30 |
31 | func calculateAndAllocateProportionalPayouts(market *models.Market, db *gorm.DB) error {
32 | // Step 1: Convert market ID formats
33 | marketIDStr := strconv.FormatInt(market.ID, 10)
34 |
35 | // Step 2: Calculate market positions with resolved valuation
36 | displayPositions, err := positionsmath.CalculateMarketPositions_WPAM_DBPM(db, marketIDStr)
37 | if err != nil {
38 | return err
39 | }
40 |
41 | // Step 3: Pay out each user their resolved valuation
42 | for _, pos := range displayPositions {
43 | if pos.Value > 0 {
44 | if err := usersHandlers.ApplyTransactionToUser(pos.Username, pos.Value, db, usersHandlers.TransactionWin); err != nil {
45 | return err
46 | }
47 | }
48 | }
49 |
50 | return nil
51 | }
52 |
53 | func refundAllBets(market *models.Market, db *gorm.DB) error {
54 | // Retrieve all bets associated with the market
55 | var bets []models.Bet
56 | if err := db.Where("market_id = ?", market.ID).Find(&bets).Error; err != nil {
57 | return err
58 | }
59 |
60 | // Refund each bet to the user
61 | for _, bet := range bets {
62 | if err := usersHandlers.ApplyTransactionToUser(bet.Username, bet.Amount, db, usersHandlers.TransactionRefund); err != nil {
63 | return err
64 | }
65 | }
66 |
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/scripts/seed_markets_go.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # seed_markets_go.sh
4 | # Wrapper script to run the Go market seeder with proper module setup
5 | # Usage: ./seed_markets_go.sh [SQL_FILE]
6 | # Default: example_markets.sql
7 |
8 | set -e
9 |
10 | # Colors for output
11 | RED='\033[0;31m'
12 | GREEN='\033[0;32m'
13 | YELLOW='\033[1;33m'
14 | NC='\033[0m' # No Color
15 |
16 | # Function to print colored output
17 | print_status() {
18 | echo -e "${GREEN}[INFO]${NC} $1"
19 | }
20 |
21 | print_warning() {
22 | echo -e "${YELLOW}[WARNING]${NC} $1"
23 | }
24 |
25 | print_error() {
26 | echo -e "${RED}[ERROR]${NC} $1"
27 | }
28 |
29 | # Get script directory
30 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
32 |
33 | print_status "SocialPredict Go Market Seeder"
34 | echo "==============================="
35 |
36 | # Check if Go is installed
37 | if ! command -v go &> /dev/null; then
38 | print_error "Go is not installed or not in PATH"
39 | print_error "Please install Go from https://golang.org/dl/"
40 | exit 1
41 | fi
42 |
43 | print_status "Go found: $(go version)"
44 |
45 | # Navigate to scripts directory
46 | cd "$SCRIPT_DIR"
47 |
48 | # Check if backend go.mod exists
49 | if [ ! -f "$PROJECT_ROOT/backend/go.mod" ]; then
50 | print_error "Backend go.mod not found at $PROJECT_ROOT/backend/"
51 | print_error "Make sure you're running this from the correct directory"
52 | exit 1
53 | fi
54 |
55 | # Initialize go module in scripts directory if needed
56 | if [ ! -f "go.mod" ]; then
57 | print_status "Initializing Go module for scripts..."
58 | go mod init socialpredict-scripts
59 | go mod edit -replace socialpredict="$PROJECT_ROOT/backend"
60 | fi
61 |
62 | # Download dependencies
63 | print_status "Downloading Go dependencies..."
64 | go mod tidy
65 |
66 | # Build and run the seeder
67 | print_status "Building and running market seeder..."
68 | if go run seed_markets.go "$@"; then
69 | print_status "Market seeding completed successfully!"
70 | else
71 | print_error "Market seeding failed!"
72 | exit 1
73 | fi
--------------------------------------------------------------------------------
/backend/handlers/math/positions/valuation.go:
--------------------------------------------------------------------------------
1 | package positionsmath
2 |
3 | import (
4 | "fmt"
5 | "math"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | type UserValuationResult struct {
11 | Username string
12 | RoundedValue int64
13 | }
14 |
15 | func CalculateRoundedUserValuationsFromUserMarketPositions(
16 | db *gorm.DB,
17 | marketID uint,
18 | userPositions map[string]UserMarketPosition,
19 | currentProbability float64,
20 | totalVolume int64,
21 | isResolved bool,
22 | resolutionResult string,
23 | ) (map[string]UserValuationResult, error) {
24 | result := make(map[string]UserValuationResult)
25 | var finalProb float64
26 |
27 | finalProb = getFinalProbabilityFromMarketModel(currentProbability, isResolved, resolutionResult)
28 |
29 | for username, pos := range userPositions {
30 | var floatVal float64
31 |
32 | if isResolved {
33 | floatVal = 0
34 | if resolutionResult == "YES" {
35 | floatVal = float64(pos.YesSharesOwned)
36 | } else if resolutionResult == "NO" {
37 | floatVal = float64(pos.NoSharesOwned)
38 | }
39 | } else {
40 | if pos.YesSharesOwned > 0 {
41 | floatVal = float64(pos.YesSharesOwned) * finalProb
42 | } else if pos.NoSharesOwned > 0 {
43 | floatVal = float64(pos.NoSharesOwned) * (1 - finalProb)
44 | }
45 | }
46 |
47 | fmt.Printf("user=%s YES=%d NO=%d isResolved=%v result=%s val=%v\n",
48 | username, pos.YesSharesOwned, pos.NoSharesOwned, isResolved, resolutionResult, floatVal)
49 |
50 | roundedVal := int64(math.Round(floatVal))
51 |
52 | result[username] = UserValuationResult{
53 | Username: username,
54 | RoundedValue: roundedVal,
55 | }
56 | }
57 |
58 | adjusted, err := AdjustUserValuationsToMarketVolume(db, marketID, result, totalVolume)
59 | if err != nil {
60 | return nil, err
61 | }
62 | return adjusted, nil
63 | }
64 |
65 | func getFinalProbabilityFromMarketModel(
66 | currentProbability float64,
67 | isResolved bool,
68 | resolutionResult string,
69 | ) float64 {
70 | if isResolved {
71 | switch resolutionResult {
72 | case "YES":
73 | return 1.0
74 | case "NO":
75 | return 0.0
76 | default:
77 | return currentProbability
78 | }
79 | }
80 | return currentProbability
81 | }
82 |
--------------------------------------------------------------------------------
/backend/go.mod:
--------------------------------------------------------------------------------
1 | module socialpredict
2 |
3 | go 1.25
4 |
5 | require (
6 | github.com/brianvoe/gofakeit v3.18.0+incompatible
7 | github.com/glebarez/sqlite v1.11.0
8 | github.com/go-playground/validator/v10 v10.27.0
9 | github.com/golang-jwt/jwt/v4 v4.5.2
10 | github.com/google/uuid v1.6.0
11 | github.com/gorilla/mux v1.8.1
12 | github.com/joho/godotenv v1.5.1
13 | github.com/microcosm-cc/bluemonday v1.0.27
14 | github.com/rs/cors v1.11.1
15 | github.com/stretchr/testify v1.8.4
16 | github.com/yuin/goldmark v1.7.13
17 | golang.org/x/crypto v0.36.0
18 | golang.org/x/time v0.12.0
19 | gopkg.in/yaml.v3 v3.0.1
20 | gorm.io/driver/postgres v1.5.9
21 | gorm.io/gorm v1.25.12
22 | )
23 |
24 | require (
25 | github.com/aymerick/douceur v0.2.0 // indirect
26 | github.com/davecgh/go-spew v1.1.1 // indirect
27 | github.com/dustin/go-humanize v1.0.1 // indirect
28 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect
29 | github.com/glebarez/go-sqlite v1.22.0 // indirect
30 | github.com/go-playground/locales v0.14.1 // indirect
31 | github.com/go-playground/universal-translator v0.18.1 // indirect
32 | github.com/gorilla/css v1.0.1 // indirect
33 | github.com/jackc/pgpassfile v1.0.0 // indirect
34 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
35 | github.com/jackc/pgx/v5 v5.7.1 // indirect
36 | github.com/jackc/puddle/v2 v2.2.2 // indirect
37 | github.com/jinzhu/inflection v1.0.0 // indirect
38 | github.com/jinzhu/now v1.1.5 // indirect
39 | github.com/kr/text v0.2.0 // indirect
40 | github.com/leodido/go-urn v1.4.0 // indirect
41 | github.com/mattn/go-isatty v0.0.20 // indirect
42 | github.com/ncruces/go-strftime v0.1.9 // indirect
43 | github.com/pmezard/go-difflib v1.0.0 // indirect
44 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
45 | github.com/rogpeppe/go-internal v1.12.0 // indirect
46 | golang.org/x/net v0.38.0 // indirect
47 | golang.org/x/sync v0.12.0 // indirect
48 | golang.org/x/sys v0.31.0 // indirect
49 | golang.org/x/text v0.23.0 // indirect
50 | modernc.org/libc v1.60.1 // indirect
51 | modernc.org/mathutil v1.6.0 // indirect
52 | modernc.org/memory v1.8.0 // indirect
53 | modernc.org/sqlite v1.33.1 // indirect
54 | )
55 |
--------------------------------------------------------------------------------
/frontend/src/api/marketsApi.js:
--------------------------------------------------------------------------------
1 | import { API_URL } from '../config';
2 |
3 | /**
4 | * Search markets by query and status
5 | * @param {string} query - Search query
6 | * @param {string} status - Market status filter ('all', 'active', 'closed', 'resolved')
7 | * @param {number} limit - Maximum number of results (optional, defaults to 20)
8 | * @returns {Promise} Search results object
9 | */
10 | export const searchMarkets = async (query, status = 'all', limit = 20) => {
11 | if (!query?.trim()) {
12 | throw new Error('Search query is required');
13 | }
14 |
15 | const params = new URLSearchParams({
16 | query: query.trim(),
17 | status: status,
18 | limit: limit.toString()
19 | });
20 |
21 | const response = await fetch(`${API_URL}/v0/markets/search?${params}`);
22 |
23 | if (!response.ok) {
24 | const errorText = await response.text();
25 | throw new Error(`Search failed: ${response.status} ${errorText}`);
26 | }
27 |
28 | return await response.json();
29 | };
30 |
31 | /**
32 | * Get markets by status (existing endpoint)
33 | * @param {string} status - Market status ('active', 'closed', 'resolved', 'all')
34 | * @returns {Promise} Markets data
35 | */
36 | export const getMarketsByStatus = async (status = 'all') => {
37 | const endpoint = status === 'all'
38 | ? `${API_URL}/v0/markets`
39 | : `${API_URL}/v0/markets/${status}`;
40 |
41 | const response = await fetch(endpoint);
42 |
43 | if (!response.ok) {
44 | throw new Error(`Failed to fetch ${status} markets: ${response.status}`);
45 | }
46 |
47 | return await response.json();
48 | };
49 |
50 | /**
51 | * Get market details by ID
52 | * @param {string|number} marketId - Market ID
53 | * @returns {Promise} Market details
54 | */
55 | export const getMarketDetails = async (marketId) => {
56 | if (!marketId) {
57 | throw new Error('Market ID is required');
58 | }
59 |
60 | const response = await fetch(`${API_URL}/v0/markets/${marketId}`);
61 |
62 | if (!response.ok) {
63 | throw new Error(`Failed to fetch market details: ${response.status}`);
64 | }
65 |
66 | return await response.json();
67 | };
68 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to SocialPredict
2 |
3 | We welcome contributions from the community and are pleased that you're interested in participating in the development of SocialPredict. Here are the guidelines we'd like you to follow:
4 |
5 | ## How to Contribute
6 |
7 | 1. **Fork the Repository**
8 | - Start by forking the SocialPredict repository to your GitHub account.
9 |
10 | 2. **Clone Your Fork**
11 | - Clone your fork to your local machine to start making changes.
12 |
13 | 3. **Create a Branch**
14 | - Create a new branch in your local repository before starting your work.
15 | - Use a clear branch name that describes the intent of your work, such as `feature/add-login` or `fix/header-bug`.
16 | - Make sure your branch name starts with `feature`, `fix`, `refactor` or `doc` followed by a `/` and description.
17 |
18 | 4. **Commit Your Changes**
19 | - Keep your commits as atomic as possible, each addressing a single concern.
20 | - Use clear and descriptive commit messages.
21 |
22 | 5. **Push Your Changes**
23 | - Push your changes to your fork on GitHub.
24 |
25 | 6. **Submit a Pull Request**
26 | - Open a pull request to the main SocialPredict repository.
27 | - Provide a concise description of the changes and the reasoning behind them.
28 |
29 | ## Reporting Issues
30 |
31 | If you find a bug or have a feature request, please use the [GitHub Issues](https://github.com/openpredictionmarkets/socialpredict/issues) tab to report them. Before creating a new issue, check to ensure the issue hasn't already been reported. When filing an issue, please include:
32 |
33 | - A clear title and description.
34 | - As much relevant information as possible.
35 | - A code sample or an executable test case demonstrating the expected behavior that is not occurring.
36 |
37 | ## Conduct
38 |
39 | We are committed to providing a welcoming and inspiring community for all. Please take a moment to read our [Code of Conduct](https://github.com/openpredictionmarkets/socialpredict/blob/main/CODE_OF_CONDUCT.md) before participating.
40 |
41 | ## Questions?
42 |
43 | If you have any questions, please feel free to ask them on our Github discussions page or reach out directly via GitHub issues.
44 |
45 | Thank you for contributing to SocialPredict!
46 |
--------------------------------------------------------------------------------
/README/CHECKPOINTS/CHECKPOINT20250803-04.md:
--------------------------------------------------------------------------------
1 | 📄 Project Plan – Fix Search with Tab-Synced Query
2 | Objective
3 | The current search behavior does not properly reflect the active tab’s status filter (Active, Closed, Resolved, All). This leads to confusion when the correct search results are fetched but not visually tied to the user’s selected tab.
4 |
5 | This task ensures that:
6 |
7 | Tab selection and search are fully synchronized
8 |
9 | The correct status is included in all backend /markets/search queries
10 |
11 | The system continues supporting fallback results and status-filtered views
12 |
13 | ✅ Milestone 1 – Create TAB_TO_STATUS map utility
14 | Tasks:
15 | Create a new file at src/utils/statusMap.js (or utils/statusMap.ts).
16 |
17 | Add and export the following constant:
18 |
19 | js
20 | Copy
21 | Edit
22 | export const TAB_TO_STATUS = {
23 | Active: "active",
24 | Closed: "closed",
25 | Resolved: "resolved",
26 | All: "all", // Backend interprets "all" as no filter
27 | };
28 | Use this mapping wherever status logic is tied to UI tab labels.
29 |
30 | Acceptance Criteria:
31 | File exists and is importable.
32 |
33 | Mapping is correct and matches backend expectations.
34 |
35 | ✅ Milestone 2 – Update Markets.jsx to manage search state and trigger API calls
36 | Tasks:
37 | In pages/markets/Markets.jsx, import the utility:
38 |
39 | js
40 | Copy
41 | Edit
42 | import { TAB_TO_STATUS } from "../../utils/statusMap";
43 | Add state variables:
44 |
45 | js
46 | Copy
47 | Edit
48 | const [activeTab, setActiveTab] = useState("Active");
49 | const [query, setQuery] = useState("");
50 | Add useEffect to trigger API calls on either state change:
51 |
52 | js
53 | Copy
54 | Edit
55 | useEffect(() => {
56 | const status = TAB_TO_STATUS[activeTab];
57 | fetch(`/v0/markets/search?query=${encodeURIComponent(query)}&status=${status}`)
58 | .then(res => res.json())
59 | .then(setResults)
60 | .catch(console.error);
61 | }, [query, activeTab]);
62 | Notes:
63 | query is controlled by the search input.
64 |
65 | activeTab is controlled by the tabs.
66 |
67 | Hook glues them together.
68 |
69 | Acceptance Criteria:
70 | API is called with updated query and status params as either value changes.
71 |
72 | Correct search results populate for each tab + query combo.
73 |
74 |
--------------------------------------------------------------------------------