From 0fbf1f0200decd1a1d35d1d01c75eede8b6c9f94 Mon Sep 17 00:00:00 2001 From: t1amak Date: Tue, 27 May 2025 11:13:33 +0000 Subject: [PATCH] Initial commit - purely ai generated code --- .gitignore | 8 ++ LICENSE | 15 ++++ README.md | 28 +++++++ salvium_tipbot.php | 112 ++++++++++++++++++++++++++ salvium_tipbot_monitor.php | 112 ++++++++++++++++++++++++++ src/salvium_tipbot_db.php | 147 ++++++++++++++++++++++++++++++++++ src/salvium_tipbot_wallet.php | 102 +++++++++++++++++++++++ 7 files changed, 524 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 salvium_tipbot.php create mode 100644 salvium_tipbot_monitor.php create mode 100644 src/salvium_tipbot_db.php create mode 100644 src/salvium_tipbot_wallet.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d32adca --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +// .gitignore +// config.php +config.php +*.log +.DS_Store +*.swp +.vscode/ +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dbe6e02 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +// LICENSE +/* +MIT License + +Copyright (c) 2025 [Your Name] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. +*/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4d3a23 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +// README.md +/* +# Salvium Tip Bot + +A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency. + +## Features +- Telegram tipping via subaddresses +- Wallet interaction via JSON-RPC +- Secure MySQL backend using PDO +- Deposit, withdraw, balance, and tipping commands +- Cron-compatible monitoring of deposits and withdrawals + +## Installation +1. Clone the repo +2. Update `config.php` with your RPC, DB, and Telegram Bot credentials +3. Run `salvium_tipbot.php` as a Telegram webhook listener +4. Schedule `salvium_tipbot_monitor.php` using `cron` for periodic checks + +## Requirements +- PHP 8.1+ +- MySQL 5.7+/MariaDB +- Telegram Bot Token +- Salvium RPC Wallet node + +## License +MIT +*/ diff --git a/salvium_tipbot.php b/salvium_tipbot.php new file mode 100644 index 0000000..aecbe80 --- /dev/null +++ b/salvium_tipbot.php @@ -0,0 +1,112 @@ + $chatId, 'text' => $text], $options); + file_get_contents("https://api.telegram.org/bot{$config['TELEGRAM_BOT_TOKEN']}/sendMessage?" . http_build_query($payload)); +} + +$update = json_decode(file_get_contents("php://input"), true); +if (!$update || !isset($update['message'])) exit; + +$message = $update['message']; +$chatId = $message['chat']['id']; +$userId = $message['from']['id']; +$username = $message['from']['username'] ?? ''; +$text = trim($message['text'] ?? ''); +$args = explode(' ', $text); +$command = strtolower($args[0] ?? ''); + +switch ($command) { + case '/start': + sendMessage($chatId, "Welcome to the Salvium Tip Bot! Use /deposit to get started."); + break; + + case '/deposit': + $user = $db->getUserByTelegramId($userId); + if (!$user) { + $subaddress = $wallet->getNewSubaddress(); + if (!$subaddress) { + sendMessage($chatId, "Error generating subaddress. Try again later."); + exit; + } + $db->createUser($userId, $subaddress); + $user = ['salvium_subaddress' => $subaddress]; + } + sendMessage($chatId, "Your Salvium deposit address is: {$user['salvium_subaddress']}"); + break; + + case '/balance': + $user = $db->getUserByTelegramId($userId); + if (!$user) { + sendMessage($chatId, "You don't have an account yet. Use /deposit to create one."); + break; + } + sendMessage($chatId, "Your balance: {$user['tip_balance']} XSL."); + break; + + case '/withdraw': + if (count($args) < 3) { + sendMessage($chatId, "Usage: /withdraw
"); + break; + } + list(, $address, $amount) = $args; + $amount = (float)$amount; + $user = $db->getUserByTelegramId($userId); + if (!$user || $user['tip_balance'] < $amount) { + sendMessage($chatId, "Insufficient balance or invalid account."); + break; + } + if (!preg_match('/^4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$/', $address)) { + sendMessage($chatId, "Invalid address format."); + break; + } + $db->updateUserTipBalance($user['id'], $amount, 'subtract'); + $db->logWithdrawal($user['id'], $address, $amount); + sendMessage($chatId, "Withdrawal request submitted. Processing soon."); + break; + + default: + if (str_starts_with($command, '/tip')) { + if (count($args) < 3) { + sendMessage($chatId, "Usage: /tip "); + break; + } + list(, $targetUsername, $amount) = $args; + $amount = (float)$amount; + $sender = $db->getUserByTelegramId($userId); + $recipient = $db->getUserByTelegramId(ltrim($targetUsername, '@')); + if (!$sender || $sender['tip_balance'] < $amount) { + sendMessage($chatId, "Insufficient funds or invalid sender."); + break; + } + if (!$recipient) { + sendMessage($chatId, "Recipient not found. Ask them to run /start first."); + break; + } + $db->updateUserTipBalance($sender['id'], $amount, 'subtract'); + $db->addTip($sender['id'], $recipient['id'], $amount, $chatId); + sendMessage($chatId, "Tipped {$targetUsername} {$amount} XSL successfully!"); + sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} XSL! Use /balance to check."); + } else { + sendMessage($chatId, "Unknown command."); + } + break; +} +?> diff --git a/salvium_tipbot_monitor.php b/salvium_tipbot_monitor.php new file mode 100644 index 0000000..aecbe80 --- /dev/null +++ b/salvium_tipbot_monitor.php @@ -0,0 +1,112 @@ + $chatId, 'text' => $text], $options); + file_get_contents("https://api.telegram.org/bot{$config['TELEGRAM_BOT_TOKEN']}/sendMessage?" . http_build_query($payload)); +} + +$update = json_decode(file_get_contents("php://input"), true); +if (!$update || !isset($update['message'])) exit; + +$message = $update['message']; +$chatId = $message['chat']['id']; +$userId = $message['from']['id']; +$username = $message['from']['username'] ?? ''; +$text = trim($message['text'] ?? ''); +$args = explode(' ', $text); +$command = strtolower($args[0] ?? ''); + +switch ($command) { + case '/start': + sendMessage($chatId, "Welcome to the Salvium Tip Bot! Use /deposit to get started."); + break; + + case '/deposit': + $user = $db->getUserByTelegramId($userId); + if (!$user) { + $subaddress = $wallet->getNewSubaddress(); + if (!$subaddress) { + sendMessage($chatId, "Error generating subaddress. Try again later."); + exit; + } + $db->createUser($userId, $subaddress); + $user = ['salvium_subaddress' => $subaddress]; + } + sendMessage($chatId, "Your Salvium deposit address is: {$user['salvium_subaddress']}"); + break; + + case '/balance': + $user = $db->getUserByTelegramId($userId); + if (!$user) { + sendMessage($chatId, "You don't have an account yet. Use /deposit to create one."); + break; + } + sendMessage($chatId, "Your balance: {$user['tip_balance']} XSL."); + break; + + case '/withdraw': + if (count($args) < 3) { + sendMessage($chatId, "Usage: /withdraw
"); + break; + } + list(, $address, $amount) = $args; + $amount = (float)$amount; + $user = $db->getUserByTelegramId($userId); + if (!$user || $user['tip_balance'] < $amount) { + sendMessage($chatId, "Insufficient balance or invalid account."); + break; + } + if (!preg_match('/^4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$/', $address)) { + sendMessage($chatId, "Invalid address format."); + break; + } + $db->updateUserTipBalance($user['id'], $amount, 'subtract'); + $db->logWithdrawal($user['id'], $address, $amount); + sendMessage($chatId, "Withdrawal request submitted. Processing soon."); + break; + + default: + if (str_starts_with($command, '/tip')) { + if (count($args) < 3) { + sendMessage($chatId, "Usage: /tip "); + break; + } + list(, $targetUsername, $amount) = $args; + $amount = (float)$amount; + $sender = $db->getUserByTelegramId($userId); + $recipient = $db->getUserByTelegramId(ltrim($targetUsername, '@')); + if (!$sender || $sender['tip_balance'] < $amount) { + sendMessage($chatId, "Insufficient funds or invalid sender."); + break; + } + if (!$recipient) { + sendMessage($chatId, "Recipient not found. Ask them to run /start first."); + break; + } + $db->updateUserTipBalance($sender['id'], $amount, 'subtract'); + $db->addTip($sender['id'], $recipient['id'], $amount, $chatId); + sendMessage($chatId, "Tipped {$targetUsername} {$amount} XSL successfully!"); + sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} XSL! Use /balance to check."); + } else { + sendMessage($chatId, "Unknown command."); + } + break; +} +?> diff --git a/src/salvium_tipbot_db.php b/src/salvium_tipbot_db.php new file mode 100644 index 0000000..9af18ca --- /dev/null +++ b/src/salvium_tipbot_db.php @@ -0,0 +1,147 @@ +pdo = new PDO($dsn, $config['DB_USER'], $config['DB_PASS']); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $e) { + error_log("Database connection failed: " . $e->getMessage()); + die("Database connection error."); + } + } + + // --- Table Creation SQL (Commented for reference) --- + /* + CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + telegram_user_id BIGINT UNIQUE NOT NULL, + salvium_subaddress VARCHAR(128) UNIQUE NOT NULL, + tip_balance DECIMAL(20, 12) DEFAULT 0.000000000000, + withdrawal_address VARCHAR(128), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ); + + CREATE TABLE deposits ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + txid VARCHAR(64) UNIQUE NOT NULL, + amount DECIMAL(20, 12) NOT NULL, + block_height INT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE withdrawals ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + txid VARCHAR(64), + address VARCHAR(128) NOT NULL, + amount DECIMAL(20, 12) NOT NULL, + status ENUM('pending', 'sent', 'failed') DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE tips ( + id INT PRIMARY KEY AUTO_INCREMENT, + sender_user_id INT NOT NULL, + recipient_user_id INT NOT NULL, + amount DECIMAL(20, 12) NOT NULL, + channel_id BIGINT, + status ENUM('pending', 'credited') DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (sender_user_id) REFERENCES users(id), + FOREIGN KEY (recipient_user_id) REFERENCES users(id) + ); + */ + + public function getUserByTelegramId(int $telegramUserId): array|false { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE telegram_user_id = ?"); + $stmt->execute([$telegramUserId]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function getUserBySubaddress(string $subaddress): array|false { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE salvium_subaddress = ?"); + $stmt->execute([$subaddress]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function createUser(int $telegramUserId, string $subaddress): bool { + $stmt = $this->pdo->prepare("INSERT INTO users (telegram_user_id, salvium_subaddress) VALUES (?, ?)"); + return $stmt->execute([$telegramUserId, $subaddress]); + } + + public function updateUserTipBalance(int $userId, float $amount, string $operation = 'add'): bool { + $sql = $operation === 'add' ? "UPDATE users SET tip_balance = tip_balance + ? WHERE id = ?" : "UPDATE users SET tip_balance = tip_balance - ? WHERE id = ?"; + $stmt = $this->pdo->prepare($sql); + return $stmt->execute([$amount, $userId]); + } + + public function setWithdrawalAddress(int $userId, string $address): bool { + $stmt = $this->pdo->prepare("UPDATE users SET withdrawal_address = ? WHERE id = ?"); + return $stmt->execute([$address, $userId]); + } + + public function logDeposit(int $userId, string $txid, float $amount, int $blockHeight): bool { + $stmt = $this->pdo->prepare("INSERT INTO deposits (user_id, txid, amount, block_height) VALUES (?, ?, ?, ?)"); + return $stmt->execute([$userId, $txid, $amount, $blockHeight]); + } + + public function isTxidLogged(string $txid): bool { + $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM deposits WHERE txid = ?"); + $stmt->execute([$txid]); + return $stmt->fetchColumn() > 0; + } + + public function logWithdrawal(int $userId, string $address, float $amount): int|false { + $stmt = $this->pdo->prepare("INSERT INTO withdrawals (user_id, address, amount) VALUES (?, ?, ?)"); + if ($stmt->execute([$userId, $address, $amount])) { + return $this->pdo->lastInsertId(); + } + return false; + } + + public function updateWithdrawalTxid(int $withdrawalId, string $txid): bool { + $stmt = $this->pdo->prepare("UPDATE withdrawals SET txid = ? WHERE id = ?"); + return $stmt->execute([$txid, $withdrawalId]); + } + + public function updateWithdrawalStatus(int $withdrawalId, string $status): bool { + $stmt = $this->pdo->prepare("UPDATE withdrawals SET status = ? WHERE id = ?"); + return $stmt->execute([$status, $withdrawalId]); + } + + public function addTip(int $senderUserId, int $recipientUserId, float $amount, ?int $channelId = null): bool { + $stmt = $this->pdo->prepare("INSERT INTO tips (sender_user_id, recipient_user_id, amount, channel_id) VALUES (?, ?, ?, ?)"); + return $stmt->execute([$senderUserId, $recipientUserId, $amount, $channelId]); + } + + public function getPendingTipsForUser(int $recipientUserId): array { + $stmt = $this->pdo->prepare("SELECT * FROM tips WHERE recipient_user_id = ? AND status = 'pending'"); + $stmt->execute([$recipientUserId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function markTipsAsCredited(array $tipIds): bool { + $placeholders = implode(',', array_fill(0, count($tipIds), '?')); + $stmt = $this->pdo->prepare("UPDATE tips SET status = 'credited' WHERE id IN ($placeholders)"); + return $stmt->execute($tipIds); + } + + public function getPendingWithdrawals(): array { + $stmt = $this->pdo->query("SELECT * FROM withdrawals WHERE status = 'pending'"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } +} +?> diff --git a/src/salvium_tipbot_wallet.php b/src/salvium_tipbot_wallet.php new file mode 100644 index 0000000..0713b7b --- /dev/null +++ b/src/salvium_tipbot_wallet.php @@ -0,0 +1,102 @@ +host = $host; + $this->port = $port; + $this->username = $username; + $this->password = $password; + } + + private function _callRpc(string $method, array $params = []): array|false { + $url = "http://{$this->host}:{$this->port}/json_rpc"; + $request = json_encode([ + 'jsonrpc' => '2.0', + 'id' => '0', + 'method' => $method, + 'params' => $params + ]); + + $headers = ['Content-Type: application/json']; + $options = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $request, + CURLOPT_HTTPHEADER => $headers, + ]; + + if ($this->username && $this->password) { + $options[CURLOPT_USERPWD] = "{$this->username}:{$this->password}"; + } + + $ch = curl_init(); + curl_setopt_array($ch, $options); + $response = curl_exec($ch); + + if (curl_errno($ch)) { + error_log('RPC Curl Error: ' . curl_error($ch)); + curl_close($ch); + return false; + } + + curl_close($ch); + $decoded = json_decode($response, true); + return $decoded['result'] ?? false; + } + + public function getWalletBalance(): array|false { + return $this->_callRpc('get_balance'); + } + + public function getNewSubaddress(int $accountIndex = 0, ?string $label = null): string|false { + $params = ['account_index' => $accountIndex]; + if ($label) $params['label'] = $label; + $result = $this->_callRpc('create_address', $params); + return $result['address'] ?? false; + } + + public function getAddresses(): array|false { + $result = $this->_callRpc('get_address'); + return $result['addresses'] ?? false; + } + + public function transfer(array $destinations, int $mixin = 11, int $unlockTime = 0, bool $getTxKey = false, bool $doNotRelay = false): array|false { + $params = [ + 'destinations' => $destinations, + 'mixin' => $mixin, + 'unlock_time' => $unlockTime, + 'get_tx_key' => $getTxKey, + 'do_not_relay' => $doNotRelay + ]; + return $this->_callRpc('transfer', $params); + } + + public function getTransfers(string $inOrOut = 'in', bool $pending = false, bool $failed = false): array|false { + $params = [ + $inOrOut => true, + 'pending' => $pending, + 'failed' => $failed + ]; + $result = $this->_callRpc('get_transfers', $params); + return $result[$inOrOut] ?? false; + } + + public function getPayments(string $paymentId): array|false { + $result = $this->_callRpc('get_payments', ['payment_id' => $paymentId]); + return $result['payments'] ?? false; + } + + public function getHeight(): int|false { + $result = $this->_callRpc('get_height'); + return $result['height'] ?? false; + } +} +?>