ваше сообщение коммита
This commit is contained in:
@@ -35,8 +35,8 @@ async function upsertProviderSettings({ provider, api_key, base_url, selected_mo
|
||||
const existing = await encryptedDb.getData(TABLE, { provider: provider }, 1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Обновляем существующую запись
|
||||
return await encryptedDb.saveData(TABLE, data, { provider: provider });
|
||||
// Обновляем существующую запись по ID
|
||||
return await encryptedDb.saveData(TABLE, data, { id: existing[0].id });
|
||||
} else {
|
||||
// Создаем новую запись
|
||||
return await encryptedDb.saveData(TABLE, data);
|
||||
@@ -120,29 +120,51 @@ async function getAllLLMModels() {
|
||||
|
||||
for (const provider of providers) {
|
||||
if (provider.selected_model) {
|
||||
allModels.push({
|
||||
id: provider.selected_model,
|
||||
provider: provider.provider
|
||||
});
|
||||
// Фильтруем embedding модели - они не должны быть в списке LLM
|
||||
const modelName = provider.selected_model.toLowerCase();
|
||||
const isEmbeddingModel = modelName.includes('embed') ||
|
||||
modelName.includes('embedding') ||
|
||||
modelName.includes('bge') ||
|
||||
modelName.includes('nomic') ||
|
||||
modelName.includes('text-embedding') ||
|
||||
modelName.includes('mxbai') ||
|
||||
modelName.includes('sentence') ||
|
||||
modelName.includes('ada-002') ||
|
||||
modelName.includes('text-embedding-ada') ||
|
||||
modelName.includes('text-embedding-3');
|
||||
|
||||
if (!isEmbeddingModel) {
|
||||
allModels.push({
|
||||
id: provider.selected_model,
|
||||
provider: provider.provider
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Для Ollama проверяем реально установленные модели
|
||||
// Для Ollama проверяем реально установленные модели через HTTP API
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const execAsync = util.promisify(exec);
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
|
||||
// Проверяем, какие модели установлены в Ollama
|
||||
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
|
||||
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const modelName = parts[0];
|
||||
const models = response.data.models || [];
|
||||
for (const model of models) {
|
||||
// Фильтруем embedding модели из Ollama
|
||||
const modelName = model.name.toLowerCase();
|
||||
const isEmbeddingModel = modelName.includes('embed') ||
|
||||
modelName.includes('embedding') ||
|
||||
modelName.includes('bge') ||
|
||||
modelName.includes('nomic') ||
|
||||
modelName.includes('mxbai') ||
|
||||
modelName.includes('sentence');
|
||||
|
||||
if (!isEmbeddingModel) {
|
||||
allModels.push({
|
||||
id: modelName,
|
||||
id: model.name,
|
||||
provider: 'ollama'
|
||||
});
|
||||
}
|
||||
@@ -189,27 +211,23 @@ async function getAllEmbeddingModels() {
|
||||
}
|
||||
}
|
||||
|
||||
// Для Ollama проверяем реально установленные embedding модели
|
||||
// Для Ollama проверяем реально установленные embedding модели через HTTP API
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const execAsync = util.promisify(exec);
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
|
||||
// Проверяем, какие embedding модели установлены в Ollama
|
||||
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
|
||||
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const modelName = parts[0];
|
||||
// Проверяем, что это embedding модель
|
||||
if (modelName.includes('embed') || modelName.includes('bge') || modelName.includes('nomic')) {
|
||||
allModels.push({
|
||||
id: modelName,
|
||||
provider: 'ollama'
|
||||
});
|
||||
}
|
||||
const models = response.data.models || [];
|
||||
for (const model of models) {
|
||||
// Проверяем, что это embedding модель
|
||||
if (model.name.includes('embed') || model.name.includes('bge') || model.name.includes('nomic')) {
|
||||
allModels.push({
|
||||
id: model.name,
|
||||
provider: 'ollama'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (ollamaError) {
|
||||
|
||||
@@ -466,6 +466,191 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Определяет уровень доступа пользователя на основе количества токенов
|
||||
* @param {string} address - Адрес кошелька
|
||||
* @returns {Promise<{level: string, tokenCount: number, hasAccess: boolean}>}
|
||||
*/
|
||||
async getUserAccessLevel(address) {
|
||||
if (!address) {
|
||||
return { level: 'user', tokenCount: 0, hasAccess: false };
|
||||
}
|
||||
|
||||
logger.info(`Checking access level for address: ${address}`);
|
||||
|
||||
try {
|
||||
// Получаем токены из базы данных напрямую (как в checkAdminRole)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Получаем токены из базы с расшифровкой
|
||||
const tokensResult = await db.getQuery()(
|
||||
'SELECT id, min_balance, readonly_threshold, editor_threshold, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
|
||||
[encryptionKey]
|
||||
);
|
||||
const tokens = tokensResult.rows;
|
||||
|
||||
// Получаем RPC провайдеры
|
||||
const rpcProvidersResult = await db.getQuery()(
|
||||
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
|
||||
[encryptionKey]
|
||||
);
|
||||
const rpcProviders = rpcProvidersResult.rows;
|
||||
|
||||
const rpcMap = {};
|
||||
for (const rpc of rpcProviders) {
|
||||
rpcMap[rpc.network_id] = rpc.rpc_url;
|
||||
}
|
||||
|
||||
// Получаем балансы токенов из блокчейна
|
||||
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
|
||||
const tokenBalances = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
const rpcUrl = rpcMap[token.network];
|
||||
if (!rpcUrl) continue;
|
||||
|
||||
try {
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
|
||||
|
||||
// Получаем баланс с таймаутом
|
||||
const balancePromise = tokenContract.balanceOf(address);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 5000) // Увеличиваем таймаут до 5 секунд
|
||||
);
|
||||
|
||||
const rawBalance = await Promise.race([balancePromise, timeoutPromise]);
|
||||
const balance = ethers.formatUnits(rawBalance, 18);
|
||||
|
||||
tokenBalances.push({
|
||||
network: token.network,
|
||||
tokenAddress: token.address,
|
||||
tokenName: token.name,
|
||||
symbol: '',
|
||||
balance,
|
||||
minBalance: token.min_balance,
|
||||
readonlyThreshold: token.readonly_threshold || 1,
|
||||
editorThreshold: token.editor_threshold || 2,
|
||||
});
|
||||
|
||||
logger.info(`[getUserAccessLevel] Token balance for ${token.name} (${token.address}): ${balance}`);
|
||||
} catch (error) {
|
||||
logger.error(`[getUserAccessLevel] Error getting balance for ${token.name} (${token.address}):`, error.message);
|
||||
// Добавляем токен с нулевым балансом
|
||||
tokenBalances.push({
|
||||
network: token.network,
|
||||
tokenAddress: token.address,
|
||||
tokenName: token.name,
|
||||
symbol: '',
|
||||
balance: '0',
|
||||
minBalance: token.min_balance,
|
||||
readonlyThreshold: token.readonly_threshold || 1,
|
||||
editorThreshold: token.editor_threshold || 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenBalances || !Array.isArray(tokenBalances)) {
|
||||
logger.warn(`No token balances found for address: ${address}`);
|
||||
return { level: 'user', tokenCount: 0, hasAccess: false };
|
||||
}
|
||||
|
||||
// Подсчитываем сумму токенов с достаточным балансом
|
||||
let validTokenCount = 0;
|
||||
const validTokens = [];
|
||||
|
||||
for (const token of tokenBalances) {
|
||||
const balance = parseFloat(token.balance || '0');
|
||||
const minBalance = parseFloat(token.minBalance || '0');
|
||||
|
||||
if (balance >= minBalance) {
|
||||
validTokenCount += balance; // Суммируем баланс токенов, а не количество сетей
|
||||
validTokens.push({
|
||||
name: token.name,
|
||||
network: token.network,
|
||||
balance: balance,
|
||||
minBalance: minBalance
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Token validation for ${address}:`, {
|
||||
totalTokens: tokenBalances.length,
|
||||
validTokens: validTokenCount,
|
||||
validTokenDetails: validTokens
|
||||
});
|
||||
|
||||
// Определяем уровень доступа на основе настроек токенов
|
||||
let accessLevel = 'user';
|
||||
let hasAccess = false;
|
||||
|
||||
// Получаем настройки порогов из токенов (используем самые низкие требования для максимального доступа)
|
||||
let readonlyThreshold = Infinity;
|
||||
let editorThreshold = Infinity;
|
||||
|
||||
if (tokenBalances.length > 0) {
|
||||
// Находим самые низкие пороги среди всех токенов
|
||||
for (const token of tokenBalances) {
|
||||
const tokenReadonlyThreshold = token.readonlyThreshold || 1;
|
||||
const tokenEditorThreshold = token.editorThreshold || 2;
|
||||
|
||||
if (tokenReadonlyThreshold < readonlyThreshold) {
|
||||
readonlyThreshold = tokenReadonlyThreshold;
|
||||
}
|
||||
if (tokenEditorThreshold < editorThreshold) {
|
||||
editorThreshold = tokenEditorThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
// Если не нашли токены с порогами, используем дефолтные значения
|
||||
if (readonlyThreshold === Infinity) readonlyThreshold = 1;
|
||||
if (editorThreshold === Infinity) editorThreshold = 2;
|
||||
|
||||
logger.info(`[AuthService] Определены пороги доступа: readonly=${readonlyThreshold}, editor=${editorThreshold} (из ${tokenBalances.length} токенов)`);
|
||||
} else {
|
||||
readonlyThreshold = 1;
|
||||
editorThreshold = 2;
|
||||
}
|
||||
|
||||
if (validTokenCount >= editorThreshold) {
|
||||
// Достаточно токенов для полных прав редактора
|
||||
accessLevel = 'editor';
|
||||
hasAccess = true;
|
||||
} else if (validTokenCount > 0) {
|
||||
// Есть токены, но недостаточно для редактора - права только на чтение
|
||||
accessLevel = 'readonly';
|
||||
hasAccess = true;
|
||||
} else {
|
||||
// Нет токенов - обычный пользователь
|
||||
accessLevel = 'user';
|
||||
hasAccess = false;
|
||||
}
|
||||
|
||||
logger.info(`Access level determined for ${address}: ${accessLevel} (${validTokenCount} tokens)`);
|
||||
|
||||
return {
|
||||
level: accessLevel,
|
||||
tokenCount: validTokenCount,
|
||||
hasAccess: hasAccess,
|
||||
validTokens: validTokens
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error in getUserAccessLevel: ${error.message}`);
|
||||
return { level: 'user', tokenCount: 0, hasAccess: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем псевдоним функции checkAdminRole для обратной совместимости
|
||||
async checkAdminTokens(address) {
|
||||
if (!address) return false;
|
||||
@@ -473,9 +658,11 @@ class AuthService {
|
||||
logger.info(`Checking admin tokens for address: ${address}`);
|
||||
|
||||
try {
|
||||
const isAdmin = await checkAdminRole(address);
|
||||
// Используем новую функцию для определения уровня доступа
|
||||
const accessLevel = await this.getUserAccessLevel(address);
|
||||
const isAdmin = accessLevel.hasAccess; // Любой доступ выше 'user' считается админским
|
||||
|
||||
// Обновляем роль пользователя в базе данных, если есть админские токены
|
||||
// Обновляем роль пользователя в базе данных
|
||||
if (isAdmin) {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
@@ -503,16 +690,17 @@ class AuthService {
|
||||
|
||||
if (userResult.rows.length > 0) {
|
||||
const userId = userResult.rows[0].id;
|
||||
// Обновляем роль пользователя
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
|
||||
logger.info(`Updated user ${userId} role to admin based on token holdings`);
|
||||
// Обновляем роль пользователя с учетом уровня доступа
|
||||
const role = accessLevel.level;
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, userId]);
|
||||
logger.info(`Updated user ${userId} role to ${role} based on token holdings (${accessLevel.tokenCount} tokens)`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating user role:', error);
|
||||
// Продолжаем выполнение, даже если обновление роли не удалось
|
||||
}
|
||||
} else {
|
||||
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
|
||||
// Если пользователь не имеет доступа, сбрасываем роль на "user"
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
@@ -536,10 +724,10 @@ class AuthService {
|
||||
[address.toLowerCase(), encryptionKey]
|
||||
);
|
||||
|
||||
if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') {
|
||||
if (userResult.rows.length > 0 && userResult.rows[0].role !== 'user') {
|
||||
const userId = userResult.rows[0].id;
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]);
|
||||
logger.info(`Reset user ${userId} role from admin to user (no tokens found)`);
|
||||
logger.info(`Reset user ${userId} role to user (no valid tokens found)`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating user role:', error);
|
||||
@@ -594,20 +782,18 @@ class AuthService {
|
||||
const address = user.address;
|
||||
const currentRole = user.role;
|
||||
|
||||
logger.info(`Rechecking admin status for user ${user.id} with address ${address}`);
|
||||
logger.info(`Rechecking access level for user ${user.id} with address ${address}`);
|
||||
|
||||
// Проверяем баланс токенов
|
||||
const isAdmin = await checkAdminRole(address);
|
||||
|
||||
// Определяем новую роль
|
||||
const newRole = isAdmin ? 'admin' : 'user';
|
||||
// Получаем новый уровень доступа
|
||||
const accessLevel = await this.getUserAccessLevel(address);
|
||||
const newRole = accessLevel.hasAccess ? accessLevel.level : 'user';
|
||||
|
||||
// Обновляем роль только если она изменилась
|
||||
if (currentRole !== newRole) {
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, user.id]);
|
||||
logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address})`);
|
||||
logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address}, tokens: ${accessLevel.tokenCount})`);
|
||||
} else {
|
||||
logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address})`);
|
||||
logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address}, tokens: ${accessLevel.tokenCount})`);
|
||||
}
|
||||
|
||||
} catch (userError) {
|
||||
|
||||
@@ -27,13 +27,25 @@ async function saveAllAuthTokens(authTokens) {
|
||||
name: token.name,
|
||||
address: token.address,
|
||||
network: token.network,
|
||||
min_balance: token.minBalance == null ? 0 : Number(token.minBalance)
|
||||
min_balance: token.minBalance == null ? 0 : Number(token.minBalance),
|
||||
readonly_threshold: token.readonlyThreshold == null ? 1 : Number(token.readonlyThreshold),
|
||||
editor_threshold: token.editorThreshold == null ? 2 : Number(token.editorThreshold)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertAuthToken(token) {
|
||||
console.log('[AuthTokenService] Получены данные токена:', token);
|
||||
console.log('[AuthTokenService] token.readonlyThreshold:', token.readonlyThreshold, 'тип:', typeof token.readonlyThreshold);
|
||||
console.log('[AuthTokenService] token.editorThreshold:', token.editorThreshold, 'тип:', typeof token.editorThreshold);
|
||||
|
||||
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
|
||||
const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold);
|
||||
const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold);
|
||||
|
||||
console.log('[AuthTokenService] Вычисленные значения:');
|
||||
console.log('[AuthTokenService] readonlyThreshold:', readonlyThreshold);
|
||||
console.log('[AuthTokenService] editorThreshold:', editorThreshold);
|
||||
|
||||
// Проверяем, существует ли токен
|
||||
const existingTokens = await encryptedDb.getData('auth_tokens', {
|
||||
@@ -45,7 +57,9 @@ async function upsertAuthToken(token) {
|
||||
// Обновляем существующий токен
|
||||
await encryptedDb.saveData('auth_tokens', {
|
||||
name: token.name,
|
||||
min_balance: minBalance
|
||||
min_balance: minBalance,
|
||||
readonly_threshold: readonlyThreshold,
|
||||
editor_threshold: editorThreshold
|
||||
}, {
|
||||
address: token.address,
|
||||
network: token.network
|
||||
@@ -56,7 +70,9 @@ async function upsertAuthToken(token) {
|
||||
name: token.name,
|
||||
address: token.address,
|
||||
network: token.network,
|
||||
min_balance: minBalance
|
||||
min_balance: minBalance,
|
||||
readonly_threshold: readonlyThreshold,
|
||||
editor_threshold: editorThreshold
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,12 +289,23 @@ class EncryptedDataService {
|
||||
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
|
||||
.join(', ');
|
||||
const whereClause = Object.keys(whereConditions)
|
||||
.map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`)
|
||||
.map((key, index) => {
|
||||
// Для WHERE условий используем зашифрованные имена колонок
|
||||
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
|
||||
if (encryptedColumn) {
|
||||
// Для зашифрованных колонок используем encrypt_text для сравнения
|
||||
return `${quoteReservedWord(`${key}_encrypted`)} = encrypt_text($${paramIndex + index}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
|
||||
} else {
|
||||
// Для незашифрованных колонок используем обычное сравнение
|
||||
return `${quoteReservedWord(key)} = $${paramIndex + index}`;
|
||||
}
|
||||
})
|
||||
.join(' AND ');
|
||||
|
||||
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
||||
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
|
||||
|
||||
|
||||
const { rows } = await db.getQuery()(query, allParams);
|
||||
return rows[0];
|
||||
} else {
|
||||
|
||||
@@ -37,7 +37,7 @@ async function getUserTokenBalances(address) {
|
||||
|
||||
// Получаем токены и RPC с расшифровкой
|
||||
const tokensResult = await db.getQuery()(
|
||||
'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
|
||||
'SELECT id, min_balance, readonly_threshold, editor_threshold, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
|
||||
[encryptionKey]
|
||||
);
|
||||
const tokens = tokensResult.rows;
|
||||
@@ -57,18 +57,42 @@ async function getUserTokenBalances(address) {
|
||||
|
||||
for (const token of tokens) {
|
||||
const rpcUrl = rpcMap[token.network];
|
||||
if (!rpcUrl) continue;
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
if (!rpcUrl) {
|
||||
logger.warn(`[tokenBalanceService] RPC URL не найден для сети ${token.network}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Создаем провайдер с таймаутом
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
|
||||
polling: false,
|
||||
staticNetwork: true
|
||||
});
|
||||
|
||||
// Устанавливаем таймаут для запросов
|
||||
provider._getConnection().timeout = 10000; // 10 секунд
|
||||
|
||||
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
|
||||
let balance = '0';
|
||||
|
||||
try {
|
||||
const rawBalance = await tokenContract.balanceOf(address);
|
||||
logger.info(`[tokenBalanceService] Получение баланса для ${token.name} (${token.address}) в сети ${token.network} для адреса ${address}`);
|
||||
|
||||
// Создаем промис с таймаутом
|
||||
const balancePromise = tokenContract.balanceOf(address);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Request timeout')), 10000)
|
||||
);
|
||||
|
||||
const rawBalance = await Promise.race([balancePromise, timeoutPromise]);
|
||||
balance = ethers.formatUnits(rawBalance, 18);
|
||||
|
||||
if (!balance || isNaN(Number(balance))) balance = '0';
|
||||
|
||||
logger.info(`[tokenBalanceService] Баланс получен для ${token.name}: ${balance}`);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`[tokenBalanceService] Ошибка получения баланса для ${token.name} (${token.address}) в сети ${token.network}:`,
|
||||
e
|
||||
e.message || e
|
||||
);
|
||||
balance = '0';
|
||||
}
|
||||
@@ -79,6 +103,8 @@ async function getUserTokenBalances(address) {
|
||||
symbol: token.symbol || '',
|
||||
balance,
|
||||
minBalance: token.min_balance,
|
||||
readonlyThreshold: token.readonly_threshold || 1,
|
||||
editorThreshold: token.editor_threshold || 2,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user