diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
index dbe6e02..10828e1
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,3 @@
-// LICENSE
-/*
MIT License
Copyright (c) 2025 [Your Name]
@@ -12,4 +10,4 @@ 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
old mode 100644
new mode 100755
index bd7e522..051c22d
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-// README.md
-/*
# Salvium Tip Bot
A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency.
@@ -26,4 +24,3 @@ A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency.
## License
MIT
-*/
diff --git a/config.sample.php b/config.sample.php
old mode 100644
new mode 100755
index ef7f5ee..2257685
--- a/config.sample.php
+++ b/config.sample.php
@@ -12,4 +12,5 @@ return [
'DB_CHARSET' => 'utf8mb4',
'TELEGRAM_BOT_TOKEN' => 'YOUR_TELEGRAM_BOT_TOKEN_HERE',
+ 'IPV4_ONLY' => 1
];
diff --git a/salvium_tipbot.php b/salvium_tipbot.php
old mode 100644
new mode 100755
index aecbe80..a34e15e
--- a/salvium_tipbot.php
+++ b/salvium_tipbot.php
@@ -7,6 +7,8 @@ use Salvium\SalviumWallet;
$config = require __DIR__ . '/config.php';
require_once __DIR__ . '/src/salvium_tipbot_db.php';
require_once __DIR__ . '/src/salvium_tipbot_wallet.php';
+require_once __DIR__ . '/src/salvium_tipbot_common.php';
+require_once __DIR__ . '/src/salvium_tipbot_commands.php';
$db = new SalviumTipBotDB($config);
$wallet = new SalviumWallet(
@@ -16,97 +18,21 @@ $wallet = new SalviumWallet(
$config['SALVIUM_RPC_PASSWORD']
);
-function sendMessage(int $chatId, string $text, array $options = []): void {
- global $config;
- $payload = array_merge(['chat_id' => $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);
+$args = explode(' ', trim($message['text'] ?? ''));
$command = strtolower($args[0] ?? '');
-switch ($command) {
- case '/start':
- sendMessage($chatId, "Welcome to the Salvium Tip Bot! Use /deposit to get started.");
- break;
+$context = [
+ 'chat_id' => $message['chat']['id'],
+ 'chat_name' => $message['chat']['title'] ?? $message['chat']['first_name'] ?? '',
+ 'username' => $message['from']['username'] ?? '',
+ 'user_id' => (int) $message['from']['id'],
+ 'raw' => $message['text'] ?? '',
+];
- 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;
-}
+$handler = new SalviumTipBotCommands($db, $wallet);
+$handler->handle($command, $args, $context);
?>
diff --git a/salvium_tipbot_monitor.php b/salvium_tipbot_monitor.php
old mode 100644
new mode 100755
index aecbe80..ad9895f
--- a/salvium_tipbot_monitor.php
+++ b/salvium_tipbot_monitor.php
@@ -1,12 +1,12 @@
$chatId, 'text' => $text], $options);
- file_get_contents("https://api.telegram.org/bot{$config['TELEGRAM_BOT_TOKEN']}/sendMessage?" . http_build_query($payload));
+// Handle incoming transfers
+$incoming = $wallet->getTransfers('in');
+if ($incoming) {
+ foreach ($incoming as $tx) {
+ if ($db->isTxidLogged($tx['txid'])) continue;
+
+ $subaddress = $tx['address'];
+ $user = $db->getUserBySubaddress($subaddress);
+ if (!$user) continue;
+
+ $amount = $tx['amount'] / 1e8; // Convert atomic to major units (1 SAL = 1e8 atomic)
+ $db->logDeposit($user['id'], $tx['txid'], $amount, $tx['height']);
+ $db->updateUserTipBalance($user['id'], $amount, 'add');
+
+ sendMessage($user['telegram_user_id'], "Deposit received: {$amount} SAL added to your balance.");
+ }
}
-$update = json_decode(file_get_contents("php://input"), true);
-if (!$update || !isset($update['message'])) exit;
+// Handle pending withdrawals
+$withdrawals = $db->getPendingWithdrawals();
+foreach ($withdrawals as $withdrawal) {
+ $result = $wallet->transfer([
+ [
+ 'address' => $withdrawal['address'],
+ 'amount' => (int)($withdrawal['amount'] * 1e8) // major to atomic
+ ]
+ ]);
-$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;
+ if ($result && isset($result['tx_hash'])) {
+ $db->updateWithdrawalTxid($withdrawal['id'], $result['tx_hash']);
+ $db->updateWithdrawalStatus($withdrawal['id'], 'sent');
+ sendMessage($withdrawal['user_id'], "Withdrawal of {$withdrawal['amount']} SAL sent. TxID: {$result['tx_hash']}");
+ } else {
+ $db->updateWithdrawalStatus($withdrawal['id'], 'failed');
+ sendMessage($withdrawal['user_id'], "Withdrawal failed. Please try again later or contact support.");
+ }
}
+
+// Handle pending tips
+$users = [];
+$tips = $db->getAllPendingTips();
+foreach ($tips as $tip) {
+ $recipientId = $tip['recipient_user_id'];
+ if (!isset($users[$recipientId])) {
+ $users[$recipientId] = $db->getUserByTelegramId($recipientId);
+ }
+ $db->updateUserTipBalance($recipientId, $tip['amount'], 'add');
+ $db->markTipsAsCredited([$tip['id']]);
+ sendMessage($recipientId, "You received a credited tip of {$tip['amount']} SAL. Use /balance to check.");
+}
+
?>
diff --git a/src/salvium_tipbot_commands.php b/src/salvium_tipbot_commands.php
new file mode 100644
index 0000000..e359696
--- /dev/null
+++ b/src/salvium_tipbot_commands.php
@@ -0,0 +1,87 @@
+db = $db;
+ $this->wallet = $wallet;
+ }
+
+ public function handle(string $command, array $args, array $context): void {
+ $method = 'cmd_' . ltrim($command, '/');
+ if (method_exists($this, $method)) {
+ $response = $this->$method($args, $context);
+ } else {
+ //$response = "Unknown command.";
+ $response = "";
+ }
+
+ sendMessage($context['chat_id'], $response);
+ $this->db->logMessage($context['chat_id'], $context['chat_name'], $context['username'], $context['raw'], $response);
+ }
+
+ private function cmd_start(array $args, array $ctx): string {
+ if (!empty($ctx['username'])) {
+ $this->db->updateUsername($ctx['user_id'], $ctx['username']);
+ }
+ return "Welcome to the Salvium Tip Bot! Use /deposit to get started.";
+ }
+
+ private function cmd_deposit(array $args, array $ctx): string {
+ $user = $this->db->getUserByTelegramId($ctx['user_id']);
+ if (!$user) {
+ $sub = $this->wallet->getNewSubaddress();
+ if (!$sub) return "Error generating subaddress.";
+ $this->db->createUser($ctx['user_id'], $sub);
+ if (!empty($ctx['username'])) {
+ $this->db->updateUsername($ctx['user_id'], $ctx['username']);
+ }
+ return "Your deposit address: $sub";
+ }
+ return "Your deposit address: {$user['salvium_subaddress']}";
+ }
+
+ private function cmd_balance(array $args, array $ctx): string {
+ $user = $this->db->getUserByTelegramId($ctx['user_id']);
+ return $user ? "Your balance: {$user['tip_balance']} SAL" : "No account found. Use /deposit first.";
+ }
+
+ private function cmd_withdraw(array $args, array $ctx): string {
+ if (count($args) < 3) return "Usage: /withdraw ";
+
+ list(, $address, $amount) = $args;
+ $amount = (float) $amount;
+ $user = $this->db->getUserByTelegramId($ctx['user_id']);
+
+ if (!$user || $user['tip_balance'] < $amount) return "Insufficient balance or invalid account.";
+ if (!isValidSalviumAddress($address)) return "Invalid SAL address format.";
+
+ $this->db->updateUserTipBalance($user['id'], $amount, 'subtract');
+ $this->db->logWithdrawal($user['id'], $address, $amount);
+ return "Withdrawal request submitted. Processing soon.";
+ }
+
+
+ private function cmd_tip(array $args, array $ctx): string {
+ if (count($args) < 3) return "Usage: /tip ";
+
+ list(, $targetUsername, $amount) = $args;
+ $amount = (float)$amount;
+ $sender = $this->db->getUserByTelegramId($ctx['user_id']);
+ $recipient = $this->db->getUserByUsername(ltrim($targetUsername, '@'));
+
+ if (!$sender || $sender['tip_balance'] < $amount) return "Insufficient funds or invalid sender.";
+ if (!$recipient) return "Recipient not found. Ask them to run /start first.";
+
+ $this->db->updateUserTipBalance($sender['id'], $amount, 'subtract');
+ $this->db->addTip($sender['id'], $recipient['id'], $amount, $ctx['chat_id']);
+ sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} SAL! Use /balance to check.");
+
+ return "Tipped {$targetUsername} {$amount} SAL successfully!";
+ }
+}
+?>
diff --git a/src/salvium_tipbot_common.php b/src/salvium_tipbot_common.php
new file mode 100644
index 0000000..bfaf0c7
--- /dev/null
+++ b/src/salvium_tipbot_common.php
@@ -0,0 +1,24 @@
+ $chatId, 'text' => $text], $options);
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
+ if (!empty($config['IPV4_ONLY'])) {
+ curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+ }
+ curl_exec($ch);
+ curl_close($ch);
+}
+
+function isValidSalviumAddress(string $address): bool {
+ // Accepts standard and subaddress prefixes for Salvium (e.g. SaLvd, SaLvs)
+ return preg_match('/^SaLv[a-zA-Z0-9]{95}$/', $address) === 1;
+}
+
+?>
diff --git a/src/salvium_tipbot_db.php b/src/salvium_tipbot_db.php
old mode 100644
new mode 100755
index 9af18ca..5e6f05b
--- a/src/salvium_tipbot_db.php
+++ b/src/salvium_tipbot_db.php
@@ -63,6 +63,17 @@ class SalviumTipBotDB {
FOREIGN KEY (sender_user_id) REFERENCES users(id),
FOREIGN KEY (recipient_user_id) REFERENCES users(id)
);
+
+ CREATE TABLE bot_log (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ chat_id BIGINT NOT NULL,
+ chat_name VARCHAR(255),
+ username VARCHAR(255),
+ message TEXT,
+ response TEXT,
+ logged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ );
+
*/
public function getUserByTelegramId(int $telegramUserId): array|false {
@@ -71,6 +82,17 @@ class SalviumTipBotDB {
return $stmt->fetch(PDO::FETCH_ASSOC);
}
+ public function getUserByUsername(string $username): array|false {
+ $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?");
+ $stmt->execute([$username]);
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+
+ public function updateUsername(int $telegramUserId, string $username): void {
+ $stmt = $this->pdo->prepare("UPDATE users SET username = ? WHERE telegram_user_id = ?");
+ $stmt->execute([$username, $telegramUserId]);
+ }
+
public function getUserBySubaddress(string $subaddress): array|false {
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE salvium_subaddress = ?");
$stmt->execute([$subaddress]);
@@ -127,6 +149,11 @@ class SalviumTipBotDB {
return $stmt->execute([$senderUserId, $recipientUserId, $amount, $channelId]);
}
+ public function getAllPendingTips(): array {
+ $stmt = $this->pdo->query("SELECT * FROM tips WHERE status = 'pending'");
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+ }
+
public function getPendingTipsForUser(int $recipientUserId): array {
$stmt = $this->pdo->prepare("SELECT * FROM tips WHERE recipient_user_id = ? AND status = 'pending'");
$stmt->execute([$recipientUserId]);
@@ -143,5 +170,22 @@ class SalviumTipBotDB {
$stmt = $this->pdo->query("SELECT * FROM withdrawals WHERE status = 'pending'");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
+
+ public function logMessage(int $chatId, string $chatName, string $username, string $message, string $response): void {
+ try {
+ $sql = "INSERT INTO bot_log (chat_id, chat_name, username, message, response) VALUES (:chat_id, :chat_name, :username, :message, :response)";
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute([
+ ':chat_id' => $chatId,
+ ':chat_name' => $chatName,
+ ':username' => $username,
+ ':message' => $message,
+ ':response' => $response,
+ ]);
+ } catch (PDOException $e) {
+ error_log("Error logging message: " . $e->getMessage());
+ }
+ }
+
}
?>
diff --git a/src/salvium_tipbot_wallet.php b/src/salvium_tipbot_wallet.php
old mode 100644
new mode 100755
index 0713b7b..2b34bbb
--- a/src/salvium_tipbot_wallet.php
+++ b/src/salvium_tipbot_wallet.php
@@ -68,14 +68,25 @@ class SalviumWallet {
return $result['addresses'] ?? false;
}
- public function transfer(array $destinations, int $mixin = 11, int $unlockTime = 0, bool $getTxKey = false, bool $doNotRelay = false): array|false {
+ public function transfer(array $destinations, int $accountIndex = 0, array $subaddrIndices = [0], int $priority = 0, int $ringSize = 16, bool $getTxKey = true): array|false {
$params = [
- 'destinations' => $destinations,
- 'mixin' => $mixin,
- 'unlock_time' => $unlockTime,
- 'get_tx_key' => $getTxKey,
- 'do_not_relay' => $doNotRelay
+ 'destinations' => array_map(function ($dest) {
+ return [
+ 'address' => $dest['address'],
+ 'amount' => (int)($dest['amount']), // already in atomic units
+ 'asset_type' => 'SAL1'
+ ];
+ }, $destinations),
+ 'source_asset' => 'SAL1',
+ 'dest_asset' => 'SAL1',
+ 'tx_type' => 3,
+ 'account_index' => $accountIndex,
+ // 'subaddr_indices' => $subaddrIndices,
+ 'priority' => $priority,
+ 'ring_size' => $ringSize,
+ 'get_tx_key' => $getTxKey
];
+
return $this->_callRpc('transfer', $params);
}