Add asset-aware user balances
This commit is contained in:
@@ -7,6 +7,7 @@ A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency.
|
||||
- Wallet interaction via JSON-RPC
|
||||
- Secure MySQL backend using PDO
|
||||
- Deposit, withdraw, balance, and tipping commands
|
||||
- Per-user balances for SAL1 and token assets such as `salCULT`
|
||||
- Cron-compatible monitoring of deposits and withdrawals
|
||||
|
||||
## Installation
|
||||
@@ -14,8 +15,9 @@ A PHP-based Telegram tip bot for the Salvium (Monero fork) cryptocurrency.
|
||||
2. Copy `config.sample.php` to `config.php` and fill in your credentials
|
||||
3. Update `config.php` with your RPC, DB, and Telegram Bot credentials
|
||||
4. Set `WITHDRAWALS_ENABLED` to `true` only when wallet RPC withdrawals should be active
|
||||
5. Run `salvium_tipbot.php` as a Telegram webhook listener
|
||||
6. Schedule `salvium_tipbot_monitor.php` using `cron` for periodic checks
|
||||
5. Create the `user_balances` table and add `asset_type` columns to `deposits`, `tips`, and `withdrawals`
|
||||
6. Run `salvium_tipbot.php` as a Telegram webhook listener
|
||||
7. Schedule `salvium_tipbot_monitor.php` using `cron` for periodic checks
|
||||
|
||||
## Requirements
|
||||
- PHP 8.1+
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Asset-aware balances migration.
|
||||
-- Back up the database before running this on an existing installation.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_balances (
|
||||
user_id INT NOT NULL,
|
||||
asset_type VARCHAR(16) NOT NULL,
|
||||
balance DECIMAL(32, 12) NOT NULL DEFAULT 0.000000000000,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, asset_type),
|
||||
CONSTRAINT user_balances_user_id_fk FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
ALTER TABLE deposits ADD COLUMN asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1';
|
||||
ALTER TABLE tips ADD COLUMN asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1';
|
||||
ALTER TABLE withdrawals ADD COLUMN asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1';
|
||||
|
||||
-- Existing production data was classified as SAL1, except known token deposits.
|
||||
-- For this deployment deposits.id 41 and 42 were salCULT.
|
||||
UPDATE deposits SET asset_type = 'SAL1';
|
||||
UPDATE deposits SET asset_type = 'salCULT' WHERE id IN (41, 42);
|
||||
UPDATE tips SET asset_type = 'SAL1';
|
||||
UPDATE withdrawals SET asset_type = 'SAL1';
|
||||
|
||||
-- Replace the old txid-only uniqueness with per-user, per-asset uniqueness.
|
||||
ALTER TABLE deposits DROP INDEX txid;
|
||||
ALTER TABLE deposits ADD UNIQUE KEY uniq_deposit_tx_user_asset (txid, user_id, asset_type);
|
||||
|
||||
-- Seed SAL1 balances from the legacy users.tip_balance column after any manual corrections.
|
||||
INSERT INTO user_balances (user_id, asset_type, balance)
|
||||
SELECT id, 'SAL1', COALESCE(tip_balance, 0)
|
||||
FROM users
|
||||
WHERE COALESCE(tip_balance, 0) <> 0;
|
||||
|
||||
-- Seed token balances from non-SAL1 deposits.
|
||||
INSERT INTO user_balances (user_id, asset_type, balance)
|
||||
SELECT user_id, asset_type, SUM(amount)
|
||||
FROM deposits
|
||||
WHERE asset_type <> 'SAL1'
|
||||
GROUP BY user_id, asset_type
|
||||
ON DUPLICATE KEY UPDATE balance = VALUES(balance);
|
||||
@@ -20,17 +20,24 @@ $wallet = new SalviumWallet(
|
||||
$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');
|
||||
try {
|
||||
$assetType = $db->normalizeAssetType($tx['asset_type'] ?? 'SAL1');
|
||||
} catch (Throwable $e) {
|
||||
error_log("Unsupported asset type in {$tx['txid']}: " . ($tx['asset_type'] ?? 'missing'));
|
||||
continue;
|
||||
}
|
||||
|
||||
sendMessage($user['telegram_user_id'], "Deposit received: {$amount} SAL added to your balance.");
|
||||
if ($db->isDepositLogged($tx['txid'], $user['id'], $assetType)) continue;
|
||||
|
||||
$amount = $tx['amount'] / 1e8; // Convert atomic to major units (1 SAL = 1e8 atomic)
|
||||
$db->logDeposit($user['id'], $tx['txid'], $amount, $tx['height'], $assetType);
|
||||
$db->updateUserBalance($user['id'], $amount, 'add', $assetType);
|
||||
|
||||
sendMessage($user['telegram_user_id'], "Deposit received: {$amount} {$assetType} added to your balance.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +69,7 @@ if (!filter_var($config['WITHDRAWALS_ENABLED'] ?? false, FILTER_VALIDATE_BOOL))
|
||||
} else {
|
||||
// Refund full amount including fee
|
||||
$db->updateWithdrawalStatus($withdrawal['id'], 'failed');
|
||||
$db->updateUserTipBalance($withdrawal['user_id'], $withdrawal['amount'], 'add');
|
||||
$db->updateUserBalance($withdrawal['user_id'], $withdrawal['amount'], 'add', $withdrawal['asset_type'] ?? 'SAL1');
|
||||
sendMessage($withdrawal['user_id'], "Withdrawal failed. {$withdrawal['amount']} SAL returned to your balance. Please try again later.");
|
||||
}
|
||||
}
|
||||
@@ -77,12 +84,13 @@ foreach ($tips as $tip) {
|
||||
if ($tip['amount'] < $config['MIN_TIP_AMOUNT']) continue; // skip small tips
|
||||
|
||||
$recipientId = $tip['recipient_user_id'];
|
||||
$assetType = $tip['asset_type'] ?? 'SAL1';
|
||||
if (!isset($users[$recipientId])) {
|
||||
$users[$recipientId] = $db->getUserByTelegramId($recipientId);
|
||||
}
|
||||
$db->updateUserTipBalance($recipientId, $tip['amount'], 'add');
|
||||
$db->updateUserBalance($recipientId, $tip['amount'], 'add', $assetType);
|
||||
$db->markTipsAsCredited([$tip['id']]);
|
||||
sendMessage($recipientId, "You received a credited tip of {$tip['amount']} SAL. Use /balance to check.");
|
||||
sendMessage($recipientId, "You received a credited tip of {$tip['amount']} {$assetType}. Use /balance to check.");
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -61,11 +61,11 @@ class SalviumTipBotCommands {
|
||||
return "👋 Welcome to the Salvium Tip Bot!\n\n"
|
||||
. "You can use the following commands:\n"
|
||||
. "/deposit – View your deposit address\n"
|
||||
. "/balance – Check your current balance\n"
|
||||
. "/balance [asset] – Check your current balance\n"
|
||||
. ($this->withdrawalsEnabled()
|
||||
? "/withdraw <address> <amount> – Withdraw your SAL\n"
|
||||
: "Withdrawals are temporarily disabled.\n")
|
||||
. "/tip <username> <amount> – Tip another user\n"
|
||||
. "/tip [asset] <username> <amount> – Tip another user\n"
|
||||
. "/claim – Claim tips sent to your username (if you haven’t used the bot before)\n\n"
|
||||
. "ℹ️ All commands except /tip must be used in a private chat with the bot.";
|
||||
}
|
||||
@@ -83,7 +83,32 @@ class SalviumTipBotCommands {
|
||||
|
||||
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.";
|
||||
if (!$user) {
|
||||
return "No account found. Use /deposit first.";
|
||||
}
|
||||
|
||||
if (isset($args[1])) {
|
||||
try {
|
||||
$assetType = $this->parseAssetType($args[1]);
|
||||
} catch (Throwable $e) {
|
||||
return "Invalid asset.";
|
||||
}
|
||||
|
||||
$balance = $this->db->getUserBalance($user['id'], $assetType);
|
||||
return "Your {$assetType} balance: {$balance}";
|
||||
}
|
||||
|
||||
$balances = $this->db->getUserBalances($user['id']);
|
||||
if (empty($balances)) {
|
||||
return "Your balances: 0 SAL1";
|
||||
}
|
||||
|
||||
$output = "Your balances:\n";
|
||||
foreach ($balances as $balance) {
|
||||
$output .= "{$balance['asset_type']}: {$balance['balance']}\n";
|
||||
}
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +219,7 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
|
||||
foreach ($recentTxs as $tx) {
|
||||
$amount = $this->atomicToHuman($tx['amount']);
|
||||
$assetType = $tx['asset_type'] ?? 'SAL1';
|
||||
$hashShort = $tx['txid']; // Full hash as requested
|
||||
$txHeight = $tx['height'];
|
||||
|
||||
@@ -216,7 +242,7 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
|
||||
// Output format: User string attached to TxID line
|
||||
$output .= "TxID: $hashShort$userString\n";
|
||||
$output .= "Amt: $amount SAL\n";
|
||||
$output .= "Amt: $amount $assetType\n";
|
||||
$output .= "Blk: $txHeight ($confText)\n\n";
|
||||
}
|
||||
|
||||
@@ -251,17 +277,28 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
|
||||
$user = $this->db->getUserByTelegramId($ctx['user_id']);
|
||||
|
||||
if (!$user || $user['tip_balance'] < $amount) return "Insufficient balance or invalid account.";
|
||||
if (!$user || (float)$this->db->getUserBalance($user['id'], 'SAL1') < $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);
|
||||
$this->db->updateUserBalance($user['id'], $amount, 'subtract', 'SAL1');
|
||||
$this->db->logWithdrawal($user['id'], $address, $amount, 'SAL1');
|
||||
return "Withdrawal request submitted. Processing soon.";
|
||||
}
|
||||
|
||||
private function cmd_tip(array $args, array $ctx): string {
|
||||
if (count($args) < 3) {
|
||||
return "Usage: /tip <user1> [user2 ...] <amount>";
|
||||
return "Usage: /tip [asset] <user1> [user2 ...] <amount>";
|
||||
}
|
||||
|
||||
$assetType = 'SAL1';
|
||||
$recipientStart = 1;
|
||||
if (count($args) >= 4 && $this->looksLikeAssetType($args[1])) {
|
||||
try {
|
||||
$assetType = $this->parseAssetType($args[1]);
|
||||
} catch (Throwable $e) {
|
||||
return "Invalid asset.";
|
||||
}
|
||||
$recipientStart = 2;
|
||||
}
|
||||
|
||||
$rawAmount = $args[count($args) - 1];
|
||||
@@ -272,11 +309,11 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
}
|
||||
|
||||
if ($amount < $this->config['MIN_TIP_AMOUNT']) {
|
||||
return "Each tip must be at least {$this->config['MIN_TIP_AMOUNT']} SAL.";
|
||||
return "Each tip must be at least {$this->config['MIN_TIP_AMOUNT']} {$assetType}.";
|
||||
}
|
||||
|
||||
|
||||
$usernames = array_filter(array_slice($args, 1, -1), function($u) {
|
||||
$usernames = array_filter(array_slice($args, $recipientStart, -1), function($u) {
|
||||
$u = trim($u);
|
||||
return $u !== '' && $u !== '@';
|
||||
});
|
||||
@@ -290,8 +327,8 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
$sender = $this->db->getUserByTelegramId($ctx['user_id']);
|
||||
$total = $amount * count($usernames);
|
||||
|
||||
if (!$sender || $sender['tip_balance'] < $total) {
|
||||
return "Insufficient funds. You need at least {$total} SAL to tip these users.";
|
||||
if (!$sender || (float)$this->db->getUserBalance($sender['id'], $assetType) < $total) {
|
||||
return "Insufficient funds. You need at least {$total} {$assetType} to tip these users.";
|
||||
}
|
||||
|
||||
$successful = [];
|
||||
@@ -313,12 +350,12 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
|
||||
if (!$recipient) continue;
|
||||
|
||||
$this->db->updateUserTipBalance($sender['id'], $amount, 'subtract');
|
||||
$this->db->addTip($sender['id'], $recipient['id'], $amount, $ctx['chat_id']);
|
||||
$this->db->updateUserBalance($sender['id'], $amount, 'subtract', $assetType);
|
||||
$this->db->addTip($sender['id'], $recipient['id'], $amount, $ctx['chat_id'], $assetType);
|
||||
$successful[] = $cleanUsername;
|
||||
|
||||
if (!empty($recipient['telegram_user_id']) && $recipient['telegram_user_id'] > 0 && $recipient['telegram_user_id'] !== (100_000 + (crc32($cleanUsername) % 900_000))) {
|
||||
sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} SAL! Use /balance to check.");
|
||||
sendMessage($recipient['telegram_user_id'], "You received a tip of {$amount} {$assetType}! Use /balance to check.");
|
||||
} else {
|
||||
sendMessage($ctx['chat_id'], "Hey @$cleanUsername, you just got a tip from @$ctx[username]!");
|
||||
sendGif(
|
||||
@@ -337,7 +374,7 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
return "Tip failed — no valid recipients.";
|
||||
}
|
||||
|
||||
return "Tipped " . implode(', ', $successful) . " {$amount} SAL each!";
|
||||
return "Tipped " . implode(', ', $successful) . " {$amount} {$assetType} each!";
|
||||
}
|
||||
|
||||
private function cmd_claim(array $args, array $ctx): string {
|
||||
@@ -372,5 +409,18 @@ private function cmd_txs(array $args, array $ctx): string {
|
||||
return "Nothing to claim or already claimed.";
|
||||
}
|
||||
|
||||
private function looksLikeAssetType(string $value): bool {
|
||||
$value = trim($value);
|
||||
if (in_array(strtoupper($value), ['SAL', 'SAL1'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return preg_match('/^(sal[a-zA-Z0-9]{4}|[a-zA-Z0-9]{4})$/', $value) === 1;
|
||||
}
|
||||
|
||||
private function parseAssetType(string $value): string {
|
||||
return $this->db->normalizeAssetType($value);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
||||
+111
-47
@@ -5,8 +5,11 @@ namespace Salvium;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class SalviumTipBotDB {
|
||||
private const DEFAULT_ASSET_TYPE = 'SAL1';
|
||||
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(array $config) {
|
||||
@@ -27,18 +30,29 @@ class SalviumTipBotDB {
|
||||
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 user_balances (
|
||||
user_id INT NOT NULL,
|
||||
asset_type VARCHAR(16) NOT NULL,
|
||||
balance DECIMAL(32, 12) NOT NULL DEFAULT 0.000000000000,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, asset_type),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE deposits (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
txid VARCHAR(64) UNIQUE NOT NULL,
|
||||
txid VARCHAR(64) NOT NULL,
|
||||
asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1',
|
||||
amount DECIMAL(20, 12) NOT NULL,
|
||||
block_height INT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_deposit_tx_user_asset (txid, user_id, asset_type),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
@@ -46,6 +60,7 @@ class SalviumTipBotDB {
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
txid VARCHAR(64),
|
||||
asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1',
|
||||
address VARCHAR(128) NOT NULL,
|
||||
amount DECIMAL(20, 12) NOT NULL,
|
||||
status ENUM('pending', 'sent', 'failed') DEFAULT 'pending',
|
||||
@@ -57,6 +72,7 @@ class SalviumTipBotDB {
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
sender_user_id INT NOT NULL,
|
||||
recipient_user_id INT NOT NULL,
|
||||
asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1',
|
||||
amount DECIMAL(20, 12) NOT NULL,
|
||||
channel_id BIGINT,
|
||||
status ENUM('pending', 'credited') DEFAULT 'pending',
|
||||
@@ -77,6 +93,23 @@ class SalviumTipBotDB {
|
||||
|
||||
*/
|
||||
|
||||
public function normalizeAssetType(string $assetType = self::DEFAULT_ASSET_TYPE): string {
|
||||
$assetType = trim($assetType);
|
||||
if ($assetType === '' || strtoupper($assetType) === 'SAL' || strtoupper($assetType) === self::DEFAULT_ASSET_TYPE) {
|
||||
return self::DEFAULT_ASSET_TYPE;
|
||||
}
|
||||
|
||||
if (preg_match('/^sal([a-zA-Z0-9]{4})$/', $assetType, $matches) === 1) {
|
||||
return 'sal' . strtoupper($matches[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-zA-Z0-9]{4}$/', $assetType) === 1) {
|
||||
return 'sal' . strtoupper($assetType);
|
||||
}
|
||||
|
||||
throw new RuntimeException("Invalid asset type: {$assetType}");
|
||||
}
|
||||
|
||||
public function getUserByTelegramId(int $telegramUserId): array|false {
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE telegram_user_id = ?");
|
||||
$stmt->execute([$telegramUserId]);
|
||||
@@ -204,59 +237,81 @@ class SalviumTipBotDB {
|
||||
$this->pdo->prepare("UPDATE withdrawals SET user_id = ? WHERE user_id = ?")
|
||||
->execute([$intoId, $fromId]);
|
||||
|
||||
// 4. Recalculate balance
|
||||
// Total deposits
|
||||
$stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM deposits WHERE user_id = ?");
|
||||
$stmt->execute([$intoId]);
|
||||
$totalDeposits = (float) $stmt->fetchColumn();
|
||||
// 4. Merge per-asset balances
|
||||
$this->pdo->prepare("
|
||||
INSERT INTO user_balances (user_id, asset_type, balance)
|
||||
SELECT ?, asset_type, balance
|
||||
FROM user_balances
|
||||
WHERE user_id = ?
|
||||
ON DUPLICATE KEY UPDATE balance = user_balances.balance + VALUES(balance)
|
||||
")->execute([$intoId, $fromId]);
|
||||
$this->pdo->prepare("DELETE FROM user_balances WHERE user_id = ?")
|
||||
->execute([$fromId]);
|
||||
$this->syncLegacyTipBalance($intoId);
|
||||
|
||||
// Total tips received
|
||||
$stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM tips WHERE recipient_user_id = ?");
|
||||
$stmt->execute([$intoId]);
|
||||
$totalReceivedTips = (float) $stmt->fetchColumn();
|
||||
|
||||
// Total tips sent
|
||||
$stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM tips WHERE sender_user_id = ?");
|
||||
$stmt->execute([$intoId]);
|
||||
$totalSentTips = (float) $stmt->fetchColumn();
|
||||
|
||||
// Total successful withdrawals
|
||||
$stmt = $this->pdo->prepare("SELECT COALESCE(SUM(amount), 0) FROM withdrawals WHERE user_id = ? AND status = 'sent'");
|
||||
$stmt->execute([$intoId]);
|
||||
$totalWithdrawals = (float) $stmt->fetchColumn();
|
||||
|
||||
// Final balance
|
||||
$finalBalance = $totalDeposits + $totalReceivedTips - $totalSentTips - $totalWithdrawals;
|
||||
|
||||
// 5. Update user record with recalculated balance
|
||||
$this->pdo->prepare("UPDATE users SET tip_balance = ? WHERE id = ?")
|
||||
->execute([$finalBalance, $intoId]);
|
||||
|
||||
// 6. Delete the placeholder user
|
||||
// 5. Delete the placeholder user
|
||||
$this->pdo->prepare("DELETE FROM users WHERE id = ?")
|
||||
->execute([$fromId]);
|
||||
|
||||
$this->pdo->commit();
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
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 getUserBalance(int $userId, string $assetType = self::DEFAULT_ASSET_TYPE): string {
|
||||
$assetType = $this->normalizeAssetType($assetType);
|
||||
$stmt = $this->pdo->prepare("SELECT balance FROM user_balances WHERE user_id = ? AND asset_type = ?");
|
||||
$stmt->execute([$userId, $assetType]);
|
||||
$balance = $stmt->fetchColumn();
|
||||
return $balance === false ? '0.000000000000' : (string)$balance;
|
||||
}
|
||||
|
||||
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 getUserBalances(int $userId): array {
|
||||
$stmt = $this->pdo->prepare("SELECT asset_type, balance FROM user_balances WHERE user_id = ? AND balance <> 0 ORDER BY asset_type ASC");
|
||||
$stmt->execute([$userId]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
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 updateUserBalance(int $userId, float $amount, string $operation = 'add', string $assetType = self::DEFAULT_ASSET_TYPE): bool {
|
||||
$assetType = $this->normalizeAssetType($assetType);
|
||||
$signedAmount = $operation === 'subtract' ? -$amount : $amount;
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO user_balances (user_id, asset_type, balance)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE balance = balance + VALUES(balance)
|
||||
");
|
||||
$ok = $stmt->execute([$userId, $assetType, $signedAmount]);
|
||||
|
||||
if ($ok && $assetType === self::DEFAULT_ASSET_TYPE) {
|
||||
$this->syncLegacyTipBalance($userId);
|
||||
}
|
||||
|
||||
return $ok;
|
||||
}
|
||||
|
||||
public function updateUserTipBalance(int $userId, float $amount, string $operation = 'add', string $assetType = self::DEFAULT_ASSET_TYPE): bool {
|
||||
return $this->updateUserBalance($userId, $amount, $operation, $assetType);
|
||||
}
|
||||
|
||||
private function syncLegacyTipBalance(int $userId): void {
|
||||
$stmt = $this->pdo->prepare("
|
||||
UPDATE users
|
||||
SET tip_balance = COALESCE(
|
||||
(SELECT balance FROM user_balances WHERE user_id = ? AND asset_type = ?),
|
||||
0
|
||||
)
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$userId, self::DEFAULT_ASSET_TYPE, $userId]);
|
||||
}
|
||||
|
||||
public function logDeposit(int $userId, string $txid, float $amount, int $blockHeight, string $assetType = self::DEFAULT_ASSET_TYPE): bool {
|
||||
$assetType = $this->normalizeAssetType($assetType);
|
||||
$stmt = $this->pdo->prepare("INSERT INTO deposits (user_id, txid, asset_type, amount, block_height) VALUES (?, ?, ?, ?, ?)");
|
||||
return $stmt->execute([$userId, $txid, $assetType, $amount, $blockHeight]);
|
||||
}
|
||||
|
||||
public function isTxidLogged(string $txid): bool {
|
||||
@@ -265,9 +320,17 @@ class SalviumTipBotDB {
|
||||
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])) {
|
||||
public function isDepositLogged(string $txid, int $userId, string $assetType = self::DEFAULT_ASSET_TYPE): bool {
|
||||
$assetType = $this->normalizeAssetType($assetType);
|
||||
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM deposits WHERE txid = ? AND user_id = ? AND asset_type = ?");
|
||||
$stmt->execute([$txid, $userId, $assetType]);
|
||||
return $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
public function logWithdrawal(int $userId, string $address, float $amount, string $assetType = self::DEFAULT_ASSET_TYPE): int|false {
|
||||
$assetType = $this->normalizeAssetType($assetType);
|
||||
$stmt = $this->pdo->prepare("INSERT INTO withdrawals (user_id, asset_type, address, amount) VALUES (?, ?, ?, ?)");
|
||||
if ($stmt->execute([$userId, $assetType, $address, $amount])) {
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
return false;
|
||||
@@ -283,9 +346,10 @@ class SalviumTipBotDB {
|
||||
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 addTip(int $senderUserId, int $recipientUserId, float $amount, ?int $channelId = null, string $assetType = self::DEFAULT_ASSET_TYPE): bool {
|
||||
$assetType = $this->normalizeAssetType($assetType);
|
||||
$stmt = $this->pdo->prepare("INSERT INTO tips (sender_user_id, recipient_user_id, asset_type, amount, channel_id) VALUES (?, ?, ?, ?, ?)");
|
||||
return $stmt->execute([$senderUserId, $recipientUserId, $assetType, $amount, $channelId]);
|
||||
}
|
||||
|
||||
public function getAllPendingTips(): array {
|
||||
|
||||
Reference in New Issue
Block a user