Use split transfers for token withdrawals

This commit is contained in:
root
2026-05-30 17:26:11 +00:00
parent 22d5d15743
commit 5834c8cc42
4 changed files with 73 additions and 16 deletions
@@ -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;
+22 -5
View File
@@ -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();
+21 -8
View File
@@ -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 {
+26 -3
View File
@@ -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,