diff --git a/migrations/20260530_withdrawal_txid_lists.sql b/migrations/20260530_withdrawal_txid_lists.sql new file mode 100644 index 0000000..82a6229 --- /dev/null +++ b/migrations/20260530_withdrawal_txid_lists.sql @@ -0,0 +1,4 @@ +-- Token transfer_split can return multiple transaction hashes for one withdrawal. +-- Store comma-separated txids in the existing operational column. + +ALTER TABLE withdrawals MODIFY txid TEXT NULL; diff --git a/salvium_tipbot_monitor.php b/salvium_tipbot_monitor.php index e0c38ad..ae5c0a4 100755 --- a/salvium_tipbot_monitor.php +++ b/salvium_tipbot_monitor.php @@ -92,18 +92,35 @@ if (!filter_var($config['WITHDRAWALS_ENABLED'] ?? false, FILTER_VALIDATE_BOOL)) continue; } - $result = $wallet->transfer([ + $destinations = [ [ 'address' => $withdrawal['address'], 'amount' => (int)round($amountToSend * 1e8) ] - ], 0, [0], 0, 16, true, $assetType); + ]; - if ($result && isset($result['tx_hash'])) { - $db->updateWithdrawalTxid($withdrawal['id'], $result['tx_hash']); + $result = $assetType === 'SAL1' + ? $wallet->transfer($destinations, 0, [], 0, 16, true, $assetType) + : $wallet->transferSplit($destinations, 0, [], 0, null, true, $assetType); + + $txids = []; + if (is_array($result)) { + if (!empty($result['tx_hash'])) { + $txids[] = $result['tx_hash']; + } + if (!empty($result['tx_hash_list']) && is_array($result['tx_hash_list'])) { + $txids = array_merge($txids, $result['tx_hash_list']); + } + } + $txids = array_values(array_unique(array_filter($txids))); + + if (!empty($txids)) { + $txidText = implode(',', $txids); + $db->updateWithdrawalTxid($withdrawal['id'], $txidText); $db->updateWithdrawalStatus($withdrawal['id'], 'sent'); $feeText = $feeAssetType !== null && $withdrawalFee > 0 ? " Fee: {$withdrawalFee} {$feeAssetType}." : ""; - sendMessage($chatId, "Withdrawal of {$amountToSend} {$assetType} sent.{$feeText} TxID: {$result['tx_hash']}"); + $txLabel = count($txids) === 1 ? "TxID: {$txidText}" : "TxIDs: {$txidText}"; + sendMessage($chatId, "Withdrawal of {$amountToSend} {$assetType} sent.{$feeText} {$txLabel}"); } else { $db->updateWithdrawalStatus($withdrawal['id'], 'failed'); $returned = $refundText(); diff --git a/src/salvium_tipbot_db.php b/src/salvium_tipbot_db.php index 32725b1..cd95df3 100755 --- a/src/salvium_tipbot_db.php +++ b/src/salvium_tipbot_db.php @@ -59,7 +59,7 @@ class SalviumTipBotDB { CREATE TABLE withdrawals ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, - txid VARCHAR(64), + txid TEXT, asset_type VARCHAR(16) NOT NULL DEFAULT 'SAL1', address VARCHAR(128) NOT NULL, amount DECIMAL(20, 12) NOT NULL, @@ -430,24 +430,37 @@ class SalviumTipBotDB { * Accepts an array of TxIDs and returns an associative array [txid => username] */ public function getTxOwners(array $txids): array { + $txids = array_values(array_filter(array_map('strval', $txids), fn($txid) => trim($txid) !== '')); if (empty($txids)) { return []; } - // Create the correct number of placeholders (?) for the IN clause - $placeholders = str_repeat('?,', count($txids) - 1) . '?'; + $conditions = implode(' OR ', array_fill(0, count($txids), "(w.txid = ? OR FIND_IN_SET(?, w.txid) > 0)")); + $params = []; + foreach ($txids as $txid) { + $params[] = $txid; + $params[] = $txid; + } - // Efficient JOIN query to get usernames for these specific TxIDs $sql = "SELECT w.txid, u.username FROM withdrawals w JOIN users u ON w.user_id = u.id - WHERE w.txid IN ($placeholders)"; + WHERE {$conditions}"; $stmt = $this->pdo->prepare($sql); - $stmt->execute($txids); + $stmt->execute($params); - // FETCH_KEY_PAIR returns an array like: ['txid123' => 'user_name', 'txid456' => 'other_user'] - return $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + $owners = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + foreach (explode(',', (string)$row['txid']) as $storedTxid) { + $storedTxid = trim($storedTxid); + if (in_array($storedTxid, $txids, true)) { + $owners[$storedTxid] = $row['username']; + } + } + } + + return $owners; } public function logMessage(int $chatId, string $chatName, string $username, string $message, string $response): void { diff --git a/src/salvium_tipbot_wallet.php b/src/salvium_tipbot_wallet.php index c9215af..9ffcdbe 100755 --- a/src/salvium_tipbot_wallet.php +++ b/src/salvium_tipbot_wallet.php @@ -95,6 +95,13 @@ private function _callRpc(string $method, array $params = []): array|false { curl_close($ch); $decoded = json_decode($response, true); + if (isset($decoded['error'])) { + $message = $decoded['error']['message'] ?? 'unknown error'; + $code = $decoded['error']['code'] ?? 'unknown code'; + error_log("RPC Error {$method}: {$code} {$message}"); + return false; + } + return $decoded['result'] ?? false; } public function getWalletBalance(): array|false { @@ -133,7 +140,7 @@ public function getNewSubaddress(int $accountIndex = 0, ?string $label = null): return $result['addresses'] ?? false; } - public function transfer(array $destinations, int $accountIndex = 0, array $subaddrIndices = [0], int $priority = 0, int $ringSize = 16, bool $getTxKey = true, string $assetType = 'SAL1'): array|false { + private function buildTransferParams(array $destinations, int $accountIndex, array $subaddrIndices, int $priority, ?int $ringSize, bool $getTxKey, string $assetType): array { $params = [ 'destinations' => array_map(function ($dest) use ($assetType) { return [ @@ -146,15 +153,31 @@ public function getNewSubaddress(int $accountIndex = 0, ?string $label = null): 'dest_asset' => $assetType, 'tx_type' => 3, 'account_index' => $accountIndex, - // 'subaddr_indices' => $subaddrIndices, 'priority' => $priority, - 'ring_size' => $ringSize, 'get_tx_key' => $getTxKey ]; + if (!empty($subaddrIndices)) { + $params['subaddr_indices'] = $subaddrIndices; + } + + if ($ringSize !== null) { + $params['ring_size'] = $ringSize; + } + + return $params; + } + + public function transfer(array $destinations, int $accountIndex = 0, array $subaddrIndices = [], int $priority = 0, ?int $ringSize = 16, bool $getTxKey = true, string $assetType = 'SAL1'): array|false { + $params = $this->buildTransferParams($destinations, $accountIndex, $subaddrIndices, $priority, $ringSize, $getTxKey, $assetType); return $this->_callRpc('transfer', $params); } + public function transferSplit(array $destinations, int $accountIndex = 0, array $subaddrIndices = [], int $priority = 0, ?int $ringSize = null, bool $getTxKey = true, string $assetType = 'SAL1'): array|false { + $params = $this->buildTransferParams($destinations, $accountIndex, $subaddrIndices, $priority, $ringSize, $getTxKey, $assetType); + return $this->_callRpc('transfer_split', $params); + } + public function getTransfers(string $inOrOut = 'in', bool $pending = false, bool $failed = false): array|false { $params = [ $inOrOut => true,