#!/usr/bin/env python3
import sys
import sqlite3
import random
import logging
import asyncio
import re
import json
import math
import requests
import base58
import hashlib
import ecdsa
from datetime import datetime, timedelta
from typing import Optional, List, Tuple
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
ConversationHandler,
MessageHandler,
filters
)
# ========== CONFIGURATION ==========
TELEGRAM_TOKEN = "8663175508:AAF6aV0VvEg7xBDH66s2K-iz5JPKIFPkqSY" # <-- INSERT YOUR BOT TOKEN HERE
ADMIN_USER_ID = 691311362 # <-- YOUR TELEGRAM USER ID
COOLDOWN_SECONDS = 2
BLOCKCYPHER_TOKEN = ""
START_POINTS = 1000
# Multiplayer Settings
MINIMUM_BET_USD = 0.0
FEE_PERCENTAGE = 0.0
FEE_ADDRESS = "Fee_WALLET"
# Prediction Settings
PREDICTION_MIN_BET = 10
PREDICTION_DURATION_SECONDS = 300
# Auto‑refresh interval for open positions (seconds) – now 1 second
REFRESH_INTERVAL = 1.0
# Bot virtual user (acts as the pool custodian)
BOT_USER_ID = 0 # will be created if not exists
BOT_POINTS = 999_999_999_999 # initial balance and reset target
# NEW: Bot placeholder amount for initial liquidity
BOT_PLACEHOLDER_AMOUNT = 1 # points
# Conversation states – we keep ADD_AMOUNT but it's no longer used for Buy UP/DOWN
ADD_AMOUNT = 1
# ========== LOGGING ==========
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# ========== DATABASE ==========
DB_PATH = "dice_bot.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS players (
user_id INTEGER PRIMARY KEY,
username TEXT,
points INTEGER DEFAULT 0,
banned INTEGER DEFAULT 0,
timeout_until TIMESTAMP
)''')
c.execute('''CREATE TABLE IF NOT EXISTS user_btc (
user_id INTEGER PRIMARY KEY,
btc_address TEXT UNIQUE,
private_key_wif TEXT,
last_tx_hash TEXT
)''')
c.execute('''CREATE TABLE IF NOT EXISTS withdraw_txs (
tx_hash TEXT PRIMARY KEY,
user_id INTEGER,
btc_amount REAL,
created_at TIMESTAMP
)''')
c.execute('''CREATE TABLE IF NOT EXISTS games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER,
challenger_id INTEGER,
challenged_id INTEGER,
bet REAL,
currency TEXT DEFAULT 'btc',
status TEXT DEFAULT 'pending',
winner_id INTEGER,
loser_id INTEGER,
created_at TIMESTAMP,
finished_at TIMESTAMP,
winner_amount REAL,
loser_amount REAL,
fee REAL
)''')
for col in ['winner_amount', 'loser_amount', 'fee', 'loser_id']:
try:
c.execute(f"ALTER TABLE games ADD COLUMN {col}")
except sqlite3.OperationalError:
pass
try:
c.execute("ALTER TABLE games ADD COLUMN currency TEXT DEFAULT 'btc'")
except sqlite3.OperationalError:
pass
c.execute('''CREATE TABLE IF NOT EXISTS point_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
amount INTEGER,
reason TEXT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
c.execute('''CREATE TABLE IF NOT EXISTS predictions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
amount INTEGER,
direction INTEGER, -- 1 = UP, 0 = DOWN
start_price REAL,
end_price REAL,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP,
resolved_at TIMESTAMP,
profit INTEGER,
currency TEXT DEFAULT 'points',
multiplier REAL DEFAULT 1.0
)''')
try:
c.execute("ALTER TABLE predictions ADD COLUMN currency TEXT DEFAULT 'points'")
except sqlite3.OperationalError:
pass
try:
c.execute("ALTER TABLE predictions ADD COLUMN multiplier REAL DEFAULT 1.0")
except sqlite3.OperationalError:
pass
conn.commit()
conn.close()
# Ensure bot player exists and has points
ensure_bot_player()
# ========== BOT PLAYER ==========
def ensure_bot_player():
"""Create the bot user as a player with a huge point balance."""
player = get_player(BOT_USER_ID)
if not player:
create_player(BOT_USER_ID, "Admin")
# Set points to BOT_POINTS (or add if exists)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE players SET points = ? WHERE user_id = ?", (BOT_POINTS, BOT_USER_ID))
conn.commit()
conn.close()
# ========== BTC/USD EXCHANGE RATE ==========
def get_btc_usd_rate() -> float:
"""
Fetch BTC/USD price from multiple APIs with fallback.
Tries: CoinGecko, Binance, CoinCap, Kraken, Coinbase.
Returns a realistic price or 30000.0 as fallback.
"""
sources = [
(
"https://[Log in to view URL]",
lambda data: data.get('bitcoin', {}).get('usd'),
"CoinGecko"
),
(
"https://[Log in to view URL]",
lambda data: float(data.get('price', 0)),
"Binance"
),
(
"https://[Log in to view URL]",
lambda data: float(data.get('data', {}).get('priceUsd', 0)),
"CoinCap"
),
(
"https://[Log in to view URL]",
lambda data: float(data.get('result', {}).get('XXBTZUSD', {}).get('c', [0])[0]),
"Kraken"
),
(
"https://[Log in to view URL]",
lambda data: float(data.get('data', {}).get('amount', 0)),
"Coinbase"
)
]
for url, parser, name in sources:
try:
resp = requests.get(url, timeout=5)
if resp.status_code == 200:
data = resp.json()
price = parser(data)
if price and isinstance(price, (int, float)) and 1000 < price < 200000:
logger.debug(f"BTC price fetched from {name}: ${price:.2f}")
return price
else:
logger.warning(f"Unrealistic price from {name}: {price}")
else:
logger.warning(f"API {name} returned status {resp.status_code}")
except Exception as e:
logger.warning(f"Failed to fetch from {name}: {e}")
logger.error("All BTC price sources failed. Using fallback 30000.")
return 30000.0
def get_btc_price_history(minutes: int = 15, interval: int = 5, round_to_interval: bool = True) -> List[Tuple[datetime, float, float, float]]:
try:
now = datetime.now()
if round_to_interval:
mins = (now.minute // interval) * interval
rounded_now = now.replace(minute=mins, second=0, microsecond=0)
start = rounded_now - timedelta(minutes=minutes)
else:
start = now - timedelta(minutes=minutes)
from_ts = int(start.timestamp())
to_ts = int(now.timestamp())
url = f"https://[Log in to view URL]"
resp = requests.get(url, timeout=10)
if resp.status_code != 200:
return []
data = resp.json()
prices = data.get('prices', [])
if not prices:
return []
history = []
for i in range(0, minutes // interval + 1):
target_time = start + timedelta(minutes=i * interval)
closest = min(prices, key=lambda x: abs(x[0] - target_time.timestamp()*1000))
history.append((target_time, closest[1]))
result = []
for i, (dt, price) in enumerate(history):
if i == 0:
change = 0.0
change_pct = 0.0
else:
prev_price = history[i-1][1]
change = price - prev_price
change_pct = (change / prev_price) * 100 if prev_price else 0.0
result.append((dt, price, change, change_pct))
return result
except Exception as e:
logger.error(f"Error fetching price history: {e}")
return []
def get_price_at_time(target_time: datetime) -> Optional[float]:
"""
Get BTC price at a specific datetime (rounded to the nearest 5-minute interval).
Uses the price history from CoinGecko.
"""
# Fetch history from 10 minutes before to 5 minutes after to ensure we have data
now = datetime.now()
# We need to get history that includes target_time
# We'll fetch a range that covers it
start_time = target_time - timedelta(minutes=5)
end_time = target_time + timedelta(minutes=1)
# Use get_btc_price_history with specific start and end? We'll just fetch the last 15 minutes
# and find the closest to target_time.
history = get_btc_price_history(minutes=15, interval=5, round_to_interval=True)
if not history:
return None
# Find the entry with time closest to target_time (within a tolerance)
closest = min(history, key=lambda x: abs(x[0] - target_time))
if abs(closest[0] - target_time) < timedelta(minutes=1):
return closest[1]
return None
def estimate_volatility(price_history: List[Tuple[datetime, float, float, float]]) -> float:
"""Estimate volatility from price history (standard deviation of returns)."""
if len(price_history) < 2:
return 0.005 # default 0.5%
prices = [p[1] for p in price_history]
returns = [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]
if not returns:
return 0.005
mean = sum(returns) / len(returns)
variance = sum((r - mean) ** 2 for r in returns) / len(returns)
return max(0.001, math.sqrt(variance)) # at least 0.1%
# ========== PLAYER FUNCTIONS ==========
def get_player(user_id: int) -> Optional[dict]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT user_id, username, points, banned, timeout_until FROM players WHERE user_id = ?", (user_id,))
row = c.fetchone()
conn.close()
if row:
return {
"user_id": row[0],
"username": row[1],
"points": row[2],
"banned": row[3],
"timeout_until": row[4]
}
except Exception as e:
logger.error(f"Database error fetching player: {e}")
return None
def get_player_by_username(username: str) -> Optional[dict]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT user_id, username, points, banned, timeout_until FROM players WHERE username = ?", (username,))
row = c.fetchone()
conn.close()
if row:
return {
"user_id": row[0],
"username": row[1],
"points": row[2],
"banned": row[3],
"timeout_until": row[4]
}
except Exception as e:
logger.error(f"Error searching by username: {e}")
return None
def create_player(user_id: int, username: str):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"INSERT INTO players (user_id, username, points, banned, timeout_until) VALUES (?, ?, ?, 0, NULL)",
(user_id, username, START_POINTS)
)
conn.commit()
conn.close()
logger.info(f"New player: {username} ({user_id}) with {START_POINTS} points")
except Exception as e:
logger.error(f"Error creating player: {e}")
def update_points(user_id: int, delta: int, reason: str = None):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE players SET points = points + ? WHERE user_id = ?", (delta, user_id))
if reason is not None:
c.execute("INSERT INTO point_transactions (user_id, amount, reason) VALUES (?, ?, ?)",
(user_id, delta, reason))
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Error updating points: {e}")
def set_banned(user_id: int, banned: bool):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE players SET banned = ? WHERE user_id = ?", (1 if banned else 0, user_id))
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Error setting banned: {e}")
def set_timeout(user_id: int, until: datetime):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE players SET timeout_until = ? WHERE user_id = ?", (until.isoformat() if until else None, user_id))
conn.commit()
conn.close()
except Exception as e:
logger.error(f"Error setting timeout: {e}")
def is_player_blocked(player: dict) -> Tuple[bool, str]:
if player['banned'] == 1:
return True, "🚫 You are permanently banned."
if player['timeout_until']:
timeout_end = datetime.fromisoformat(player['timeout_until'])
if timeout_end > datetime.now():
remaining = (timeout_end - datetime.now()).total_seconds()
minutes = int(remaining // 60)
seconds = int(remaining % 60)
return True, f"⏳ You are blocked for {minutes} min. and {seconds} sec."
else:
set_timeout(player['user_id'], None)
return False, ""
return False, ""
def get_leaderboard(limit: int = 10) -> List[Tuple[str, int]]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT username, points FROM players WHERE banned = 0 AND user_id != ? ORDER BY points DESC LIMIT ?", (BOT_USER_ID, limit))
rows = c.fetchall()
conn.close()
return rows
except Exception as e:
logger.error(f"Error fetching leaderboard: {e}")
return []
# ========== BTC ADDRESS FUNCTIONS ==========
def get_user_by_btc_address(btc_address: str) -> Optional[int]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT user_id FROM user_btc WHERE btc_address = ?", (btc_address,))
row = c.fetchone()
conn.close()
return row[0] if row else None
except Exception as e:
logger.error(f"Error searching BTC address: {e}")
return None
def get_user_btc_address(user_id: int) -> Optional[str]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT btc_address FROM user_btc WHERE user_id = ?", (user_id,))
row = c.fetchone()
conn.close()
return row[0] if row else None
except Exception as e:
logger.error(f"Error fetching BTC address: {e}")
return None
def set_user_btc_address_and_key(user_id: int, btc_address: str, private_key_wif: str):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT OR REPLACE INTO user_btc (user_id, btc_address, private_key_wif, last_tx_hash) VALUES (?, ?, ?, NULL)",
(user_id, btc_address, private_key_wif))
conn.commit()
conn.close()
return True
except sqlite3.IntegrityError:
logger.warning(f"BTC address {btc_address} already used by another user.")
return False
except Exception as e:
logger.error(f"Error saving BTC address: {e}")
return False
def delete_user_btc_address(user_id: int) -> bool:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("DELETE FROM user_btc WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
return True
except Exception as e:
logger.error(f"Error deleting BTC address: {e}")
return False
def get_all_btc_addresses() -> List[Tuple[int, str]]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT user_id, btc_address FROM user_btc")
rows = c.fetchall()
conn.close()
return rows
except Exception as e:
logger.error(f"Error fetching all wallets: {e}")
return []
def get_user_btc_private_key(user_id: int) -> Optional[str]:
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT private_key_wif FROM user_btc WHERE user_id = ?", (user_id,))
row = c.fetchone()
conn.close()
return row[0] if row else None
except Exception as e:
logger.error(f"Error fetching private key: {e}")
return None
# ========== BITCOIN WALLET FUNCTIONS ==========
def wif_to_private_key(wif: str) -> bytes:
decoded = base58.b58decode(wif)
return decoded[1:-4]
def private_key_to_wif(private_key: bytes) -> str:
prefix = b'\x80'
payload = prefix + private_key
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
return base58.b58encode(payload + checksum).decode()
def private_key_to_address(private_key: bytes) -> str:
sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
vk = sk.get_verifying_key()
public_key = b'\x04' + vk.to_string()
sha256_hash = hashlib.sha256(public_key).digest()
ripemd160 = hashlib.new('ripemd160')
ripemd160.update(sha256_hash)
public_key_hash = ripemd160.digest()
address_payload = b'\x00' + public_key_hash
checksum = hashlib.sha256(hashlib.sha256(address_payload).digest()).digest()[:4]
return base58.b58encode(address_payload + checksum).decode()
def create_btc_wallet() -> Tuple[str, str]:
private_key = ecdsa.util.randrange(ecdsa.SECP256k1.order).to_bytes(32, 'big')
wif = private_key_to_wif(private_key)
address = private_key_to_address(private_key)
return wif, address
def import_btc_wallet(private_key: str) -> Tuple[str, str]:
try:
decoded = base58.b58decode(private_key)
if len(decoded) != 38:
return None, "Invalid WIF private key."
private_key_bytes = decoded[1:-4]
address = private_key_to_address(private_key_bytes)
return address, None
except Exception as e:
return None, f"Invalid private key: {str(e)}"
def get_btc_balance(address: str) -> float:
try:
url = f"https://[Log in to view URL]"
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
data = resp.json()
if address in data:
return data[address]['final_balance'] / 1e8
return 0.0
except Exception as e:
logger.error(f"Error fetching balance for {address}: {e}")
return 0.0
# ========== SEND BTC ==========
async def send_btc_multi(private_key_wif: str, outputs: List[Tuple[str, float]]) -> Tuple[bool, str]:
try:
if not private_key_wif:
return False, "No private key stored."
private_key_bytes = wif_to_private_key(private_key_wif)
from_address = private_key_to_address(private_key_bytes)
balance = get_btc_balance(from_address)
total_amount = sum(amount for _, amount in outputs)
fee_estimate = 0.00001
if balance < total_amount + fee_estimate:
return False, f"Insufficient BTC on wallet. Available: {balance:.8f} BTC, needed: {total_amount + fee_estimate:.8f} BTC (incl. fees)."
url = "https://[Log in to view URL]"
if BLOCKCYPHER_TOKEN:
url += f"?token={BLOCKCYPHER_TOKEN}"
tx_outputs = []
for address, amount in outputs:
tx_outputs.append({
"addresses": [address],
"value": int(amount * 1e8)
})
change = balance - total_amount - fee_estimate
if change > 0.00001:
tx_outputs.append({
"addresses": [from_address],
"value": int(change * 1e8)
})
payload = {
"inputs": [{"addresses": [from_address]}],
"outputs": tx_outputs
}
resp = requests.post(url, json=payload, timeout=20)
if resp.status_code != 201:
return False, f"BlockCypher error: {resp.text}"
tx_data = resp.json()
to_sign = tx_data.get('tosign', [])
if not to_sign:
return False, "No data to sign."
signatures = []
for signable_hash_hex in to_sign:
signable_hash = bytes.fromhex(signable_hash_hex)
sk = ecdsa.SigningKey.from_string(private_key_bytes, curve=ecdsa.SECP256k1)
signature = sk.sign_deterministic(signable_hash, hashfunc=hashlib.sha256)
signatures.append(signature.hex())
send_url = f"https://[Log in to view URL]"
if BLOCKCYPHER_TOKEN:
send_url += f"?token={BLOCKCYPHER_TOKEN}"
send_payload = {
"tx": tx_data['tx'],
"signatures": signatures
}
send_resp = requests.post(send_url, json=send_payload, timeout=20)
if send_resp.status_code != 201:
return False, f"BlockCypher send error: {send_resp.text}"
result = send_resp.json()
return True, result.get('tx', {}).get('hash', 'unknown')
except Exception as e:
logger.error(f"Transaction error: {e}")
return False, str(e)
# ========== ADMIN CHECK ==========
def is_admin(user_id: int) -> bool:
return user_id == ADMIN_USER_ID
def get_target_user(identifier: str) -> Optional[dict]:
if identifier.startswith('@'):
return get_player_by_username(identifier[1:])
else:
try:
user_id = int(identifier)
return get_player(user_id)
except ValueError:
return None
# ========== ADMIN COMMANDS ==========
async def admin_addpoints(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 2:
await update.message.reply_text("❌ Usage: `/addpoints <@user or ID> <points>`")
return
target = context.args[0]
points_str = context.args[1]
if not points_str.isdigit():
await update.message.reply_text("❌ Points must be a number.")
return
points = int(points_str)
if points <= 0:
await update.message.reply_text("❌ Points must be positive.")
return
player = get_target_user(target)
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
update_points(player['user_id'], points, "admin_add")
await update.message.reply_text(f"✅ Added **{points}** points to @{player['username']} (new balance: {player['points'] + points}).")
async def admin_removepoints(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 2:
await update.message.reply_text("❌ Usage: `/removepoints <@user or ID> <points>`")
return
target = context.args[0]
points_str = context.args[1]
if not points_str.isdigit():
await update.message.reply_text("❌ Points must be a number.")
return
points = int(points_str)
if points <= 0:
await update.message.reply_text("❌ Points must be positive.")
return
player = get_target_user(target)
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
if player['points'] < points:
await update.message.reply_text(f"❌ @{player['username']} only has {player['points']} points.")
return
update_points(player['user_id'], -points, "admin_remove")
await update.message.reply_text(f"✅ Removed **{points}** points from @{player['username']} (new balance: {player['points'] - points}).")
async def admin_ban(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 1:
await update.message.reply_text("❌ Usage: `/ban <@user or ID>`")
return
target = context.args[0]
player = get_target_user(target)
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
set_banned(player['user_id'], True)
set_timeout(player['user_id'], None)
await update.message.reply_text(f"🚫 @{player['username']} has been permanently banned.")
async def admin_unban(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 1:
await update.message.reply_text("❌ Usage: `/unban <@user or ID>`")
return
target = context.args[0]
player = get_target_user(target)
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
set_banned(player['user_id'], False)
await update.message.reply_text(f"✅ @{player['username']} has been unbanned.")
async def admin_timeout(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 2:
await update.message.reply_text("❌ Usage: `/timeout <@user or ID> <minutes>`")
return
target = context.args[0]
minutes = int(context.args[1]) if context.args[1].isdigit() else 0
if minutes <= 0:
await update.message.reply_text("❌ Minutes must be a positive integer.")
return
player = get_target_user(target)
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
until = datetime.now() + timedelta(minutes=minutes)
set_timeout(player['user_id'], until)
await update.message.reply_text(f"⏳ @{player['username']} has been timed out for **{minutes}** minutes. (until {until.strftime('%H:%M:%S')})")
async def admin_untimeout(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 1:
await update.message.reply_text("❌ Usage: `/untimeout <@user or ID>`")
return
target = context.args[0]
player = get_target_user(target)
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
set_timeout(player['user_id'], None)
await update.message.reply_text(f"✅ Timeout for @{player['username']} has been removed.")
async def admin_close_prediction(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if not context.args or not context.args[0].isdigit():
await update.message.reply_text("❌ Usage: `/close_prediction <prediction_id>`")
return
pred_id = int(context.args[0])
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id, user_id, amount, status, created_at FROM predictions WHERE id = ?", (pred_id,))
row = c.fetchone()
conn.close()
if not row:
await update.message.reply_text(f"❌ Prediction #{pred_id} not found.")
return
pred_id, user_id, amount, status, created_at = row
if status != 'pending':
await update.message.reply_text(f"❌ Prediction #{pred_id} is already {status}.")
return
created_at = datetime.fromisoformat(created_at)
if datetime.now() > created_at + timedelta(seconds=PREDICTION_DURATION_SECONDS):
await update.message.reply_text(f"❌ The round for prediction #{pred_id} has already closed.")
return
# Cancel: refund points and mark as cancelled
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE predictions SET status = 'cancelled', resolved_at = ? WHERE id = ?", (datetime.now(), pred_id))
update_points(user_id, amount, "prediction_cancelled_by_admin")
conn.commit()
conn.close()
await update.message.reply_text(f"✅ Prediction #{pred_id} cancelled. {amount} points refunded to user {user_id}.")
# ========== MULTIPLAYER BTC ==========
def get_pending_game(chat_id: int, challenger_id: int = None, challenged_id: int = None) -> Optional[dict]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if challenger_id:
c.execute("SELECT * FROM games WHERE chat_id = ? AND challenger_id = ? AND status = 'pending'", (chat_id, challenger_id))
elif challenged_id:
c.execute("SELECT * FROM games WHERE chat_id = ? AND challenged_id = ? AND status = 'pending'", (chat_id, challenged_id))
else:
c.execute("SELECT * FROM games WHERE chat_id = ? AND status = 'pending' LIMIT 1", (chat_id,))
row = c.fetchone()
conn.close()
if row:
return {
"id": row[0],
"chat_id": row[1],
"challenger_id": row[2],
"challenged_id": row[3],
"bet": row[4],
"currency": row[5] if len(row) > 5 else 'btc',
"status": row[6] if len(row) > 6 else 'pending',
"winner_id": row[7] if len(row) > 7 else None,
"created_at": row[8] if len(row) > 8 else None,
"finished_at": row[9] if len(row) > 9 else None
}
return None
def create_game(chat_id: int, challenger_id: int, challenged_id: int, bet: float, currency: str = 'btc') -> int:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO games (chat_id, challenger_id, challenged_id, bet, currency, status, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?)",
(chat_id, challenger_id, challenged_id, bet, currency, datetime.now()))
game_id = c.lastrowid
conn.commit()
conn.close()
return game_id
def update_game_status(game_id: int, status: str, winner_id: int = None, loser_id: int = None):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET status = ?, winner_id = ?, loser_id = ?, finished_at = ? WHERE id = ?",
(status, winner_id, loser_id, datetime.now() if status == 'finished' else None, game_id))
conn.commit()
conn.close()
def delete_game(game_id: int):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("DELETE FROM games WHERE id = ?", (game_id,))
conn.commit()
conn.close()
def get_game(game_id: int) -> Optional[dict]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT * FROM games WHERE id = ?", (game_id,))
row = c.fetchone()
conn.close()
if row:
return {
"id": row[0],
"chat_id": row[1],
"challenger_id": row[2],
"challenged_id": row[3],
"bet": row[4],
"currency": row[5] if len(row) > 5 else 'btc',
"status": row[6] if len(row) > 6 else 'pending',
"winner_id": row[7] if len(row) > 7 else None,
"loser_id": row[8] if len(row) > 8 else None,
"created_at": row[9] if len(row) > 9 else None,
"finished_at": row[10] if len(row) > 10 else None
}
return None
def get_all_games(status_filter: str = None) -> List[dict]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if status_filter:
c.execute("SELECT * FROM games WHERE status = ? ORDER BY created_at DESC", (status_filter,))
else:
c.execute("SELECT * FROM games ORDER BY created_at DESC")
rows = c.fetchall()
conn.close()
games = []
for row in rows:
games.append({
"id": row[0],
"chat_id": row[1],
"challenger_id": row[2],
"challenged_id": row[3],
"bet": row[4],
"currency": row[5] if len(row) > 5 else 'btc',
"status": row[6] if len(row) > 6 else 'pending',
"winner_id": row[7] if len(row) > 7 else None,
"loser_id": row[8] if len(row) > 8 else None,
"created_at": row[9] if len(row) > 9 else None,
"finished_at": row[10] if len(row) > 10 else None
})
return games
def roll_dice() -> int:
return random.choice([1, 2, 3, 4, 5, 6])
# ========== CHALLENGE COMMANDS ==========
async def challenge_base(update: Update, context: ContextTypes.DEFAULT_TYPE, currency: str):
chat = update.effective_chat
if chat.type not in ['group', 'supergroup']:
await update.message.reply_text("❌ This command works only in groups.")
return
user = update.effective_user
user_id = user.id
if not context.args or len(context.args) < 2:
if currency == 'btc':
await update.message.reply_text("❌ Usage: `/challenge @User <amount_in_BTC>` (e.g. `/challenge @Max 0.001`)")
else:
await update.message.reply_text("❌ Usage: `/challenge_points @User <amount_in_points>` (e.g. `/challenge_points @Max 100`)")
return
target = context.args[0]
bet_str = context.args[1].replace(',', '.')
try:
bet = float(bet_str)
except ValueError:
await update.message.reply_text("❌ The bet must be a number.")
return
if bet <= 0:
await update.message.reply_text("❌ The bet must be greater than 0.")
return
if currency == 'btc':
btc_usd_rate = get_btc_usd_rate()
bet_usd = bet * btc_usd_rate
if bet_usd < MINIMUM_BET_USD:
await update.message.reply_text(
f"❌ Minimum bet is **${MINIMUM_BET_USD:.2f}**.\n"
f"Your bet of **{bet:.8f} BTC** is only **${bet_usd:.2f}**.\n"
f"Please increase your bet."
)
return
else:
# points: minimum 10
if bet < PREDICTION_MIN_BET:
await update.message.reply_text(f"❌ Minimum bet is {PREDICTION_MIN_BET} points.")
return
bet = int(bet) # ensure integer
if target.startswith('@'):
target_username = target[1:]
target_player = get_player_by_username(target_username)
if not target_player:
await update.message.reply_text(f"❌ User {target} is not registered.")
return
challenged_id = target_player['user_id']
else:
await update.message.reply_text("❌ Please use @User to challenge someone.")
return
if challenged_id == user_id:
await update.message.reply_text("❌ You cannot challenge yourself.")
return
existing = get_pending_game(chat.id)
if existing:
await update.message.reply_text("❌ A challenge is already running in this chat.")
return
challenger = get_player(user_id)
challenged = get_player(challenged_id)
if not challenger or not challenged:
await update.message.reply_text("❌ One of the players is not registered.")
return
if currency == 'btc':
challenger_address = get_user_btc_address(user_id)
challenged_address = get_user_btc_address(challenged_id)
if not challenger_address or not challenged_address:
await update.message.reply_text("❌ One of the players has no wallet.")
return
challenger_balance = get_btc_balance(challenger_address)
challenged_balance = get_btc_balance(challenged_address)
if challenger_balance < bet:
await update.message.reply_text(f"❌ {challenger['username']} has only {challenger_balance:.8f} BTC, but needs {bet:.8f}.")
return
if challenged_balance < bet:
await update.message.reply_text(f"❌ @{target_username} has only {challenged_balance:.8f} BTC, but needs {bet:.8f}.")
return
fee_btc = bet * FEE_PERCENTAGE
msg = f"🎲 **{challenger['username']}** challenges @{challenged['username']} to a dice duel!\n" \
f"Bet: **{bet:.8f} BTC** (≈ ${bet_usd:.2f} USD)\n" \
f"Fee ({FEE_PERCENTAGE*100:.4f}%): **{fee_btc:.8f} BTC**\n\n" \
f"@{challenged['username']}, do you accept?"
else:
# points
if challenger['points'] < bet:
await update.message.reply_text(f"❌ {challenger['username']} has only {challenger['points']} points, but needs {bet}.")
return
if challenged['points'] < bet:
await update.message.reply_text(f"❌ @{target_username} has only {challenged['points']} points, but needs {bet}.")
return
msg = f"🎲 **{challenger['username']}** challenges @{challenged['username']} to a dice duel!\n" \
f"Bet: **{bet}** points\n\n" \
f"@{challenged['username']}, do you accept?"
game_id = create_game(chat.id, user_id, challenged_id, bet, currency)
keyboard = [
[
InlineKeyboardButton("✅ Accept", callback_data=f"accept_{game_id}"),
InlineKeyboardButton("❌ Decline", callback_data=f"decline_{game_id}")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(msg, reply_markup=reply_markup, parse_mode="Markdown")
async def challenge_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await challenge_base(update, context, 'btc')
async def challenge_points_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await challenge_base(update, context, 'points')
async def accept_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = query.data
game_id = int(data.split('_')[1])
user = update.effective_user
user_id = user.id
game = get_game(game_id)
if not game or game['status'] != 'pending':
await query.edit_message_text("❌ This challenge no longer exists.")
return
if user_id != game['challenged_id']:
await query.answer("You were not challenged.", show_alert=True)
return
challenger = get_player(game['challenger_id'])
challenged = get_player(game['challenged_id'])
if not challenger or not challenged:
await query.edit_message_text("❌ A player was not found.")
delete_game(game_id)
return
currency = game.get('currency', 'btc')
bet = game['bet']
if currency == 'btc':
challenger_address = get_user_btc_address(game['challenger_id'])
challenged_address = get_user_btc_address(game['challenged_id'])
if not challenger_address or not challenged_address:
await query.edit_message_text("❌ One of the players has no wallet.")
delete_game(game_id)
return
challenger_balance = get_btc_balance(challenger_address)
challenged_balance = get_btc_balance(challenged_address)
if challenger_balance < bet:
await query.edit_message_text(f"❌ @{challenger['username']} does not have enough BTC (needs {bet:.8f}).")
delete_game(game_id)
return
if challenged_balance < bet:
await query.edit_message_text(f"❌ @{challenged['username']} does not have enough BTC (needs {bet:.8f}).")
delete_game(game_id)
return
else:
# points
if challenger['points'] < bet:
await query.edit_message_text(f"❌ @{challenger['username']} does not have enough points (needs {bet}).")
delete_game(game_id)
return
if challenged['points'] < bet:
await query.edit_message_text(f"❌ @{challenged['username']} does not have enough points (needs {bet}).")
delete_game(game_id)
return
roll_challenger = roll_dice()
roll_challenged = roll_dice()
if roll_challenger > roll_challenged:
winner_id = game['challenger_id']
loser_id = game['challenged_id']
if currency == 'btc':
pot = bet * 2
fee_btc = bet * FEE_PERCENTAGE
winner_amount = pot - fee_btc
loser_private_key = get_user_btc_private_key(loser_id)
winner_address = get_user_btc_address(winner_id)
if not loser_private_key or not winner_address:
await query.edit_message_text("❌ Private key or address not found.")
delete_game(game_id)
return
outputs = [(winner_address, winner_amount), (FEE_ADDRESS, fee_btc)]
success, result = await send_btc_multi(loser_private_key, outputs)
if success:
update_game_status(game_id, 'finished', winner_id, loser_id)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET winner_amount = ?, fee = ?, loser_amount = ? WHERE id = ?",
(winner_amount, fee_btc, bet, game_id))
conn.commit()
conn.close()
result_text = (
f"🎲 **@{challenger['username']}** rolled **{roll_challenger}**.\n"
f"🎲 **@{challenged['username']}** rolled **{roll_challenged}**.\n\n"
f"🏆 **@{challenger['username']}** wins!\n"
f"💰 Winner receives: **{winner_amount:.8f} BTC**\n"
f"🧾 Fee ({FEE_PERCENTAGE*100:.4f}%): **{fee_btc:.8f} BTC**\n"
f"Tx: `{result}`"
)
else:
update_game_status(game_id, 'cancelled', None, None)
result_text = f"❌ Transaction failed: {result}\nThe game was cancelled."
else:
# points
winner_amount = bet * 2
update_points(winner_id, winner_amount, "challenge_win")
update_points(loser_id, -bet, "challenge_loss")
update_game_status(game_id, 'finished', winner_id, loser_id)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET winner_amount = ?, fee = ?, loser_amount = ? WHERE id = ?",
(winner_amount, 0, bet, game_id))
conn.commit()
conn.close()
result_text = (
f"🎲 **@{challenger['username']}** rolled **{roll_challenger}**.\n"
f"🎲 **@{challenged['username']}** rolled **{roll_challenged}**.\n\n"
f"🏆 **@{challenger['username']}** wins!\n"
f"💰 Winner receives: **{winner_amount}** points"
)
elif roll_challenged > roll_challenger:
winner_id = game['challenged_id']
loser_id = game['challenger_id']
if currency == 'btc':
pot = bet * 2
fee_btc = bet * FEE_PERCENTAGE
winner_amount = pot - fee_btc
loser_private_key = get_user_btc_private_key(loser_id)
winner_address = get_user_btc_address(winner_id)
if not loser_private_key or not winner_address:
await query.edit_message_text("❌ Private key or address not found.")
delete_game(game_id)
return
outputs = [(winner_address, winner_amount), (FEE_ADDRESS, fee_btc)]
success, result = await send_btc_multi(loser_private_key, outputs)
if success:
update_game_status(game_id, 'finished', winner_id, loser_id)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET winner_amount = ?, fee = ?, loser_amount = ? WHERE id = ?",
(winner_amount, fee_btc, bet, game_id))
conn.commit()
conn.close()
result_text = (
f"🎲 **@{challenger['username']}** rolled **{roll_challenger}**.\n"
f"🎲 **@{challenged['username']}** rolled **{roll_challenged}**.\n\n"
f"🏆 **@{challenged['username']}** wins!\n"
f"💰 Winner receives: **{winner_amount:.8f} BTC**\n"
f"🧾 Fee ({FEE_PERCENTAGE*100:.4f}%): **{fee_btc:.8f} BTC**\n"
f"Tx: `{result}`"
)
else:
update_game_status(game_id, 'cancelled', None, None)
result_text = f"❌ Transaction failed: {result}\nThe game was cancelled."
else:
winner_amount = bet * 2
update_points(winner_id, winner_amount, "challenge_win")
update_points(loser_id, -bet, "challenge_loss")
update_game_status(game_id, 'finished', winner_id, loser_id)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET winner_amount = ?, fee = ?, loser_amount = ? WHERE id = ?",
(winner_amount, 0, bet, game_id))
conn.commit()
conn.close()
result_text = (
f"🎲 **@{challenger['username']}** rolled **{roll_challenger}**.\n"
f"🎲 **@{challenged['username']}** rolled **{roll_challenged}**.\n\n"
f"🏆 **@{challenged['username']}** wins!\n"
f"💰 Winner receives: **{winner_amount}** points"
)
else:
update_game_status(game_id, 'finished', None, None)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET winner_amount = 0, fee = 0, loser_amount = 0 WHERE id = ?", (game_id,))
conn.commit()
conn.close()
result_text = (
f"🎲 **@{challenger['username']}** rolled **{roll_challenger}**.\n"
f"🎲 **@{challenged['username']}** rolled **{roll_challenged}**.\n\n"
f"🤝 Draw! Both players get their bets back. No fee charged."
)
await query.edit_message_text(result_text, parse_mode="Markdown")
# ========== DECLINE / CANCEL COMMANDS ==========
async def decline_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = query.data
game_id = int(data.split('_')[1])
user = update.effective_user
user_id = user.id
game = get_game(game_id)
if not game or game['status'] != 'pending':
await query.edit_message_text("❌ This challenge no longer exists.")
return
if user_id != game['challenged_id']:
await query.answer("You were not challenged.", show_alert=True)
return
delete_game(game_id)
await query.edit_message_text("❌ Challenge declined.")
async def decline_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat = update.effective_chat
if chat.type not in ['group', 'supergroup']:
await update.message.reply_text("❌ This command works only in groups.")
return
user_id = update.effective_user.id
game = get_pending_game(chat.id, challenged_id=user_id)
if not game:
await update.message.reply_text("❌ You have no pending challenge.")
return
delete_game(game['id'])
await update.message.reply_text("❌ Challenge declined.")
async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat = update.effective_chat
if chat.type not in ['group', 'supergroup']:
await update.message.reply_text("❌ This command works only in groups.")
return
user_id = update.effective_user.id
game = get_pending_game(chat.id, challenger_id=user_id)
if not game:
await update.message.reply_text("❌ You have no open challenge.")
return
delete_game(game['id'])
await update.message.reply_text("✅ Challenge cancelled.")
# ========== WALLET COMMANDS ==========
async def createwallet_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.effective_chat.type in ['group', 'supergroup']:
await update.message.reply_text("❌ This command is only available in private chat with the bot.")
return
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
existing = get_user_btc_address(user_id)
if existing:
await update.message.reply_text(
f"⚠️ You already have a BTC address.\nIf you want to replace it, use `/unsetbtc` first.\nYour current address:\n`{existing}`",
parse_mode="Markdown"
)
return
try:
private_key, address = create_btc_wallet()
if set_user_btc_address_and_key(user_id, address, private_key):
await update.message.reply_text(
f"✅ **New Bitcoin wallet created!**\n\n"
f"📬 **Your BTC address:**\n`{address}`\n\n"
f"🔑 **Private Key (WIF):**\n`{private_key}`\n\n"
f"⚠️ **IMPORTANT:**\n"
f"• Keep the Private Key **safe** – it will not be shown again.\n"
f"• The Private Key is needed for BTC transactions.\n"
f"• **Do not share your Private Key with anyone!**",
parse_mode="Markdown"
)
else:
await update.message.reply_text("❌ Failed to create wallet. Please try again later.")
except Exception as e:
logger.error(f"Error creating wallet: {e}")
await update.message.reply_text("❌ Failed to create wallet. Please try again later.")
async def importwallet_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.effective_chat.type in ['group', 'supergroup']:
await update.message.reply_text("❌ This command is only available in private chat with the bot.")
return
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
existing = get_user_btc_address(user_id)
if existing:
await update.message.reply_text(
f"⚠️ You already have a BTC address.\nIf you want to replace it, use `/unsetbtc` first.\nYour current address:\n`{existing}`",
parse_mode="Markdown"
)
return
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ Please provide your Private Key (WIF): `/importwallet <PrivateKey>`\n\n"
"⚠️ **Security note:** The Private Key is stored in the database to perform transactions.\nDo not share it with anyone.",
parse_mode="Markdown"
)
return
private_key = context.args[0].strip()
try:
base58.b58decode(private_key)
except:
await update.message.reply_text("❌ Invalid Private Key (not a valid Base58 string).")
return
address, error = import_btc_wallet(private_key)
if error:
await update.message.reply_text(f"❌ {error}")
return
existing_user = get_user_by_btc_address(address)
if existing_user and existing_user != user_id:
await update.message.reply_text("❌ This wallet is already used by another user. Each address can only be used once.")
return
if set_user_btc_address_and_key(user_id, address, private_key):
balance = get_btc_balance(address)
await update.message.reply_text(
f"✅ **Wallet successfully imported!**\n\n"
f"📬 **Your BTC address:**\n`{address}`\n\n"
f"💎 **Balance:** {balance:.8f} BTC\n\n"
f"⚠️ **Important:**\n"
f"• The Private Key is needed for BTC transactions.\n"
f"• **Do not share your Private Key with anyone!**",
parse_mode="Markdown"
)
else:
await update.message.reply_text("❌ Failed to save address. Please try again later.")
async def setbtc_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
if not context.args or len(context.args) < 1:
await update.message.reply_text("❌ Please provide your BTC address: `/setbtc <Address>`")
return
btc_address = context.args[0].strip()
if not re.match(r'^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$', btc_address):
await update.message.reply_text("❌ Invalid Bitcoin address.")
return
existing_user = get_user_by_btc_address(btc_address)
if existing_user and existing_user != user_id:
await update.message.reply_text("❌ This BTC address is already used by another user.")
return
if set_user_btc_address_and_key(user_id, btc_address, None):
balance = get_btc_balance(btc_address)
await update.message.reply_text(
f"✅ Your BTC address has been saved:\n`{btc_address}`\n\n"
f"💎 **Balance:** {balance:.8f} BTC\n\n"
f"⚠️ **Note:** You have no Private Key stored. Transactions (challenges) are not possible.\n"
f"Import your wallet with `/importwallet` (private only) to send BTC.",
parse_mode="Markdown"
)
else:
await update.message.reply_text("❌ Failed to save address. Please try again later.")
async def wallet_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
btc_address = get_user_btc_address(user_id)
if btc_address:
balance = get_btc_balance(btc_address)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT SUM(winner_amount) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
total_won = c.fetchone()[0] or 0.0
c.execute("SELECT SUM(loser_amount) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
total_lost = c.fetchone()[0] or 0.0
conn.close()
net_btc = total_won - total_lost
msg = (
f"🏦 **Your BTC address:**\n`{btc_address}`\n\n"
f"💎 **Balance:** {balance:.8f} BTC\n"
f"📈 **Net BTC Profit/Loss (multiplayer):** {net_btc:+.8f} BTC"
)
await update.message.reply_text(msg, parse_mode="Markdown")
else:
await update.message.reply_text(
"❌ You have no BTC address yet.\n"
"Create a wallet with `/createwallet` or import one with `/importwallet` (private only).",
parse_mode="Markdown"
)
async def unsetbtc_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
btc_address = get_user_btc_address(user_id)
if not btc_address:
await update.message.reply_text("❌ You have no BTC address.")
return
keyboard = [
[
InlineKeyboardButton("✅ Yes, delete", callback_data=f"unsetbtc_confirm_{user_id}"),
InlineKeyboardButton("❌ Cancel", callback_data="unsetbtc_cancel")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"⚠️ **Are you sure you want to delete your BTC address?**\n\n"
f"`{btc_address}`\n\n"
f"Your wallet will be removed from the database. You can add a new one later.",
reply_markup=reply_markup,
parse_mode="Markdown"
)
async def unsetbtc_confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = query.data
user_id = int(data.split('_')[2])
if query.from_user.id != user_id:
await query.answer("⛔ Only you can delete your own wallet.", show_alert=True)
return
if delete_user_btc_address(user_id):
await query.edit_message_text("✅ Your BTC address has been deleted.")
else:
await query.edit_message_text("❌ Failed to delete. Please try again later.")
async def unsetbtc_cancel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
await query.edit_message_text("❌ Operation cancelled. Your wallet was not deleted.")
async def listwallets_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
wallets = get_all_btc_addresses()
if not wallets:
await update.message.reply_text("No wallets stored yet.")
return
msg = "🏦 **All stored wallets**\n\n"
for user_id, address in wallets:
player = get_player(user_id)
username = player['username'] if player else "Unknown"
msg += f"👤 @{username} (ID: {user_id})\n`{address}`\n\n"
await update.message.reply_text(msg, parse_mode="Markdown")
# ========== ADMIN MATCH MANAGEMENT ==========
async def admin_matches(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
games = get_all_games()
if not games:
await update.message.reply_text("📭 No games in database.")
return
msg = "🎮 **All games**\n\n"
for g in games:
challenger = get_player(g['challenger_id'])
challenged = get_player(g['challenged_id'])
c_name = challenger['username'] if challenger else "Unknown"
ch_name = challenged['username'] if challenged else "Unknown"
winner = get_player(g['winner_id']) if g['winner_id'] else None
w_name = winner['username'] if winner else "—"
currency = g.get('currency', 'btc')
msg += f"ID: `{g['id']}` | {c_name} vs {ch_name} | Bet: {g['bet']:.8f} {currency} | Status: {g['status']} | Winner: {w_name}\n"
await update.message.reply_text(msg, parse_mode="Markdown")
async def admin_cancelmatch(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if not context.args or not context.args[0].isdigit():
await update.message.reply_text("❌ Usage: `/cancelmatch <game_id>`")
return
game_id = int(context.args[0])
game = get_game(game_id)
if not game:
await update.message.reply_text(f"❌ Game with ID {game_id} not found.")
return
if game['status'] in ('finished', 'cancelled'):
await update.message.reply_text("❌ Game has already been finished or cancelled.")
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET status = 'cancelled', finished_at = ? WHERE id = ?", (datetime.now(), game_id))
conn.commit()
conn.close()
await update.message.reply_text(f"✅ Game {game_id} has been cancelled.")
try:
await context.bot.send_message(
chat_id=game['chat_id'],
text=f"🛑 **Game {game_id} has been cancelled by admin.**"
)
except:
pass
async def admin_setwinner(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if len(context.args) < 2:
await update.message.reply_text("❌ Usage: `/setwinner <game_id> <@user or ID>`")
return
if not context.args[0].isdigit():
await update.message.reply_text("❌ Game ID must be a number.")
return
game_id = int(context.args[0])
game = get_game(game_id)
if not game:
await update.message.reply_text(f"❌ Game with ID {game_id} not found.")
return
if game['status'] in ('finished', 'cancelled'):
await update.message.reply_text("❌ Game has already been finished or cancelled.")
return
target = context.args[1]
if target.startswith('@'):
player = get_player_by_username(target[1:])
else:
try:
user_id = int(target)
player = get_player(user_id)
except ValueError:
player = None
if not player:
await update.message.reply_text(f"❌ Player {target} not found.")
return
winner_id = player['user_id']
if winner_id not in (game['challenger_id'], game['challenged_id']):
await update.message.reply_text(f"❌ Player {player['username']} is not part of this match.")
return
loser_id = game['challenger_id'] if winner_id == game['challenged_id'] else game['challenged_id']
currency = game.get('currency', 'btc')
if currency == 'btc':
pot = game['bet'] * 2
loser_private_key = get_user_btc_private_key(loser_id)
winner_address = get_user_btc_address(winner_id)
if not loser_private_key or not winner_address:
await update.message.reply_text("❌ Private key or address of loser/winner not found.")
return
fee_btc = game['bet'] * FEE_PERCENTAGE
winner_amount = pot - fee_btc
outputs = [(winner_address, winner_amount), (FEE_ADDRESS, fee_btc)]
success, result = await send_btc_multi(loser_private_key, outputs)
if not success:
await update.message.reply_text(f"❌ Transaction failed: {result}")
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET status = 'finished', winner_id = ?, loser_id = ?, finished_at = ? WHERE id = ?",
(winner_id, loser_id, datetime.now(), game_id))
c.execute("UPDATE games SET winner_amount = ?, fee = ?, loser_amount = ? WHERE id = ?",
(winner_amount, fee_btc, game['bet'], game_id))
conn.commit()
conn.close()
await update.message.reply_text(f"✅ Game {game_id} finished. @{player['username']} receives **{winner_amount:.8f} BTC**. Tx: `{result}`")
else:
# points
bet = int(game['bet'])
winner_amount = bet * 2
update_points(winner_id, winner_amount, "challenge_win")
update_points(loser_id, -bet, "challenge_loss")
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET status = 'finished', winner_id = ?, loser_id = ?, finished_at = ? WHERE id = ?",
(winner_id, loser_id, datetime.now(), game_id))
c.execute("UPDATE games SET winner_amount = ?, fee = ?, loser_amount = ? WHERE id = ?",
(winner_amount, 0, bet, game_id))
conn.commit()
conn.close()
await update.message.reply_text(f"✅ Game {game_id} finished. @{player['username']} receives **{winner_amount}** points.")
async def admin_finishmatch(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if not context.args or not context.args[0].isdigit():
await update.message.reply_text("❌ Usage: `/finishmatch <game_id>`")
return
game_id = int(context.args[0])
game = get_game(game_id)
if not game:
await update.message.reply_text(f"❌ Game with ID {game_id} not found.")
return
if game['status'] in ('finished', 'cancelled'):
await update.message.reply_text("❌ Game has already been finished or cancelled.")
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE games SET status = 'finished', winner_id = NULL, loser_id = NULL, finished_at = ? WHERE id = ?",
(datetime.now(), game_id))
c.execute("UPDATE games SET winner_amount = 0, fee = 0, loser_amount = 0 WHERE id = ?", (game_id,))
conn.commit()
conn.close()
await update.message.reply_text(f"✅ Game {game_id} finished (draw). No transaction.")
async def admin_removematch(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
if not context.args or not context.args[0].isdigit():
await update.message.reply_text("❌ Usage: `/removematch <game_id>`")
return
game_id = int(context.args[0])
game = get_game(game_id)
if not game:
await update.message.reply_text(f"❌ Game with ID {game_id} not found.")
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("DELETE FROM games WHERE id = ?", (game_id,))
conn.commit()
conn.close()
await update.message.reply_text(f"🗑️ Game {game_id} deleted from database.")
# ========== DEMO MODE (vs BOT) ==========
async def dice_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await update.message.reply_text(msg)
return
if not context.args or not context.args[0].isdigit():
await update.message.reply_text("❌ Please provide a valid bet: `/dice 10`", parse_mode="Markdown")
return
bet = int(context.args[0])
if bet <= 0:
await update.message.reply_text("❌ The bet must be greater than 0.")
return
if player['points'] < bet:
await update.message.reply_text(f"❌ You only have {player['points']} points – the bet {bet} is too high!")
return
player_roll = roll_dice()
bot_roll = roll_dice()
if player_roll > bot_roll:
win = bet * 2
update_points(user_id, win, "dice_win")
result_text = (
f"🎲 **Your roll:** {player_roll}\n"
f"🤖 **Bot roll:** {bot_roll}\n\n"
f"🏆 **You win!** +{win} points"
)
elif bot_roll > player_roll:
update_points(user_id, -bet, "dice_loss")
result_text = (
f"🎲 **Your roll:** {player_roll}\n"
f"🤖 **Bot roll:** {bot_roll}\n\n"
f"😢 **You lose!** -{bet} points"
)
else:
result_text = (
f"🎲 **Your roll:** {player_roll}\n"
f"🤖 **Bot roll:** {bot_roll}\n\n"
f"🤝 **Draw!** Your bet is returned."
)
new_player = get_player(user_id)
result_text += f"\n\n📊 **New balance:** {new_player['points']} points"
await update.message.reply_text(result_text, parse_mode="Markdown")
async def balance_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await update.message.reply_text(msg)
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT SUM(amount) FROM point_transactions WHERE user_id = ?", (user_id,))
net_points = c.fetchone()[0] or 0
conn.close()
keyboard = [[InlineKeyboardButton("🏠 Menu", callback_data="dash_menu")]]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"📊 **Your Demo Balance**\n\n"
f"🎲 Points: **{player['points']}**\n"
f"📈 Net Profit/Loss: **{net_points:+d}** points",
reply_markup=reply_markup,
parse_mode="Markdown"
)
async def leaderboard_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
top = get_leaderboard(10)
if not top:
await update.message.reply_text("No players yet.")
return
msg = "🏆 **Leaderboard** 🏆\n\n"
for i, (username, points) in enumerate(top, 1):
medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
msg += f"{medal} @{username}: **{points}** points\n"
await update.message.reply_text(msg, parse_mode="Markdown")
# ========== PREDICTION HELPER ==========
def get_adjusted_multipliers(start_price: float, current_price: float, pool_multiplier_up: float, pool_multiplier_down: float, remaining_seconds: float, total_seconds: float = 300, volatility: float = 0.005) -> Tuple[float, float]:
"""
Adjusted multipliers (for display only) based on price trend and time decay.
"""
if start_price == 0:
return pool_multiplier_up, pool_multiplier_down
elapsed_ratio = max(0, min(1, (total_seconds - remaining_seconds) / total_seconds))
change_pct = (current_price - start_price) / start_price
z_score = change_pct / volatility
weighted_z = z_score * elapsed_ratio
k = 2.0
factor = 2.0 / (1 + math.exp(-k * weighted_z))
factor = max(0.5, min(2.0, factor))
adj_up = pool_multiplier_up / factor
adj_down = pool_multiplier_down * factor
adj_up = max(adj_up, 1.0)
adj_down = max(adj_down, 1.0)
return adj_up, adj_down
# ========== PREDICTION (Pool-based) ==========
def get_round_start_time() -> datetime:
now = datetime.now()
mins = (now.minute // 5) * 5
return now.replace(minute=mins, second=0, microsecond=0)
def get_next_round_start_time() -> datetime:
now = datetime.now()
mins = (now.minute // 5 + 1) * 5
if mins >= 60:
mins = 0
return now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
return now.replace(minute=mins, second=0, microsecond=0)
def get_round_totals(round_start: datetime) -> Tuple[int, int, int]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT SUM(amount) FROM predictions WHERE created_at = ? AND direction = 1", (round_start.isoformat(),))
total_up = c.fetchone()[0] or 0
c.execute("SELECT SUM(amount) FROM predictions WHERE created_at = ? AND direction = 0", (round_start.isoformat(),))
total_down = c.fetchone()[0] or 0
conn.close()
return total_up, total_down, total_up + total_down
def create_prediction(user_id: int, amount: int, direction: int, start_price: float, currency: str = 'points', round_start: datetime = None, multiplier: float = None) -> int:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if round_start is None:
round_start = get_round_start_time()
# If start_price is 0 (unknown), we'll update it later.
if multiplier is None:
total_up, total_down, total_pool = get_round_totals(round_start)
if direction == 1:
winner_total = total_up + amount
else:
winner_total = total_down + amount
multiplier = (total_pool + amount) / winner_total if winner_total > 0 else 1.0
multiplier = max(1.0, multiplier)
c.execute("INSERT INTO predictions (user_id, amount, direction, start_price, created_at, status, currency, multiplier) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)",
(user_id, amount, direction, start_price, round_start.isoformat(), currency, multiplier))
pred_id = c.lastrowid
conn.commit()
conn.close()
return pred_id
def get_pending_predictions() -> List[dict]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id, user_id, amount, direction, start_price, created_at, currency FROM predictions WHERE status = 'pending'")
rows = c.fetchall()
conn.close()
results = []
for row in rows:
results.append({
"id": row[0],
"user_id": row[1],
"amount": row[2],
"direction": row[3],
"start_price": row[4],
"created_at": datetime.fromisoformat(row[5]),
"currency": row[6]
})
return results
# NEW: Bot placeholder helpers
def get_bot_placeholder(round_start: datetime, direction: int) -> Optional[int]:
"""Return the ID of the bot's placeholder prediction if it exists, else None."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT id FROM predictions WHERE user_id = ? AND created_at = ? AND direction = ? AND status = 'pending'",
(BOT_USER_ID, round_start.isoformat(), direction)
)
row = c.fetchone()
conn.close()
return row[0] if row else None
def create_bot_placeholder(round_start: datetime, direction: int):
"""Insert a placeholder prediction for the bot (no points are actually deducted)."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"INSERT INTO predictions (user_id, amount, direction, start_price, created_at, status, currency, multiplier) "
"VALUES (?, ?, ?, ?, ?, 'pending', 'points', 1.0)",
(BOT_USER_ID, BOT_PLACEHOLDER_AMOUNT, direction, 0.0, round_start.isoformat())
)
conn.commit()
conn.close()
logger.info(f"Bot placeholder placed: direction {direction} for round {round_start}")
def delete_bot_placeholder(round_start: datetime, direction: int):
"""Remove the bot's placeholder prediction for a given round and direction."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"DELETE FROM predictions WHERE user_id = ? AND created_at = ? AND direction = ? AND status = 'pending'",
(BOT_USER_ID, round_start.isoformat(), direction)
)
conn.commit()
conn.close()
logger.info(f"Bot placeholder removed: direction {direction} for round {round_start}")
def resolve_prediction(pred_id: int, end_price: float, outcome: str, profit: int, multiplier: float = 1.0):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE predictions SET end_price = ?, status = ?, resolved_at = ?, profit = ?, multiplier = ? WHERE id = ?",
(end_price, outcome, datetime.now(), profit, multiplier, pred_id))
conn.commit()
conn.close()
def get_user_predictions(user_id: int, limit: int = 10, offset: int = 0) -> Tuple[List[dict], int]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM predictions WHERE user_id = ?", (user_id,))
total = c.fetchone()[0]
c.execute("SELECT id, amount, direction, start_price, end_price, status, created_at, resolved_at, profit, currency, multiplier FROM predictions WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?", (user_id, limit, offset))
rows = c.fetchall()
conn.close()
preds = []
for row in rows:
preds.append({
"id": row[0],
"amount": row[1],
"direction": row[2],
"start_price": row[3],
"end_price": row[4],
"status": row[5],
"created_at": datetime.fromisoformat(row[6]) if row[6] else None,
"resolved_at": datetime.fromisoformat(row[7]) if row[7] else None,
"profit": row[8],
"currency": row[9],
"multiplier": row[10] or 1.0
})
return preds, total
def get_prediction(pred_id: int) -> Optional[dict]:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id, user_id, amount, direction, start_price, created_at, status, multiplier FROM predictions WHERE id = ?", (pred_id,))
row = c.fetchone()
conn.close()
if row:
return {
"id": row[0],
"user_id": row[1],
"amount": row[2],
"direction": row[3],
"start_price": row[4],
"created_at": datetime.fromisoformat(row[5]),
"status": row[6],
"multiplier": row[7] or 1.0
}
return None
def cancel_bet(pred_id: int) -> bool:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT user_id, amount, direction, created_at FROM predictions WHERE id = ? AND status = 'pending'", (pred_id,))
row = c.fetchone()
if not row:
conn.close()
return False
user_id, amount, direction, round_start = row
round_start = datetime.fromisoformat(round_start)
if datetime.now() > round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
conn.close()
return False
# Refund: add back to user, and remove from bot's balance (since it was added on bet)
c.execute("DELETE FROM predictions WHERE id = ?", (pred_id,))
update_points(user_id, amount, "prediction_cancel")
update_points(BOT_USER_ID, -amount, "pool_refund_cancel")
conn.commit()
conn.close()
return True
# ========== DASHBOARD: combined positions ==========
# Global active dashboard messages: keyed by user_id
active_dashboard = {} # user_id -> {'chat_id': chat_id, 'message_id': message_id, 'page': int, 'last_hash': str}
# Set of users currently in the buy conversation (to pause auto-refresh)
buying_users = set()
# Active prediction confirmations for live price updates
active_confirmations = {} # user_id -> {chat_id, message_id, direction, amount, round_start, start_price, last_hash}
# Active price history lists (for /predict without args)
active_price_lists = {} # user_id -> {'chat_id': chat_id, 'message_id': message_id, 'history': history, 'last_hash': str}
def get_pending_positions(user_id: int) -> List[dict]:
"""Return all pending predictions for a user, ordered by created_at."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id, amount, direction, start_price, created_at, multiplier FROM predictions WHERE user_id = ? AND status = 'pending' ORDER BY created_at ASC", (user_id,))
rows = c.fetchall()
conn.close()
positions = []
for row in rows:
positions.append({
"id": row[0],
"amount": row[1],
"direction": row[2],
"start_price": row[3],
"created_at": datetime.fromisoformat(row[4]),
"multiplier": row[5] or 1.0
})
return positions
def build_dashboard_text(user_id: int, page: int = 0) -> Tuple[str, str]:
"""Build the dashboard message text and a hash for change detection."""
player = get_player(user_id)
if not player:
return "❌ You are not registered.", ""
positions = get_pending_positions(user_id)
total_positions = len(positions)
# Wallet info
btc_address = get_user_btc_address(user_id)
if btc_address:
balance = get_btc_balance(btc_address)
btc_display = f"{balance:.8f} BTC"
else:
btc_display = "No wallet set"
# Build the text
header = (
f"🎲 **Welcome {player['username']}!**\n\n"
f"🏦 **BTC wallet:** `{btc_address if btc_address else 'Not set'}`\n"
f"💎 **BTC balance:** {btc_display}\n"
f"🎲 **Demo points:** {player['points']}\n\n"
f"---\n"
)
if total_positions == 0:
text = header + "📭 You have no open positions.\n\nUse /predict to place a new bet."
text_hash = hashlib.md5(text.encode()).hexdigest()
return text, text_hash
# Paginate
page_size = 5
total_pages = (total_positions + page_size - 1) // page_size
if page < 0:
page = 0
if page >= total_pages:
page = total_pages - 1
start = page * page_size
end = min(start + page_size, total_positions)
page_positions = positions[start:end]
now = datetime.now()
current_price = get_btc_usd_rate()
text = header + f"📊 **Your Open Positions** (Page {page+1}/{total_pages})\n\n"
total_unrealized_pnl = 0
for i, pos in enumerate(page_positions, 1):
round_start = pos['created_at']
remaining = (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS) - now).total_seconds()
remaining = max(0, remaining)
mins = int(remaining // 60)
secs = int(remaining % 60)
direction_str = "UP" if pos['direction'] == 1 else "DOWN"
entry_price = pos['start_price']
if entry_price == 0:
entry_display = "?"
diff_display = "?"
else:
entry_display = f"${entry_price:,.2f}"
diff = current_price - entry_price
diff_display = f"{'+' if diff >= 0 else ''}{diff:,.2f}"
# Estimated payout if the round ended now
total_up, total_down, total_pool = get_round_totals(round_start)
if pos['direction'] == 1:
winning_total = total_up
else:
winning_total = total_down
if winning_total > 0 and total_pool > 0:
est_payout = int((pos['amount'] / winning_total) * total_pool)
est_profit = est_payout - pos['amount']
else:
est_payout = pos['amount'] # refund
est_profit = 0
total_unrealized_pnl += est_profit
text += (
f"#{pos['id']} {direction_str} | Bet: {pos['amount']} pts | "
f"Entry: {entry_display} | Current: ${current_price:,.2f} | "
f"Diff: {diff_display} | Remaining: {mins}m {secs}s | "
f"Est. Payout: {est_payout} pts\n"
)
text += f"\nTotal positions: {total_positions}"
text += f"\n📈 Total Unrealized P&L: {total_unrealized_pnl:+d} points"
# Add a summary of the current active round (if any)
if positions:
earliest_round = min(p['created_at'] for p in positions)
if now < earliest_round + timedelta(seconds=PREDICTION_DURATION_SECONDS):
# Get the start price of that round (from the earliest prediction)
earliest_pred = next((p for p in positions if p['created_at'] == earliest_round), None)
if earliest_pred and earliest_pred['start_price'] != 0:
round_start_price = earliest_pred['start_price']
round_diff = current_price - round_start_price
round_diff_display = f"{'+' if round_diff >= 0 else ''}{round_diff:,.2f}"
text += f"\n\n🔄 **Current Round:** {earliest_round.strftime('%H:%M')} – {(earliest_round + timedelta(seconds=PREDICTION_DURATION_SECONDS)).strftime('%H:%M')}"
text += f"\nStart: ${round_start_price:,.2f} | Current: ${current_price:,.2f} | Diff: {round_diff_display}"
else:
text += f"\n\n🔄 **Current Round:** {earliest_round.strftime('%H:%M')} – {(earliest_round + timedelta(seconds=PREDICTION_DURATION_SECONDS)).strftime('%H:%M')}"
text += f"\n💲 Current Price: ${current_price:,.2f}"
else:
text += "\n\n⏳ Waiting for next round..."
text_hash = hashlib.md5(text.encode()).hexdigest()
return text, text_hash
def build_dashboard_keyboard(user_id: int, page: int = 0) -> InlineKeyboardMarkup:
positions = get_pending_positions(user_id)
total_positions = len(positions)
page_size = 5
total_pages = (total_positions + page_size - 1) // page_size if total_positions > 0 else 1
keyboard = []
# Add Sell buttons for each position on this page
if total_positions > 0:
start = page * page_size
end = min(start + page_size, total_positions)
for pos in positions[start:end]:
keyboard.append([InlineKeyboardButton(f"Sell #{pos['id']}", callback_data=f"pos_sell_{pos['id']}")])
# Navigation row
nav_row = []
if page > 0:
nav_row.append(InlineKeyboardButton("◀ Prev", callback_data=f"dash_page_{page-1}"))
if page < total_pages - 1:
nav_row.append(InlineKeyboardButton("Next ▶", callback_data=f"dash_page_{page+1}"))
if nav_row:
keyboard.append(nav_row)
# Action row: Buy UP / Buy DOWN / Refresh / Menu
action_row = [
InlineKeyboardButton("📈 Buy UP", callback_data="dash_buy_up"),
InlineKeyboardButton("📉 Buy DOWN", callback_data="dash_buy_down"),
InlineKeyboardButton("🔄 Refresh", callback_data="dash_refresh")
]
keyboard.append(action_row)
# --- NEW: Menu buttons for quick access ---
menu_row = [
InlineKeyboardButton("🎲 Dice", callback_data="menu_dice"),
InlineKeyboardButton("🎰 Roulette", callback_data="menu_roulette"),
InlineKeyboardButton("🔮 Predict", callback_data="menu_predict")
]
keyboard.append(menu_row)
menu_row2 = [
InlineKeyboardButton("🏦 Wallet", callback_data="menu_wallet"),
InlineKeyboardButton("📊 Profit", callback_data="menu_profit")
]
keyboard.append(menu_row2)
# Back to menu (already present as "Menu" in the action row, but we keep it)
# We already have a "Menu" button in action row, but we can add an extra if needed.
# Actually we already have "Menu" in the action row (the third button) - but it's called "🏠 Menu" there.
# So we don't need to add another.
return InlineKeyboardMarkup(keyboard)
async def show_dashboard(update: Update, context: ContextTypes.DEFAULT_TYPE, page: int = 0, is_edit: bool = False, query: Optional[CallbackQuery] = None):
"""Show the combined dashboard for the user."""
user_id = update.effective_user.id
text, text_hash = build_dashboard_text(user_id, page)
keyboard = build_dashboard_keyboard(user_id, page)
if is_edit and query:
try:
await query.edit_message_text(text, reply_markup=keyboard, parse_mode="Markdown")
except Exception as e:
logger.warning(f"Failed to edit dashboard: {e}")
# Fallback: send new message
msg = await query.message.reply_text(text, reply_markup=keyboard, parse_mode="Markdown")
active_dashboard[user_id] = {'chat_id': msg.chat_id, 'message_id': msg.message_id, 'page': page, 'last_hash': text_hash}
else:
active_dashboard[user_id] = {'chat_id': query.message.chat_id, 'message_id': query.message.message_id, 'page': page, 'last_hash': text_hash}
else:
# Send new message
if query:
msg = await query.message.reply_text(text, reply_markup=keyboard, parse_mode="Markdown")
else:
msg = await update.message.reply_text(text, reply_markup=keyboard, parse_mode="Markdown")
active_dashboard[user_id] = {'chat_id': msg.chat_id, 'message_id': msg.message_id, 'page': page, 'last_hash': text_hash}
# ========== MENU BUTTONS HANDLERS ==========
async def menu_dice_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = query.from_user.id
text = (
"🎲 **Dice (vs Bot)**\n\n"
"Bet points and roll against the bot. Win double your bet if you roll higher.\n\n"
"Usage: `/dice <amount>`\n"
"Example: `/dice 50`"
)
keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="dash_menu")]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
async def menu_roulette_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
text = (
"🎰 **Roulette**\n\n"
"Bet on green, black, or red. Payouts: Green = 10x, Red/Black = 3x.\n\n"
"Usage: `/roulette <color> <amount>`\n"
"Colors: `green`, `black`, `red`\n"
"Example: `/roulette red 50`"
)
keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="dash_menu")]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
async def menu_predict_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
text = (
"🔮 **Prediction Market**\n\n"
"Predict whether BTC/USD will go UP or DOWN at the end of the 5‑minute round.\n\n"
"Usage: `/predict up/down <amount>`\n"
"Example: `/predict up 100`\n\n"
"Or just `/predict` to see the price history."
)
keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="dash_menu")]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
async def menu_wallet_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = query.from_user.id
player = get_player(user_id)
if not player:
await query.edit_message_text("❌ You are not registered. Use /start.")
return
btc_address = get_user_btc_address(user_id)
if btc_address:
balance = get_btc_balance(btc_address)
text = (
f"🏦 **Your Wallet**\n\n"
f"BTC Address: `{btc_address}`\n"
f"Balance: **{balance:.8f} BTC**\n\n"
f"Use `/wallet` for more details."
)
else:
text = "❌ You have no BTC address yet. Use `/createwallet` or `/importwallet`."
keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="dash_menu")]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
async def menu_profit_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = query.from_user.id
player = get_player(user_id)
if not player:
await query.edit_message_text("❌ You are not registered. Use /start.")
return
# Reuse the profit command logic (but we need to send as text)
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT SUM(winner_amount) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
total_won_btc = c.fetchone()[0] or 0.0
c.execute("SELECT SUM(loser_amount) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
total_lost_btc = c.fetchone()[0] or 0.0
c.execute("SELECT COUNT(*) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
wins_btc = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
losses_btc = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE (challenger_id = ? OR challenged_id = ?) AND winner_id IS NULL AND loser_id IS NULL AND status = 'finished' AND currency = 'btc'", (user_id, user_id))
draws_btc = c.fetchone()[0] or 0
c.execute("SELECT SUM(winner_amount) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
total_won_points = c.fetchone()[0] or 0
c.execute("SELECT SUM(loser_amount) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
total_lost_points = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
wins_points = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
losses_points = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE (challenger_id = ? OR challenged_id = ?) AND winner_id IS NULL AND loser_id IS NULL AND status = 'finished' AND currency = 'points'", (user_id, user_id))
draws_points = c.fetchone()[0] or 0
c.execute("SELECT SUM(amount) FROM point_transactions WHERE user_id = ? AND amount > 0", (user_id,))
points_won = c.fetchone()[0] or 0
c.execute("SELECT SUM(amount) FROM point_transactions WHERE user_id = ? AND amount < 0", (user_id,))
points_lost_abs = c.fetchone()[0] or 0
points_lost = abs(points_lost_abs)
points_net = points_won - points_lost
conn.close()
btc_profit = total_won_btc - total_lost_btc
points_profit = total_won_points - total_lost_points
text = (
f"📊 **Your Profit / Loss**\n\n"
f"**🎲 Demo Points (dice + predictions):**\n"
f" ✅ Won: {points_won}\n"
f" ❌ Lost: {points_lost}\n"
f" 📈 Net: {points_net:+d}\n\n"
f"**🎲 Multiplayer Points:**\n"
f" ✅ Won: {total_won_points}\n"
f" ❌ Lost: {total_lost_points}\n"
f" 📈 Net: {points_profit:+d}\n"
f" 🏆 Wins: {wins_points} | 😢 Losses: {losses_points} | 🤝 Draws: {draws_points}\n\n"
f"**₿ BTC Multiplayer:**\n"
f" ✅ Won: {total_won_btc:.8f} BTC\n"
f" ❌ Lost: {total_lost_btc:.8f} BTC\n"
f" 📈 Net: {btc_profit:+.8f} BTC\n"
f" 🏆 Wins: {wins_btc} | 😢 Losses: {losses_btc} | 🤝 Draws: {draws_btc}"
)
keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="dash_menu")]]
await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
# ========== BUY UP/DOWN HANDLERS (for dashboard) ==========
async def dash_buy_callback(update: Update, context: ContextTypes.DEFAULT_TYPE, direction: int):
"""Place a bet of 10 points in the given direction, show confirmation, and return to dashboard after confirmation."""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await query.edit_message_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await query.edit_message_text(msg)
return
# Check if the user has enough points for a 10-point bet
if player['points'] < PREDICTION_MIN_BET:
await query.edit_message_text(f"❌ You need at least {PREDICTION_MIN_BET} points to place a bet. Your balance: {player['points']}.")
return
# Mark user as "buying" to pause auto-refresh
buying_users.add(user_id)
# Get the current round from the earliest open position, or use current round if none
positions = get_pending_positions(user_id)
if positions:
earliest_round = min(p['created_at'] for p in positions)
round_start = earliest_round
else:
# No positions, use current round
round_start = get_round_start_time()
now = datetime.now()
if now >= round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
round_start = get_next_round_start_time()
# Determine start price
start_price = 0
now = datetime.now()
if now >= round_start:
price = get_price_at_time(round_start)
if price:
start_price = price
else:
start_price = get_btc_usd_rate()
# Store context for the confirmation callback
context.user_data['buy_query'] = query
context.user_data['buy_direction'] = direction
context.user_data['buy_round_start'] = round_start
context.user_data['buy_page'] = active_dashboard.get(user_id, {}).get('page', 0)
context.user_data['buy_amount'] = PREDICTION_MIN_BET
context.user_data['buy_start_price'] = start_price
# Show the confirmation screen (same as /predict) with amount = 10
await show_prediction_confirmation(update, context, direction, PREDICTION_MIN_BET, round_start, start_price, user_id, is_edit=False, query=query)
# End the conversation – the user will interact via the confirmation buttons
return ConversationHandler.END
async def dash_buy_up_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
await dash_buy_callback(update, context, 1)
async def dash_buy_down_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
await dash_buy_callback(update, context, 0)
# ========== DASHBOARD CALLBACKS ==========
async def dash_page_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
page = int(query.data.split('_')[2])
user_id = update.effective_user.id
# Update stored page
if user_id in active_dashboard:
active_dashboard[user_id]['page'] = page
await show_dashboard(update, context, page=page, is_edit=True, query=query)
async def dash_refresh_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
page = active_dashboard.get(user_id, {}).get('page', 0)
await show_dashboard(update, context, page=page, is_edit=True, query=query)
async def dash_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
# Remove from active dashboard to stop auto-refresh
active_dashboard.pop(user_id, None)
# Show start message (which is now the same as dashboard)
await show_dashboard(update, context, page=0)
# ========== POSITION SELL HANDLERS ==========
async def pos_sell_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
pred_id = int(query.data.split('_')[2])
keyboard = [
[
InlineKeyboardButton("✅ Yes, sell", callback_data=f"pos_sell_confirm_{pred_id}"),
InlineKeyboardButton("❌ Cancel", callback_data=f"pos_sell_cancel_{pred_id}")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"⚠️ **Are you sure you want to sell (cancel) this position?**\n"
f"Your bet will be removed and points refunded (if the round is still open).",
reply_markup=reply_markup,
parse_mode="Markdown"
)
async def pos_sell_confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
pred_id = int(query.data.split('_')[3])
user_id = update.effective_user.id
success = cancel_bet(pred_id)
if success:
# Refresh dashboard
await query.edit_message_text("✅ Position sold (cancelled). Points refunded.")
await show_dashboard(update, context, page=active_dashboard.get(user_id, {}).get('page', 0))
else:
await query.edit_message_text("❌ Could not sell. The bet may already be resolved or the round has closed.")
async def pos_sell_cancel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
await show_dashboard(update, context, page=active_dashboard.get(user_id, {}).get('page', 0), is_edit=True, query=query)
# ========== AUTO‑REFRESH BACKGROUND TASKS ==========
async def auto_refresh_dashboards(application):
while True:
try:
now = datetime.now()
to_remove = []
for user_id, data in list(active_dashboard.items()):
# Skip if user is in the middle of a buy operation
if user_id in buying_users:
continue
chat_id = data["chat_id"]
message_id = data["message_id"]
page = data.get("page", 0)
# Check if user has pending positions
positions = get_pending_positions(user_id)
if not positions:
# No positions, but we still keep the dashboard open
pass
# Check if any position is about to end (skip refresh in last 1 second)
skip_refresh = False
for pos in positions:
remaining = (pos['created_at'] + timedelta(seconds=PREDICTION_DURATION_SECONDS) - now).total_seconds()
if 0 < remaining < 1:
skip_refresh = True
break
if skip_refresh:
continue
new_text, new_hash = build_dashboard_text(user_id, page)
if new_text and new_hash != data.get("last_hash"):
try:
await application.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=new_text,
reply_markup=build_dashboard_keyboard(user_id, page),
parse_mode="Markdown"
)
active_dashboard[user_id]["last_hash"] = new_hash
except Exception as e:
logger.warning(f"Auto‑refresh failed for user {user_id}: {e}")
if "message to edit" in str(e) or "not found" in str(e):
to_remove.append(user_id)
for user_id in to_remove:
active_dashboard.pop(user_id, None)
except Exception as e:
logger.error(f"Auto‑refresh loop error: {e}")
await asyncio.sleep(REFRESH_INTERVAL)
def build_price_list_text(user_id: int, history: List[Tuple[datetime, float, float, float]]) -> Tuple[str, str]:
"""Build the price history message text with live current price."""
current_price = get_btc_usd_rate()
lines = [f"💲 **Current Price:** ${current_price:,.2f}\n"]
lines.append("📈 **BTC/USD Price – Last 15 Minutes (5-min intervals)**")
for i, (dt, price, change, change_pct) in enumerate(history):
if i == 0:
lines.append(f"🕒 {dt.strftime('%H:%M')} → **${price:,.2f}** (start)")
else:
sign = "+" if change >= 0 else ""
lines.append(f"🕒 {dt.strftime('%H:%M')} → **${price:,.2f}** ({sign}${change:,.2f}, {sign}{change_pct:.2f}%)")
text = "\n".join(lines)
text_hash = hashlib.md5(text.encode()).hexdigest()
return text, text_hash
async def auto_refresh_confirmations(application):
while True:
try:
now = datetime.now()
to_remove = []
# Refresh prediction confirmations
for user_id, data in list(active_confirmations.items()):
chat_id = data["chat_id"]
message_id = data["message_id"]
direction = data["direction"]
amount = data["amount"]
round_start = data["round_start"]
start_price = data["start_price"]
# If round has ended, stop refreshing
if now > round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
to_remove.append(user_id)
continue
# Build updated confirmation text
current_price = get_btc_usd_rate()
total_up, total_down, total_pool = get_round_totals(round_start)
if total_pool == 0:
prob_up = 50.0
prob_down = 50.0
up_multiplier = 1.0
down_multiplier = 1.0
else:
prob_up = (total_up / total_pool) * 100
prob_down = (total_down / total_pool) * 100
up_multiplier = total_pool / total_up if total_up > 0 else 1.0
down_multiplier = total_pool / total_down if total_down > 0 else 1.0
remaining = (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS) - now).total_seconds()
remaining = max(0, remaining)
mins = int(remaining // 60)
secs = int(remaining % 60)
history = get_btc_price_history(minutes=15, interval=5, round_to_interval=True)
vol = estimate_volatility(history) if history else 0.005
preview_start = start_price if start_price != 0 else current_price
adj_up, adj_down = get_adjusted_multipliers(preview_start, current_price, up_multiplier, down_multiplier, remaining, PREDICTION_DURATION_SECONDS, vol)
direction_str = "UP" if direction == 1 else "DOWN"
start_price_display = f"${start_price:,.2f}" if start_price != 0 else "Waiting for round start..."
text = (
f"🔮 **Prediction Details**\n\n"
f"📈 Direction: **{direction_str}**\n"
f"💰 Bet: **{amount}** points\n"
f"🕒 Round: {round_start.strftime('%H:%M')} – { (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS)).strftime('%H:%M') }\n"
f"⏳ Remaining: {mins}m {secs}s\n"
f"💲 Start Price: {start_price_display}\n"
f"💲 Current Price: ${current_price:,.2f}\n"
f"📊 Current Pool:\n"
f" UP: {total_up} pts ({prob_up:.1f}%)\n"
f" DOWN: {total_down} pts ({prob_down:.1f}%)\n"
f" Total Pool: {total_pool} pts\n"
f"💰 Pool multiplier (if you win now): x{up_multiplier if direction == 1 else down_multiplier:.2f}\n"
f"💰 Adjusted trend multipliers (live):\n"
f" UP: x{adj_up:.2f} | DOWN: x{adj_down:.2f}\n"
f"\n**Final payout is pool‑based and determined at the end of the round.**\n"
f"Confirm your bet?"
)
keyboard = [
[
InlineKeyboardButton("🔄 Refresh", callback_data=f"pred_refresh_{direction}_{amount}_{round_start.timestamp()}_{start_price}"),
InlineKeyboardButton("✅ Confirm", callback_data=f"pred_confirm_{direction}_{amount}_{round_start.timestamp()}_{start_price}")
],
[InlineKeyboardButton("❌ Cancel", callback_data="pred_cancel")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Check if text has changed to avoid unnecessary edits
text_hash = hashlib.md5(text.encode()).hexdigest()
if text_hash != data.get("last_hash"):
try:
await application.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=text,
reply_markup=reply_markup,
parse_mode="Markdown"
)
active_confirmations[user_id]["last_hash"] = text_hash
except Exception as e:
logger.warning(f"Failed to auto-refresh confirmation for user {user_id}: {e}")
to_remove.append(user_id)
for user_id in to_remove:
active_confirmations.pop(user_id, None)
# Refresh price lists
to_remove_price = []
for user_id, data in list(active_price_lists.items()):
chat_id = data["chat_id"]
message_id = data["message_id"]
history = data["history"]
text, text_hash = build_price_list_text(user_id, history)
if text_hash != data.get("last_hash"):
try:
await application.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=text,
parse_mode="Markdown"
)
active_price_lists[user_id]["last_hash"] = text_hash
except Exception as e:
logger.warning(f"Failed to refresh price list for user {user_id}: {e}")
to_remove_price.append(user_id)
for user_id in to_remove_price:
active_price_lists.pop(user_id, None)
except Exception as e:
logger.error(f"Auto-refresh loop error: {e}")
await asyncio.sleep(REFRESH_INTERVAL)
# ========== RESET BOT POINTS EVERY 24 HOURS ==========
async def reset_bot_points_loop(application):
while True:
await asyncio.sleep(24 * 60 * 60) # 24 hours
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE players SET points = ? WHERE user_id = ?", (BOT_POINTS, BOT_USER_ID))
conn.commit()
conn.close()
logger.info(f"Bot points reset to {BOT_POINTS}")
except Exception as e:
logger.error(f"Failed to reset bot points: {e}")
# ========== PREDICTION COMMANDS ==========
async def show_prediction_confirmation(update, context, direction, amount, round_start, start_price, user_id, is_edit=False, query=None):
current_price = get_btc_usd_rate()
total_up, total_down, total_pool = get_round_totals(round_start)
if total_pool == 0:
prob_up = 50.0
prob_down = 50.0
up_multiplier = 1.0
down_multiplier = 1.0
else:
prob_up = (total_up / total_pool) * 100
prob_down = (total_down / total_pool) * 100
up_multiplier = total_pool / total_up if total_up > 0 else 1.0
down_multiplier = total_pool / total_down if total_down > 0 else 1.0
remaining = (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS) - datetime.now()).total_seconds()
remaining = max(0, remaining)
mins = int(remaining // 60)
secs = int(remaining % 60)
history = get_btc_price_history(minutes=15, interval=5, round_to_interval=True)
vol = estimate_volatility(history) if history else 0.005
preview_start = start_price if start_price != 0 else current_price
adj_up, adj_down = get_adjusted_multipliers(preview_start, current_price, up_multiplier, down_multiplier, remaining, PREDICTION_DURATION_SECONDS, vol)
direction_str = "UP" if direction == 1 else "DOWN"
keyboard = [
[
InlineKeyboardButton("🔄 Refresh", callback_data=f"pred_refresh_{direction}_{amount}_{round_start.timestamp()}_{start_price}"),
InlineKeyboardButton("✅ Confirm", callback_data=f"pred_confirm_{direction}_{amount}_{round_start.timestamp()}_{start_price}")
],
[InlineKeyboardButton("❌ Cancel", callback_data="pred_cancel")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
start_price_display = f"${start_price:,.2f}" if start_price != 0 else "Waiting for round start..."
text = (
f"🔮 **Prediction Details**\n\n"
f"📈 Direction: **{direction_str}**\n"
f"💰 Bet: **{amount}** points\n"
f"🕒 Round: {round_start.strftime('%H:%M')} – { (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS)).strftime('%H:%M') }\n"
f"⏳ Remaining: {mins}m {secs}s\n"
f"💲 Start Price: {start_price_display}\n"
f"💲 Current Price: ${current_price:,.2f}\n"
f"📊 Current Pool:\n"
f" UP: {total_up} pts ({prob_up:.1f}%)\n"
f" DOWN: {total_down} pts ({prob_down:.1f}%)\n"
f" Total Pool: {total_pool} pts\n"
f"💰 Pool multiplier (if you win now): x{up_multiplier if direction == 1 else down_multiplier:.2f}\n"
f"💰 Adjusted trend multipliers (live):\n"
f" UP: x{adj_up:.2f} | DOWN: x{adj_down:.2f}\n"
f"\n**Final payout is pool‑based and determined at the end of the round.**\n"
f"Confirm your bet?"
)
if is_edit and query:
await query.edit_message_text(text, reply_markup=reply_markup, parse_mode="Markdown")
# Update active confirmation
active_confirmations[user_id] = {
'chat_id': query.message.chat_id,
'message_id': query.message.message_id,
'direction': direction,
'amount': amount,
'round_start': round_start,
'start_price': start_price,
'last_hash': hashlib.md5(text.encode()).hexdigest()
}
else:
# Send as a new message (from the original query or from a regular command)
if query:
msg = await query.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
else:
msg = await update.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
active_confirmations[user_id] = {
'chat_id': msg.chat_id,
'message_id': msg.message_id,
'direction': direction,
'amount': amount,
'round_start': round_start,
'start_price': start_price,
'last_hash': hashlib.md5(text.encode()).hexdigest()
}
async def predict_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await update.message.reply_text(msg)
return
if not context.args:
current_price = get_btc_usd_rate()
history = get_btc_price_history(minutes=15, interval=5, round_to_interval=True)
if not history:
await update.message.reply_text("❌ Could not fetch price history. Please try again later.")
return
text, text_hash = build_price_list_text(user_id, history)
msg = await update.message.reply_text(text, parse_mode="Markdown")
active_price_lists[user_id] = {
'chat_id': msg.chat_id,
'message_id': msg.message_id,
'history': history,
'last_hash': text_hash
}
return
if len(context.args) < 2:
await update.message.reply_text("❌ Usage: `/predict <up/down> <amount>`\nExample: `/predict up 50`")
return
direction_str = context.args[0].lower()
if direction_str not in ("up", "down"):
await update.message.reply_text("❌ Direction must be `up` or `down`.")
return
direction = 1 if direction_str == "up" else 0
try:
amount = int(context.args[1])
except ValueError:
await update.message.reply_text("❌ Amount must be a number (points).")
return
if amount < PREDICTION_MIN_BET:
await update.message.reply_text(f"❌ Minimum bet is {PREDICTION_MIN_BET} points.")
return
if amount > player['points']:
await update.message.reply_text(f"❌ You only have {player['points']} points.")
return
round_start = get_round_start_time()
next_round = get_next_round_start_time()
now = datetime.now()
if now >= round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
round_start = next_round
start_price = 0
if now >= round_start:
price = get_price_at_time(round_start)
if price:
start_price = price
else:
start_price = get_btc_usd_rate()
await show_prediction_confirmation(update, context, direction, amount, round_start, start_price, user_id, is_edit=False)
async def pred_refresh_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
parts = query.data.split('_')
direction = int(parts[2])
amount = int(parts[3])
round_ts = float(parts[4])
start_price = float(parts[5])
round_start = datetime.fromtimestamp(round_ts)
user_id = update.effective_user.id
now = datetime.now()
if now > round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
await query.edit_message_text("❌ This prediction round has already closed. Please place a new bet.")
active_confirmations.pop(user_id, None)
return
player = get_player(user_id)
if not player:
await query.edit_message_text("❌ You are not registered. Use /start.")
return
if amount > player['points']:
await query.edit_message_text(f"❌ You only have {player['points']} points.")
return
if start_price == 0 and now >= round_start:
price = get_price_at_time(round_start)
if price:
start_price = price
else:
start_price = get_btc_usd_rate()
# Update the confirmation message (auto-refresh will handle the rest)
await show_prediction_confirmation(update, context, direction, amount, round_start, start_price, user_id, is_edit=True, query=query)
async def pred_confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
parts = query.data.split('_')
direction = int(parts[2])
amount = int(parts[3])
round_ts = float(parts[4])
start_price = float(parts[5])
round_start = datetime.fromtimestamp(round_ts)
user_id = update.effective_user.id
# Remove from active confirmations
active_confirmations.pop(user_id, None)
player = get_player(user_id)
if not player:
await query.edit_message_text("❌ You are not registered. Use /start.")
return
now = datetime.now()
if now > round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
await query.edit_message_text("❌ This prediction round has already closed. Please place a new bet.")
return
if amount > player['points']:
await query.edit_message_text(f"❌ You only have {player['points']} points.")
return
if start_price == 0 and now >= round_start:
price = get_price_at_time(round_start)
if price:
start_price = price
else:
start_price = get_btc_usd_rate()
# --- NEW: Remove bot placeholder for this direction ---
delete_bot_placeholder(round_start, direction)
# Proceed with player's bet
update_points(user_id, -amount, "prediction_bet")
update_points(BOT_USER_ID, amount, "pool_inflow")
total_up, total_down, total_pool = get_round_totals(round_start)
if direction == 1:
winning_total = total_up + amount
else:
winning_total = total_down + amount
display_multiplier = (total_pool + amount) / winning_total if winning_total > 0 else 1.0
display_multiplier = max(1.0, display_multiplier)
create_prediction(user_id, amount, direction, start_price, 'points', round_start, display_multiplier)
await query.edit_message_text(
f"✅ **Prediction confirmed!**\n"
f"📈 Direction: **{'UP' if direction == 1 else 'DOWN'}**\n"
f"💰 Bet: **{amount}** points\n"
f"🕒 Round: {round_start.strftime('%H:%M')} – { (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS)).strftime('%H:%M') }\n"
f"📊 Display multiplier: x{display_multiplier:.2f} (pool ratio)\n"
f"📊 New balance: **{player['points'] - amount}** points\n"
f"\nSee your open positions below:"
)
# Remove from buying_users if present (in case it was a Buy flow)
buying_users.discard(user_id)
# Show dashboard
await show_dashboard(update, context, page=context.user_data.get('buy_page', 0), is_edit=False, query=query)
async def pred_cancel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
active_confirmations.pop(user_id, None)
await query.edit_message_text("❌ Prediction cancelled. No points were deducted.")
# Remove from buying_users if present and return to dashboard
buying_users.discard(user_id)
await show_dashboard(update, context, page=context.user_data.get('buy_page', 0), is_edit=False, query=query)
# ========== MY PREDICTIONS ==========
async def mypredictions_command(update: Update, context: ContextTypes.DEFAULT_TYPE, query=None):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
text = "❌ You are not registered. Use /start."
if query:
await query.edit_message_text(text)
else:
await update.message.reply_text(text)
return
blocked, msg = is_player_blocked(player)
if blocked:
if query:
await query.edit_message_text(msg)
else:
await update.message.reply_text(msg)
return
await show_predictions_page(update, user_id, offset=0, query=query)
async def show_predictions_page(update, user_id, offset, query=None):
preds, total = get_user_predictions(user_id, limit=5, offset=offset)
if not preds:
if offset == 0:
text = "📭 You have no predictions yet."
else:
text = "📭 No more predictions."
if query:
await query.edit_message_text(text, parse_mode="Markdown")
else:
await update.message.reply_text(text, parse_mode="Markdown")
return
msg = "📊 **Your recent predictions**\n\n"
total_profit = 0
for p in preds:
direction = "UP" if p['direction'] == 1 else "DOWN"
status_emoji = {
"pending": "⏳",
"won": "✅",
"lost": "❌",
"draw": "🤝",
"cancelled": "🚫"
}.get(p['status'], "❓")
profit_str = f"+{p['profit']}" if p['profit'] and p['profit'] > 0 else f"{p['profit']}" if p['profit'] else "—"
currency = p.get('currency', 'points')
end_price_str = f"${p['end_price']:,.2f}" if p['end_price'] else "?"
round_time = p['created_at'].strftime('%H:%M') if p['created_at'] else "?"
multiplier_str = f"x{p['multiplier']:.2f}" if p['multiplier'] else ""
start_price_str = f"${p['start_price']:,.2f}" if p['start_price'] != 0 else "?"
msg += f"#{p['id']} {status_emoji} {direction} | Bet: {p['amount']} {currency} | Round: {round_time} | Start: {start_price_str} | End: {end_price_str} | Profit: {profit_str} | Multiplier: {multiplier_str}\n"
if p['profit']:
total_profit += p['profit']
if len(msg) > 4000:
msg += "..."
break
msg += f"\n📈 **Total Profit/Loss: {total_profit:+d}** {currency}"
keyboard = []
if offset > 0:
keyboard.append(InlineKeyboardButton("« Prev", callback_data=f"mypred_page_{offset-5}"))
if offset + 5 < total:
keyboard.append(InlineKeyboardButton("Next »", callback_data=f"mypred_page_{offset+5}"))
reply_markup = InlineKeyboardMarkup([keyboard]) if keyboard else None
if query:
await query.edit_message_text(msg, reply_markup=reply_markup, parse_mode="Markdown")
else:
await update.message.reply_text(msg, reply_markup=reply_markup, parse_mode="Markdown")
async def mypred_page_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
offset = int(query.data.split('_')[2])
user_id = update.effective_user.id
await show_predictions_page(update, user_id, offset, query=query)
# ========== BACKGROUND CHECKER ==========
async def check_predictions(bot):
try:
now = datetime.now()
pending = get_pending_predictions()
rounds = {}
for pred in pending:
key = pred['created_at']
if key not in rounds:
rounds[key] = []
rounds[key].append(pred)
# First, update start_price for rounds that have started but have start_price == 0
for round_start, preds in rounds.items():
if round_start <= now:
has_unknown = any(p['start_price'] == 0 for p in preds)
if has_unknown:
price = get_price_at_time(round_start)
if price:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE predictions SET start_price = ? WHERE created_at = ? AND start_price = 0",
(price, round_start.isoformat()))
conn.commit()
conn.close()
logger.info(f"Updated start_price for round {round_start} to {price}")
else:
price = get_btc_usd_rate()
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE predictions SET start_price = ? WHERE created_at = ? AND start_price = 0",
(price, round_start.isoformat()))
conn.commit()
conn.close()
logger.info(f"Updated start_price for round {round_start} to {price} (fallback)")
# Now resolve rounds that have ended
for round_start, preds in rounds.items():
resolve_time = round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS)
if now >= resolve_time:
end_price = get_btc_usd_rate()
start_price = preds[0]['start_price']
if start_price == 0:
start_price = get_btc_usd_rate()
if end_price > start_price:
actual_dir = 1
elif end_price < start_price:
actual_dir = 0
else:
actual_dir = -1
total_up, total_down, total_pool = get_round_totals(round_start)
if actual_dir == -1:
for pred in preds:
user_id = pred['user_id']
# --- NEW: Skip point updates for bot placeholders ---
if user_id == BOT_USER_ID:
resolve_prediction(pred['id'], end_price, "draw", 0, 1.0)
continue
amount = pred['amount']
currency = pred.get('currency', 'points')
if currency == 'points':
update_points(user_id, amount, "prediction_draw")
update_points(BOT_USER_ID, -amount, "pool_refund_draw")
profit = 0
outcome = "draw"
resolve_prediction(pred['id'], end_price, outcome, profit, 1.0)
direction_str = "UP" if pred['direction'] == 1 else "DOWN"
try:
await bot.send_message(
chat_id=user_id,
text=(
f"🔔 **Prediction #{pred['id']} resolved (DRAW)!**\n"
f"📈 You predicted: **{direction_str}**\n"
f"💲 Start price: ${start_price:,.2f}\n"
f"💲 End price: ${end_price:,.2f}\n"
f"💰 Result: **DRAW**\n"
f"📊 Your bet of {amount} {currency} has been refunded."
)
)
except Exception as e:
logger.error(f"Failed to notify user {user_id}: {e}")
continue
if actual_dir == 1:
winning_total = total_up
else:
winning_total = total_down
if winning_total == 0:
for pred in preds:
user_id = pred['user_id']
if user_id == BOT_USER_ID:
resolve_prediction(pred['id'], end_price, "draw", 0, 1.0)
continue
update_points(user_id, pred['amount'], "prediction_refund_no_winner")
update_points(BOT_USER_ID, -pred['amount'], "pool_refund_no_winner")
resolve_prediction(pred['id'], end_price, "draw", 0, 1.0)
continue
for pred in preds:
user_id = pred['user_id']
amount = pred['amount']
direction = pred['direction']
currency = pred.get('currency', 'points')
# --- NEW: Skip point updates for bot placeholders ---
if user_id == BOT_USER_ID:
# Resolve with no profit
resolve_prediction(pred['id'], end_price, "draw" if direction != actual_dir else "won", 0, 1.0)
continue
if direction == actual_dir:
payout = int((amount / winning_total) * total_pool)
profit = payout - amount
if currency == 'points':
update_points(user_id, payout, "prediction_win")
update_points(BOT_USER_ID, -payout, "pool_payout")
outcome = "won"
else:
profit = -amount
outcome = "lost"
multiplier_used = total_pool / winning_total if winning_total > 0 else 1.0
resolve_prediction(pred['id'], end_price, outcome, profit, multiplier_used)
direction_str = "UP" if direction == 1 else "DOWN"
try:
await bot.send_message(
chat_id=user_id,
text=(
f"🔔 **Prediction #{pred['id']} resolved!**\n"
f"📈 You predicted: **{direction_str}**\n"
f"💲 Start price: ${start_price:,.2f}\n"
f"💲 End price: ${end_price:,.2f}\n"
f"💰 Result: **{outcome.upper()}**\n"
f"📊 Payout: {profit:+d} {currency}\n"
f"Pool multiplier: x{multiplier_used:.2f}"
)
)
except Exception as e:
logger.error(f"Failed to notify user {user_id}: {e}")
except Exception as e:
logger.error(f"Error in prediction checker: {e}")
# ========== BOT PLACEHOLDER LOOP (NEW) ==========
async def bot_placeholder_loop(application):
"""Every second, check if a new round has started and create bot placeholders if missing."""
while True:
try:
now = datetime.now()
round_start = get_round_start_time()
# Only act if the round has started and is still open
if now >= round_start and now < round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
for direction in (1, 0): # UP, DOWN
# If bot doesn't have a placeholder for this direction
if get_bot_placeholder(round_start, direction) is None:
# And no other prediction (player or bot) exists for that direction in this round
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"SELECT COUNT(*) FROM predictions WHERE created_at = ? AND direction = ? AND status = 'pending'",
(round_start.isoformat(), direction)
)
count = c.fetchone()[0]
conn.close()
if count == 0:
create_bot_placeholder(round_start, direction)
except Exception as e:
logger.error(f"Bot placeholder loop error: {e}")
await asyncio.sleep(1) # check every second
async def prediction_loop(application):
while True:
try:
await check_predictions(application.bot)
except Exception as e:
logger.error(f"Prediction loop error: {e}")
await asyncio.sleep(10)
# ========== PROFIT / LOSS COMMAND ==========
async def profit_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await update.message.reply_text(msg)
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# BTC stats
c.execute("SELECT SUM(winner_amount) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
total_won_btc = c.fetchone()[0] or 0.0
c.execute("SELECT SUM(loser_amount) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
total_lost_btc = c.fetchone()[0] or 0.0
c.execute("SELECT COUNT(*) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
wins_btc = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'btc'", (user_id,))
losses_btc = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE (challenger_id = ? OR challenged_id = ?) AND winner_id IS NULL AND loser_id IS NULL AND status = 'finished' AND currency = 'btc'", (user_id, user_id))
draws_btc = c.fetchone()[0] or 0
# Points stats (challenge_points only, not dice/prediction)
c.execute("SELECT SUM(winner_amount) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
total_won_points = c.fetchone()[0] or 0
c.execute("SELECT SUM(loser_amount) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
total_lost_points = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE winner_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
wins_points = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE loser_id = ? AND status = 'finished' AND currency = 'points'", (user_id,))
losses_points = c.fetchone()[0] or 0
c.execute("SELECT COUNT(*) FROM games WHERE (challenger_id = ? OR challenged_id = ?) AND winner_id IS NULL AND loser_id IS NULL AND status = 'finished' AND currency = 'points'", (user_id, user_id))
draws_points = c.fetchone()[0] or 0
# Demo point stats (from point_transactions)
c.execute("SELECT SUM(amount) FROM point_transactions WHERE user_id = ? AND amount > 0", (user_id,))
points_won = c.fetchone()[0] or 0
c.execute("SELECT SUM(amount) FROM point_transactions WHERE user_id = ? AND amount < 0", (user_id,))
points_lost_abs = c.fetchone()[0] or 0
points_lost = abs(points_lost_abs)
points_net = points_won - points_lost
conn.close()
btc_profit = total_won_btc - total_lost_btc
points_profit = total_won_points - total_lost_points
msg = (
f"📊 **Your Profit / Loss**\n\n"
f"**🎲 Demo Points (dice + predictions):**\n"
f" ✅ Won: {points_won}\n"
f" ❌ Lost: {points_lost}\n"
f" 📈 Net: {points_net:+d}\n\n"
f"**🎲 Multiplayer Points:**\n"
f" ✅ Won: {total_won_points}\n"
f" ❌ Lost: {total_lost_points}\n"
f" 📈 Net: {points_profit:+d}\n"
f" 🏆 Wins: {wins_points} | 😢 Losses: {losses_points} | 🤝 Draws: {draws_points}\n\n"
f"**₿ BTC Multiplayer:**\n"
f" ✅ Won: {total_won_btc:.8f} BTC\n"
f" ❌ Lost: {total_lost_btc:.8f} BTC\n"
f" 📈 Net: {btc_profit:+.8f} BTC\n"
f" 🏆 Wins: {wins_btc} | 😢 Losses: {losses_btc} | 🤝 Draws: {draws_btc}"
)
await update.message.reply_text(msg, parse_mode="Markdown")
# ========== ROULETTE ==========
async def roulette_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await update.message.reply_text(msg)
return
if len(context.args) < 2:
await update.message.reply_text(
"❌ Usage: `/roulette <green/black/red> <amount>`\n"
"Example: `/roulette red 50`\n"
"Payouts: Green = 10x, Black/Red = 3x"
)
return
color = context.args[0].lower()
if color not in ("green", "black", "red"):
await update.message.reply_text("❌ Color must be `green`, `black` or `red`.")
return
try:
amount = int(context.args[1])
except ValueError:
await update.message.reply_text("❌ Amount must be a number.")
return
if amount <= 0:
await update.message.reply_text("❌ Bet must be greater than 0.")
return
if amount > player['points']:
await update.message.reply_text(f"❌ You only have {player['points']} points.")
return
# Roulette spin: 0 = green, 1-18 = red, 19-36 = black
num = random.randint(0, 36)
if num == 0:
result_color = "green"
elif num <= 18:
result_color = "red"
else:
result_color = "black"
if result_color == "green":
multiplier = 10 # multiplier green bet to player
else:
multiplier = 3 # multiplier black/red bet to player
# Deduct bet from player
update_points(user_id, -amount, "roulette_bet")
if color == result_color:
# Win – add the full win amount
total_win = amount * multiplier
update_points(user_id, total_win, "roulette_win") # add full win
update_points(BOT_USER_ID, -total_win, "roulette_payout") # bot pays full win
profit = total_win - amount
result_text = (
f"🎰 **Roulette result:** {result_color.upper()} ({num})\n"
f"✅ You won! Total win: **{total_win}** points\n"
f"💰 Profit: **+{profit}** points\n"
f"📊 New balance: **{player['points'] - amount + total_win}** points"
)
else:
# Lose – bot gains the bet
update_points(BOT_USER_ID, amount, "roulette_win") # bot gains the bet
result_text = (
f"🎰 **Roulette result:** {result_color.upper()} ({num})\n"
f"❌ You lost **{amount}** points.\n"
f"📊 New balance: **{player['points'] - amount}** points"
)
await update.message.reply_text(result_text, parse_mode="Markdown")
# ========== ID COMMAND ==========
async def id_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
username = update.effective_user.username or "No username"
await update.message.reply_text(
f"🆔 **Your Telegram ID:**\n`{user_id}`\n\n"
f"👤 **Username:** @{username}",
parse_mode="Markdown"
)
# ========== JOIN PREDICTION COMMAND ==========
async def join_prediction_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Allow a user to join an existing pending prediction by ID."""
user_id = update.effective_user.id
player = get_player(user_id)
if not player:
await update.message.reply_text("❌ You are not registered. Use /start.")
return
blocked, msg = is_player_blocked(player)
if blocked:
await update.message.reply_text(msg)
return
if len(context.args) < 2:
await update.message.reply_text("❌ Usage: `/joinpred <prediction_id> <amount>`\nExample: `/joinpred 42 50`")
return
try:
pred_id = int(context.args[0])
amount = int(context.args[1])
except ValueError:
await update.message.reply_text("❌ Prediction ID and amount must be numbers.")
return
if amount < PREDICTION_MIN_BET:
await update.message.reply_text(f"❌ Minimum bet is {PREDICTION_MIN_BET} points.")
return
if amount > player['points']:
await update.message.reply_text(f"❌ You only have {player['points']} points.")
return
# Fetch the target prediction
pred = get_prediction(pred_id)
if not pred:
await update.message.reply_text(f"❌ Prediction #{pred_id} not found.")
return
if pred['status'] != 'pending':
await update.message.reply_text(f"❌ Prediction #{pred_id} is already {pred['status']}.")
return
# Check if the round is still open
round_start = pred['created_at']
now = datetime.now()
if now > round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS):
await update.message.reply_text("❌ The round for this prediction has already closed.")
return
# Prevent joining your own prediction (optional, but makes sense)
if pred['user_id'] == user_id:
await update.message.reply_text("❌ You cannot join your own prediction. Use /predict to place a new one.")
return
# Deduct points from user
update_points(user_id, -amount, "join_prediction_bet")
update_points(BOT_USER_ID, amount, "pool_inflow_join")
# Create a new prediction record for the joiner with the same round and direction
new_pred_id = create_prediction(
user_id=user_id,
amount=amount,
direction=pred['direction'],
start_price=pred['start_price'],
currency='points',
round_start=round_start,
multiplier=pred['multiplier'] # we can recalc, but keep same for simplicity
)
# Recalculate multiplier for all predictions in this round if desired (but we keep existing)
# Optionally update the multiplier for all predictions in the round based on new totals.
# But the multiplier is mainly for display; actual payout uses pool totals at resolution.
# So we can just update the multiplier for the new prediction to reflect new pool ratio.
total_up, total_down, total_pool = get_round_totals(round_start)
if pred['direction'] == 1:
winning_total = total_up
else:
winning_total = total_down
if winning_total > 0:
new_mult = total_pool / winning_total
new_mult = max(1.0, new_mult)
else:
new_mult = 1.0
# Update the new prediction's multiplier
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE predictions SET multiplier = ? WHERE id = ?", (new_mult, new_pred_id))
conn.commit()
conn.close()
# Also update the original prediction's multiplier to reflect new pool?
# We can optionally update all predictions in that round to have the same multiplier.
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE predictions SET multiplier = ? WHERE created_at = ? AND direction = ? AND status = 'pending'",
(new_mult, round_start.isoformat(), pred['direction']))
conn.commit()
conn.close()
direction_str = "UP" if pred['direction'] == 1 else "DOWN"
await update.message.reply_text(
f"✅ **Joined prediction #{pred_id}**\n"
f"📈 Direction: **{direction_str}**\n"
f"💰 Your bet: **{amount}** points\n"
f"🕒 Round: {round_start.strftime('%H:%M')} – { (round_start + timedelta(seconds=PREDICTION_DURATION_SECONDS)).strftime('%H:%M') }\n"
f"📊 Current pool multiplier: x{new_mult:.2f}\n"
f"💲 Start price: ${pred['start_price']:,.2f}\n\n"
f"Your new balance: **{player['points'] - amount}** points"
)
# ========== LIST PREDICTIONS (ADMIN) ==========
async def admin_list_predictions(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_admin(update.effective_user.id):
await update.message.reply_text("⛔ Admin only.")
return
# We'll implement pagination via callback.
# We'll send the first page (offset=0)
await show_admin_pred_page(update, offset=0, query=None)
async def show_admin_pred_page(update, offset, query=None):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# Get total count
c.execute("SELECT COUNT(*) FROM predictions")
total = c.fetchone()[0]
# Fetch 5 predictions starting at offset
c.execute("SELECT id, user_id, amount, direction, start_price, end_price, status, created_at, resolved_at, profit, multiplier, currency FROM predictions ORDER BY id DESC LIMIT 5 OFFSET ?", (offset,))
rows = c.fetchall()
conn.close()
if not rows:
text = "📭 No predictions found."
if query:
await query.edit_message_text(text)
else:
await update.message.reply_text(text)
return
text = "📋 **List of Predictions**\n\n"
for row in rows:
pred_id, user_id, amount, direction, start_price, end_price, status, created_at, resolved_at, profit, multiplier, currency = row
player = get_player(user_id)
username = player['username'] if player else "Unknown"
direction_str = "UP" if direction == 1 else "DOWN"
status_emoji = {
"pending": "⏳",
"won": "✅",
"lost": "❌",
"draw": "🤝",
"cancelled": "🚫"
}.get(status, "❓")
created_str = datetime.fromisoformat(created_at).strftime('%Y-%m-%d %H:%M') if created_at else "?"
resolved_str = datetime.fromisoformat(resolved_at).strftime('%Y-%m-%d %H:%M') if resolved_at else "—"
profit_str = f"{profit:+d}" if profit is not None else "—"
start_price_str = f"${start_price:,.2f}" if start_price else "?"
end_price_str = f"${end_price:,.2f}" if end_price else "?"
text += (
f"**ID:** {pred_id} | {status_emoji} {status.upper()}\n"
f"👤 {username} (ID: {user_id})\n"
f"📈 {direction_str} | Bet: {amount} {currency}\n"
f"💲 Start: {start_price_str} | End: {end_price_str}\n"
f"📊 Profit: {profit_str} | Multiplier: x{multiplier:.2f}\n"
f"🕒 Created: {created_str} | Resolved: {resolved_str}\n\n"
)
# Pagination buttons
keyboard = []
nav = []
if offset > 0:
nav.append(InlineKeyboardButton("◀ Prev", callback_data=f"admin_pd_page_{offset-5}"))
if offset + 5 < total:
nav.append(InlineKeyboardButton("Next ▶", callback_data=f"admin_pd_page_{offset+5}"))
if nav:
keyboard.append(nav)
keyboard.append([InlineKeyboardButton("🔙 Close", callback_data="admin_pd_close")])
reply_markup = InlineKeyboardMarkup(keyboard)
if query:
await query.edit_message_text(text, reply_markup=reply_markup, parse_mode="Markdown")
else:
await update.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
async def admin_pd_page_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
offset = int(query.data.split('_')[3])
await show_admin_pred_page(update, offset, query=query)
async def admin_pd_close_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
await query.edit_message_text("✅ Closed prediction list.")
# ========== START & HELP ==========
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Show the start page with open positions."""
await show_dashboard(update, context, page=0)
async def openpositions_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Show the open positions dashboard."""
await show_dashboard(update, context, page=0)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
f"🎲 <b>Dice Bot – Help</b>\n\n"
f"<b>Demo Mode (vs Bot – points):</b>\n"
f"/dice <bet> – Roll against the bot (points bet)\n"
f"/roulette <green/black/red> <points> – Roulette: Green = 10x, Red/Black = 3x (bank is the bot)\n"
f"/balance – Your point balance & net profit/loss\n"
f"/leaderboard – Top 10 players\n\n"
f"<b>Multiplayer (BTC / Points – only groups):</b>\n"
f"/challenge @User <amount_in_BTC> – Challenge a player with BTC\n"
f"• Minimum bet: <b>${MINIMUM_BET_USD:.2f}</b> (in BTC)\n"
f"/challenge_points @User <amount> – Challenge a player with points\n"
f"• Minimum bet: {PREDICTION_MIN_BET} points\n"
f"/accept – Accept the challenge (or button)\n"
f"/decline – Decline (or button)\n"
f"/cancel – Cancel your own challenge\n\n"
f"<b>Prediction Market (5min BTC/USD, points):</b>\n"
f"/predict up/down <points> – Bet on price direction (you'll be asked to confirm)\n"
f"/predict – Show BTC price history with current price and 5‑min changes (auto-refreshes every second)\n"
f"/mypredictions – View your prediction history with pagination (Prev/Next)\n"
f"/joinpred <prediction_id> <amount> – Join an existing open prediction (same round & direction)\n"
f"• Your open positions are displayed in a <b>combined dashboard</b> with pagination.\n"
f"• From the dashboard you can <b>Buy UP</b> or <b>Buy DOWN</b> – each places a fixed bet of <b>10 points</b> and shows a confirmation screen.\n"
f"• Each position has a <b>Sell</b> button to cancel it (if still open).\n"
f"• Open positions are <b>auto‑refreshed every {REFRESH_INTERVAL} second</b> (except the final 1 second).\n"
f"• Auto‑refresh is <b>paused</b> while you are in the Buy process and stops when you go to Menu.\n"
f"• The <b>start price</b> is captured at the exact round start time (e.g., 1:05, 1:10, etc.) using CoinGecko price history.\n"
f"• The dashboard shows the <b>current round's start price and price difference</b>.\n"
f"• Payouts are <b>pool‑based</b> (Polymarket‑style): winners share the total pool proportionally.\n"
f"• The bot acts as the pool custodian – it does not take a cut.\n"
f"• If price is unchanged, bets are refunded (draw).\n\n"
f"<b>BTC Wallet (private chat only):</b>\n"
f"/createwallet – Create new BTC wallet\n"
f"/importwallet <PrivateKey> – Import existing wallet\n"
f"/wallet – Show BTC address + balance + net profit/loss\n"
f"/setbtc <Address> – Manually set address (no Private Key)\n"
f"/unsetbtc – Delete your address (with confirmation)\n\n"
f"<b>Statistics:</b>\n"
f"/profit – Show your BTC profit/loss, points multiplayer profit/loss, and demo point profit/loss\n\n"
f"<b>Other commands:</b>\n"
f"/start – Overview\n"
f"/openpositions – Open positions dashboard\n"
f"/id – Your Telegram ID\n"
f"/help – This help\n\n"
f"<b>Admin Commands (owner only):</b>\n"
f"/listpd – List all predictions with pagination (5 per page)\n"
f"/addpoints <@user or ID> <points> – Add demo points\n"
f"/removepoints <@user or ID> <points> – Remove demo points\n"
f"/close_prediction <id> – Cancel a pending prediction by ID\n"
f"/listwallets – Show all BTC wallets\n"
f"/matches – Show all games\n"
f"/cancelmatch <game_id> – Cancel a match\n"
f"/setwinner <game_id> <@user or ID> – Set winner manually\n"
f"/finishmatch <game_id> – End match (draw)\n"
f"/removematch <game_id> – Delete match from DB\n"
f"/ban <@user or ID>\n"
f"/unban <@user or ID>\n"
f"/timeout <@user or ID> <minutes>\n"
f"/untimeout <@user or ID>",
parse_mode="HTML"
)
# ========== MAIN ==========
def main():
if TELEGRAM_TOKEN == "DEIN_BOT_TOKEN":
print("\n❌ ERROR: You must insert your real bot token in the TELEGRAM_TOKEN variable!\n")
return
init_db()
application = Application.builder().token(TELEGRAM_TOKEN).build()
# Command handlers
application.add_handler(CommandHandler("dice", dice_command))
application.add_handler(CommandHandler("roulette", roulette_command))
application.add_handler(CommandHandler("balance", balance_command))
application.add_handler(CommandHandler("leaderboard", leaderboard_command))
application.add_handler(CommandHandler("challenge", challenge_command))
application.add_handler(CommandHandler("challenge_points", challenge_points_command))
application.add_handler(CommandHandler("accept", decline_command))
application.add_handler(CommandHandler("decline", decline_command))
application.add_handler(CommandHandler("cancel", cancel_command))
application.add_handler(CommandHandler("createwallet", createwallet_command))
application.add_handler(CommandHandler("importwallet", importwallet_command))
application.add_handler(CommandHandler("setbtc", setbtc_command))
application.add_handler(CommandHandler("wallet", wallet_command))
application.add_handler(CommandHandler("unsetbtc", unsetbtc_command))
application.add_handler(CommandHandler("listwallets", listwallets_command))
application.add_handler(CommandHandler("addpoints", admin_addpoints))
application.add_handler(CommandHandler("removepoints", admin_removepoints))
application.add_handler(CommandHandler("close_prediction", admin_close_prediction))
application.add_handler(CommandHandler("matches", admin_matches))
application.add_handler(CommandHandler("cancelmatch", admin_cancelmatch))
application.add_handler(CommandHandler("setwinner", admin_setwinner))
application.add_handler(CommandHandler("finishmatch", admin_finishmatch))
application.add_handler(CommandHandler("removematch", admin_removematch))
application.add_handler(CommandHandler("ban", admin_ban))
application.add_handler(CommandHandler("unban", admin_unban))
application.add_handler(CommandHandler("timeout", admin_timeout))
application.add_handler(CommandHandler("untimeout", admin_untimeout))
application.add_handler(CommandHandler("predict", predict_command))
application.add_handler(CommandHandler("mypredictions", mypredictions_command))
application.add_handler(CommandHandler("openpositions", openpositions_command))
application.add_handler(CommandHandler("joinpred", join_prediction_command)) # NEW
# Prediction callbacks
application.add_handler(CallbackQueryHandler(pred_refresh_callback, pattern="^pred_refresh_"))
application.add_handler(CallbackQueryHandler(pred_confirm_callback, pattern="^pred_confirm_"))
application.add_handler(CallbackQueryHandler(pred_cancel_callback, pattern="^pred_cancel"))
# Dashboard callbacks
application.add_handler(CallbackQueryHandler(dash_page_callback, pattern="^dash_page_"))
application.add_handler(CallbackQueryHandler(dash_refresh_callback, pattern="^dash_refresh$"))
application.add_handler(CallbackQueryHandler(dash_menu_callback, pattern="^dash_menu$"))
# Buy UP/DOWN callbacks (direct)
application.add_handler(CallbackQueryHandler(dash_buy_up_callback, pattern="^dash_buy_up$"))
application.add_handler(CallbackQueryHandler(dash_buy_down_callback, pattern="^dash_buy_down$"))
# Sell callbacks
application.add_handler(CallbackQueryHandler(pos_sell_callback, pattern=r"^pos_sell_\d+$"))
application.add_handler(CallbackQueryHandler(pos_sell_confirm_callback, pattern=r"^pos_sell_confirm_\d+$"))
application.add_handler(CallbackQueryHandler(pos_sell_cancel_callback, pattern=r"^pos_sell_cancel_\d+$"))
# Other callbacks
application.add_handler(CallbackQueryHandler(accept_callback, pattern="^accept_"))
application.add_handler(CallbackQueryHandler(decline_callback, pattern="^decline_"))
application.add_handler(CallbackQueryHandler(unsetbtc_confirm_callback, pattern="^unsetbtc_confirm_"))
application.add_handler(CallbackQueryHandler(unsetbtc_cancel_callback, pattern="^unsetbtc_cancel"))
# My predictions page
application.add_handler(CallbackQueryHandler(mypred_page_callback, pattern="^mypred_page_"))
# Admin list predictions
application.add_handler(CommandHandler("listpd", admin_list_predictions))
application.add_handler(CallbackQueryHandler(admin_pd_page_callback, pattern="^admin_pd_page_"))
application.add_handler(CallbackQueryHandler(admin_pd_close_callback, pattern="^admin_pd_close$"))
# Menu callbacks
application.add_handler(CallbackQueryHandler(menu_dice_callback, pattern="^menu_dice$"))
application.add_handler(CallbackQueryHandler(menu_roulette_callback, pattern="^menu_roulette$"))
application.add_handler(CallbackQueryHandler(menu_predict_callback, pattern="^menu_predict$"))
application.add_handler(CallbackQueryHandler(menu_wallet_callback, pattern="^menu_wallet$"))
application.add_handler(CallbackQueryHandler(menu_profit_callback, pattern="^menu_profit$"))
application.add_handler(CommandHandler("profit", profit_command))
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("id", id_command))
application.add_handler(CommandHandler("help", help_command))
# Background tasks
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.create_task(prediction_loop(application))
loop.create_task(auto_refresh_dashboards(application))
loop.create_task(auto_refresh_confirmations(application))
loop.create_task(reset_bot_points_loop(application)) # reset bot points daily
loop.create_task(bot_placeholder_loop(application)) # NEW: bot placeholders
print("\n" + "=" * 50)
print("🎲 DICE BOT (DEMOPOINTS + BTC MULTIPLAYER + POINTS MULTIPLAYER + PREDICTIONS + ROULETTE + ADMIN)")
print("=" * 50)
print(f"📅 Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"📁 Database: {DB_PATH}")
print(f"👑 Admin ID: {ADMIN_USER_ID}")
print(f"🤖 Bot Player: ID {BOT_USER_ID} with {BOT_POINTS} points (reset daily)")
print(f"🔗 APIs: blockchain.info + BlockCypher + CoinGecko")
print(f"💵 Min bet: ${MINIMUM_BET_USD:.2f} (BTC)")
print(f"🔄 Auto‑refresh: every {REFRESH_INTERVAL} second, stops 1s before round end, paused during Buy and stopped on Menu")
print(f"👥 Players: {len(get_all_btc_addresses())} registered")
print("-" * 50)
print("⏳ Bot is running... (Press Ctrl+C to stop)")
print("=" * 50 + "\n")
logger.info("Bot started")
application.run_polling()
if __name__ == "__main__":
main()
To embed this project on your website, copy the following code and paste it into your website's HTML: