Add asset-aware user balances

This commit is contained in:
root
2026-05-30 16:10:24 +00:00
parent 98a79575db
commit 50e4a3da1f
5 changed files with 239 additions and 74 deletions
+4 -2
View File
@@ -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+
+41
View File
@@ -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);
+17 -9
View File
@@ -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.");
}
?>
+66 -16
View File
@@ -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 havent 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
View File
@@ -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 {