ваше сообщение коммита

This commit is contained in:
2025-05-20 14:32:03 +03:00
parent 60302c9e13
commit 3d3f24d4c9
24 changed files with 5954 additions and 13502 deletions

View File

@@ -0,0 +1,19 @@
---
description:
globs:
alwaysApply: true
---
Description:
Перед написанием или предложением любого кода, связанного с использованием внешних библиотек или фреймворков, **обязательно** обратись к `context7-mcp` для получения актуальной документации. Это поможет избежать устаревших API, вымышленных функций и обеспечит использование best practices.
Задачи, для которых это правило полезно:
- Генерация кода с использованием любых npm-пакетов, Python-библиотек, Go-модулей и т.д.
- Ответы на вопросы о конкретных функциях или методах библиотек.
- Предоставление примеров использования API.
- Обновление существующего кода, использующего внешние зависимости.
Инструменты для использования:
1. `resolve-library-id`: для определения точного ID библиотеки.
2. `get-library-docs`: для получения актуальной документации по найденному ID.
Убедись, что предлагаемый код соответствует последней версии документации, полученной через context7-mcp.

View File

@@ -1,32 +0,0 @@
[
{
"name": "Ethereum Token",
"address": "0xd95a45fc46a7300e6022885afec3d618d7d3f27c",
"network": "eth",
"minBalance": "1.0"
},
{
"name": "BSC Token",
"address": "0x4B294265720B09ca39BFBA18c7E368413c0f68eB",
"network": "bsc",
"minBalance": "10.0"
},
{
"name": "Arbitrum Token",
"address": "0xdce769b847a0a697239777d0b1c7dd33b6012ba0",
"network": "arbitrum",
"minBalance": "0.5"
},
{
"name": "Custom Token",
"address": "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d",
"network": "eth",
"minBalance": "5.0"
},
{
"name": "test2",
"address": "0xef49261169B454f191678D2aFC5E91Ad2e85dfD8",
"minBalance": "1.0",
"network": "sepolia"
}
]

View File

@@ -1,27 +0,0 @@
[
{
"networkId": "bsc",
"rpcUrl": "https://bsc-dataseed1.binance.org",
"chainId": 56
},
{
"networkId": "arbitrum",
"rpcUrl": "https://arb1.arbitrum.io/rpc",
"chainId": 42161
},
{
"networkId": "polygon",
"rpcUrl": "https://polygon-rpc.com",
"chainId": 137
},
{
"networkId": "sepolia",
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"chainId": 11155111
},
{
"networkId": "ethereum",
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
"chainId": 1
}
]

View File

@@ -0,0 +1,23 @@
-- Миграция: создание таблиц для RPC провайдеров и токенов аутентификации
-- Таблица RPC провайдеров
CREATE TABLE IF NOT EXISTS rpc_providers (
id SERIAL PRIMARY KEY,
network_id VARCHAR(64) NOT NULL UNIQUE,
rpc_url TEXT NOT NULL,
chain_id INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Таблица токенов аутентификации
CREATE TABLE IF NOT EXISTS auth_tokens (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address VARCHAR(64) NOT NULL,
network VARCHAR(64) NOT NULL,
min_balance NUMERIC(36, 18) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT auth_tokens_address_network_unique UNIQUE (address, network)
);

View File

@@ -14,7 +14,7 @@ module.exports = {
}, },
networks: { networks: {
sepolia: { sepolia: {
url: process.env.ETHEREUM_NETWORK_URL, url: process.env.RPC_URL_ETH,
accounts: [process.env.PRIVATE_KEY], accounts: [process.env.PRIVATE_KEY],
}, },
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T10:39:33.237Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T10:42:46.419Z"}
{"code":"ENETUNREACH","errno":"ENETUNREACH","level":"error","message":"Error launching Telegram bot: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)","stack":"FetchError: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)\n at ClientRequest.<anonymous> (/app/node_modules/node-fetch/lib/index.js:1501:11)\n at ClientRequest.emit (node:events:524:28)\n at emitErrorEvent (node:_http_client:101:11)\n at TLSSocket.socketErrorListener (node:_http_client:504:5)\n at TLSSocket.emit (node:events:524:28)\n at emitErrorNT (node:internal/streams/destroy:169:8)\n at emitErrorCloseNT (node:internal/streams/destroy:128:3)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","timestamp":"2025-05-14T10:54:53.650Z","type":"system"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T10:55:32.362Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T11:14:55.953Z"}
{"level":"error","message":"Provider for bsc is not available: Network check timeout","timestamp":"2025-05-14T11:14:56.622Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T11:14:56.844Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T11:14:59.934Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T11:15:00.818Z"}
{"client":{"_connected":true,"_connecting":false,"_connectionCallback":null,"_connectionError":false,"_connectionTimeoutMillis":0,"_ended":false,"_ending":true,"_events":{},"_eventsCount":1,"_poolUseCount":16,"_queryable":false,"_types":{"_types":{"arrayParser":{},"builtins":{"ABSTIME":702,"ACLITEM":1033,"BIT":1560,"BOOL":16,"BPCHAR":1042,"BYTEA":17,"CHAR":18,"CID":29,"CIDR":650,"CIRCLE":718,"DATE":1082,"FLOAT4":700,"FLOAT8":701,"GTSVECTOR":3642,"INET":869,"INT2":21,"INT4":23,"INT8":20,"INTERVAL":1186,"JSON":114,"JSONB":3802,"MACADDR":829,"MACADDR8":774,"MONEY":790,"NUMERIC":1700,"OID":26,"PATH":602,"PG_DEPENDENCIES":3402,"PG_LSN":3220,"PG_NDISTINCT":3361,"PG_NODE_TREE":194,"POLYGON":604,"REFCURSOR":1790,"REGCLASS":2205,"REGCONFIG":3734,"REGDICTIONARY":3769,"REGNAMESPACE":4089,"REGOPER":2203,"REGOPERATOR":2204,"REGPROC":24,"REGPROCEDURE":2202,"REGROLE":4096,"REGTYPE":2206,"RELTIME":703,"SMGR":210,"TEXT":25,"TID":27,"TIME":1083,"TIMESTAMP":1114,"TIMESTAMPTZ":1184,"TIMETZ":1266,"TINTERVAL":704,"TSQUERY":3615,"TSVECTOR":3614,"TXID_SNAPSHOT":2970,"UUID":2950,"VARBIT":1562,"VARCHAR":1043,"XID":28,"XML":142}},"binary":{},"text":{}},"activeQuery":null,"binary":false,"connection":{"_connecting":true,"_emitMessage":false,"_ending":true,"_events":{"end":[null,null]},"_eventsCount":23,"_keepAlive":false,"_keepAliveInitialDelayMillis":0,"lastBuffer":false,"parsedStatements":{},"ssl":false,"stream":{"_closeAfterHandlingError":false,"_events":{"end":[null,null]},"_eventsCount":4,"_hadError":false,"_host":"postgres","_parent":null,"_pendingData":null,"_pendingEncoding":"","_readableState":{"awaitDrainWriters":null,"buffer":[],"bufferIndex":0,"highWaterMark":16384,"length":0,"pipes":[]},"_server":null,"_sockname":null,"_writableState":{"bufferedIndex":0,"corked":0,"highWaterMark":16384,"length":0,"pendingcb":0,"writelen":0},"allowHalfOpen":false,"connecting":false,"server":null}},"connectionParameters":{"binary":false,"client_encoding":"","connect_timeout":0,"database":"dapp_db","host":"postgres","idle_in_transaction_session_timeout":false,"isDomainSocket":false,"lock_timeout":false,"port":5432,"query_timeout":false,"ssl":false,"statement_timeout":false,"user":"dapp_user"},"database":"dapp_db","enableChannelBinding":false,"hasExecuted":true,"host":"postgres","port":5432,"processID":2655,"queryQueue":[],"readyForQuery":true,"saslSession":null,"secretKey":-818342883,"ssl":false,"user":"dapp_user"},"code":"57P01","file":"postgres.c","length":116,"level":"error","line":"3286","message":"Uncaught Exception: terminating connection due to administrator command","name":"error","routine":"ProcessInterrupts","severity":"FATAL","stack":"error: terminating connection due to administrator command\n at Parser.parseErrorMessage (/app/node_modules/pg-protocol/dist/parser.js:285:98)\n at Parser.handlePacket (/app/node_modules/pg-protocol/dist/parser.js:122:29)\n at Parser.parse (/app/node_modules/pg-protocol/dist/parser.js:35:38)\n at Socket.<anonymous> (/app/node_modules/pg-protocol/dist/index.js:11:42)\n at Socket.emit (node:events:524:28)\n at addChunk (node:internal/streams/readable:561:12)\n at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)\n at Readable.push (node:internal/streams/readable:392:5)\n at TCP.onStreamRead (node:internal/stream_base_commons:191:23)","timestamp":"2025-05-14T11:23:19.941Z"}
{"code":"ENETUNREACH","errno":"ENETUNREACH","level":"error","message":"Error launching Telegram bot: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)","stack":"FetchError: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)\n at ClientRequest.<anonymous> (/app/node_modules/node-fetch/lib/index.js:1501:11)\n at ClientRequest.emit (node:events:524:28)\n at emitErrorEvent (node:_http_client:101:11)\n at TLSSocket.socketErrorListener (node:_http_client:504:5)\n at TLSSocket.emit (node:events:524:28)\n at emitErrorNT (node:internal/streams/destroy:169:8)\n at emitErrorCloseNT (node:internal/streams/destroy:128:3)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","timestamp":"2025-05-14T11:23:22.536Z","type":"system"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T11:26:06.159Z"}
{"level":"error","message":"Provider for arbitrum is not available: ","timestamp":"2025-05-14T12:01:50.442Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T12:01:52.662Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T12:05:39.795Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:25:55.874Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:28:16.815Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:36:38.463Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:38:04.378Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:39:20.415Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T12:41:30.276Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:42:42.602Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:48:07.952Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T12:48:15.082Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:51:39.423Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T12:51:42.860Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:52:00.295Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:52:14.000Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:52:22.427Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T12:52:26.321Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T12:54:54.794Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:04:05.566Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:04:14.999Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:04:18.050Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-05-14T13:04:21.835Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-05-14T13:04:22.146Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:04:35.983Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:08:05.296Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:10:42.640Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:10:47.299Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:11:16.040Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:11:16.868Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:11:16.641Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:11:18.257Z"}
{"code":"ENETUNREACH","errno":"ENETUNREACH","level":"error","message":"Error launching Telegram bot: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)","stack":"FetchError: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)\n at ClientRequest.<anonymous> (/app/node_modules/node-fetch/lib/index.js:1501:11)\n at ClientRequest.emit (node:events:524:28)\n at emitErrorEvent (node:_http_client:101:11)\n at TLSSocket.socketErrorListener (node:_http_client:504:5)\n at TLSSocket.emit (node:events:524:28)\n at emitErrorNT (node:internal/streams/destroy:169:8)\n at emitErrorCloseNT (node:internal/streams/destroy:128:3)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","timestamp":"2025-05-14T13:15:04.700Z","type":"system"}
{"code":"ENETUNREACH","errno":"ENETUNREACH","level":"error","message":"Error launching Telegram bot: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)","stack":"FetchError: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)\n at ClientRequest.<anonymous> (/app/node_modules/node-fetch/lib/index.js:1501:11)\n at ClientRequest.emit (node:events:524:28)\n at emitErrorEvent (node:_http_client:101:11)\n at TLSSocket.socketErrorListener (node:_http_client:504:5)\n at TLSSocket.emit (node:events:524:28)\n at emitErrorNT (node:internal/streams/destroy:169:8)\n at emitErrorCloseNT (node:internal/streams/destroy:128:3)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","timestamp":"2025-05-14T13:18:24.462Z","type":"system"}
{"level":"error","message":"Provider for arbitrum is not available: ","timestamp":"2025-05-14T13:33:15.669Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:33:17.730Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T13:44:07.394Z"}
{"level":"error","message":"Error: undefined","timestamp":"2025-05-14T13:49:41.048Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:49:50.503Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:50:06.112Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:50:30.955Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:53:46.383Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:57:28.360Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:58:44.702Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:59:17.095Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:59:21.213Z"}
{"level":"error","message":"Ошибка при тестировании RPC: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T13:59:25.871Z"}
{"level":"error","message":"Provider for arbitrum is not available: ","timestamp":"2025-05-14T14:02:28.435Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T14:02:30.581Z"}
{"code":"ENETUNREACH","errno":"ENETUNREACH","level":"error","message":"Error launching Telegram bot: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)","stack":"FetchError: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)\n at ClientRequest.<anonymous> (/app/node_modules/node-fetch/lib/index.js:1501:11)\n at ClientRequest.emit (node:events:524:28)\n at emitErrorEvent (node:_http_client:101:11)\n at TLSSocket.socketErrorListener (node:_http_client:504:5)\n at TLSSocket.emit (node:events:524:28)\n at emitErrorNT (node:internal/streams/destroy:169:8)\n at emitErrorCloseNT (node:internal/streams/destroy:128:3)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","timestamp":"2025-05-14T14:05:23.622Z","type":"system"}
{"level":"error","message":"Ошибка провайдера при тестировании RPC для bsc: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T14:06:27.685Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T14:06:38.528Z"}
{"level":"error","message":"Ошибка провайдера при тестировании RPC для polygon: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T14:08:07.053Z"}
{"level":"error","message":"Ошибка провайдера при тестировании RPC для sepolia: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T14:08:26.356Z"}
{"level":"error","message":"Provider for arbitrum is not available: ","timestamp":"2025-05-14T14:09:39.744Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T14:09:40.551Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T14:09:40.807Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T14:09:43.526Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T14:09:45.376Z"}
{"level":"error","message":"Ошибка провайдера при тестировании RPC для polygon: Cannot read properties of undefined (reading 'JsonRpcProvider')","timestamp":"2025-05-14T14:20:40.664Z"}
{"code":"ENETUNREACH","errno":"ENETUNREACH","level":"error","message":"Error launching Telegram bot: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)","stack":"FetchError: request to https://api.telegram.org/bot7498026249:[REDACTED]/getMe failed, reason: connect ENETUNREACH 2001:67c:4e8:f004::9:443 - Local (:::0)\n at ClientRequest.<anonymous> (/app/node_modules/node-fetch/lib/index.js:1501:11)\n at ClientRequest.emit (node:events:524:28)\n at emitErrorEvent (node:_http_client:101:11)\n at TLSSocket.socketErrorListener (node:_http_client:504:5)\n at TLSSocket.emit (node:events:524:28)\n at emitErrorNT (node:internal/streams/destroy:169:8)\n at emitErrorCloseNT (node:internal/streams/destroy:128:3)\n at process.processTicksAndRejections (node:internal/process/task_queues:82:21)","timestamp":"2025-05-14T14:20:46.082Z","type":"system"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:20:54.332Z"}
{"level":"error","message":"Provider for arbitrum is not available: ","timestamp":"2025-05-14T16:22:11.865Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:22:12.829Z"}
{"level":"error","message":"Provider for bsc is not available: getaddrinfo ENOTFOUND bsc-dataseed.binance.org","timestamp":"2025-05-14T16:29:20.026Z"}
{"level":"error","message":"Provider for arbitrum is not available: getaddrinfo ENOTFOUND arb1.arbitrum.io","timestamp":"2025-05-14T16:29:20.083Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:29:20.792Z"}
{"level":"error","message":"Provider for bsc is not available: getaddrinfo ENOTFOUND bsc-dataseed.binance.org","timestamp":"2025-05-14T16:29:22.427Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:29:22.582Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:29:26.403Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:29:28.187Z"}
{"level":"error","message":"Provider for polygon is not available: server response 404 Not Found (request={ }, response={ }, error=null, info={ \"requestUrl\": \"https://polygon-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52\", \"responseBody\": \"<html>\\r\\n<head><title>404 Not Found</title></head>\\r\\n<body>\\r\\n<center><h1>404 Not Found</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\", \"responseStatus\": \"404 Not Found\" }, code=SERVER_ERROR, version=6.13.7)","timestamp":"2025-05-14T16:48:19.959Z"}

View File

@@ -104,7 +104,7 @@ async function requireAdmin(req, res, next) {
// Проверка через кошелек // Проверка через кошелек
if (req.session.address) { if (req.session.address) {
const isAdmin = await authService.checkAdminToken(req.session.address); const isAdmin = await authService.checkAdminTokens(req.session.address);
if (isAdmin) { if (isAdmin) {
// Обновляем сессию // Обновляем сессию
req.session.isAdmin = true; req.session.isAdmin = true;

View File

@@ -2,52 +2,17 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const { requireAdmin } = require('../middleware/auth'); const { requireAdmin } = require('../middleware/auth');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const fs = require('fs');
const path = require('path');
const { ethers } = require('ethers'); const { ethers } = require('ethers');
const rpcProviderService = require('../services/rpcProviderService');
const authTokenService = require('../services/authTokenService');
// Логируем версию ethers для отладки // Логируем версию ethers для отладки
logger.info(`Ethers version: ${ethers.version || 'unknown'}`); logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
// Путь к файлу с настройками
const RPC_CONFIG_PATH = path.join(__dirname, '../config/rpc-settings.json');
const AUTH_TOKENS_PATH = path.join(__dirname, '../config/auth-tokens.json');
// Вспомогательная функция для чтения настроек из файла
const readSettingsFile = (filePath, defaultValue = []) => {
try {
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
}
return defaultValue;
} catch (error) {
logger.error(`Ошибка при чтении файла настроек ${filePath}:`, error);
return defaultValue;
}
};
// Вспомогательная функция для записи настроек в файл
const writeSettingsFile = async (filePath, data) => {
try {
// Создаем директорию, если не существует
const dirname = path.dirname(filePath);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (error) {
logger.error(`Ошибка при записи файла настроек ${filePath}:`, error);
return false;
}
};
// Получение RPC настроек // Получение RPC настроек
router.get('/rpc', requireAdmin, async (req, res) => { router.get('/rpc', requireAdmin, async (req, res) => {
try { try {
const rpcConfigs = readSettingsFile(RPC_CONFIG_PATH); const rpcConfigs = await rpcProviderService.getAllRpcProviders();
res.json({ success: true, data: rpcConfigs }); res.json({ success: true, data: rpcConfigs });
} catch (error) { } catch (error) {
logger.error('Ошибка при получении RPC настроек:', error); logger.error('Ошибка при получении RPC настроек:', error);
@@ -55,32 +20,47 @@ router.get('/rpc', requireAdmin, async (req, res) => {
} }
}); });
// Сохранение RPC настроек // Добавление/обновление одного или нескольких RPC
router.post('/rpc', requireAdmin, async (req, res) => { router.post('/rpc', requireAdmin, async (req, res) => {
try { try {
const { rpcConfigs } = req.body; // Если пришёл массив rpcConfigs — bulk-режим
if (Array.isArray(req.body.rpcConfigs)) {
if (!Array.isArray(rpcConfigs)) { const rpcConfigs = req.body.rpcConfigs;
return res.status(400).json({ success: false, error: 'Неверный формат данных' }); if (!rpcConfigs.length) {
return res.status(400).json({ success: false, error: 'rpcConfigs не может быть пустым массивом' });
}
await rpcProviderService.saveAllRpcProviders(rpcConfigs);
return res.json({ success: true, message: 'RPC провайдеры успешно сохранены (bulk)' });
} }
// Иначе — одиночный режим (старый)
const success = await writeSettingsFile(RPC_CONFIG_PATH, rpcConfigs); const { networkId, rpcUrl, chainId } = req.body;
if (!networkId || !rpcUrl) {
if (success) { return res.status(400).json({ success: false, error: 'networkId и rpcUrl обязательны' });
res.json({ success: true, message: 'RPC настройки успешно сохранены' });
} else {
res.status(500).json({ success: false, error: 'Ошибка при сохранении RPC настроек' });
} }
await rpcProviderService.upsertRpcProvider({ networkId, rpcUrl, chainId });
res.json({ success: true, message: 'RPC провайдер сохранён' });
} catch (error) { } catch (error) {
logger.error('Ошибка при сохранении RPC настроек:', error); logger.error('Ошибка при сохранении RPC:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении настроек RPC' }); res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении RPC' });
}
});
// Удаление одного RPC
router.delete('/rpc/:networkId', requireAdmin, async (req, res) => {
try {
const { networkId } = req.params;
await rpcProviderService.deleteRpcProvider(networkId);
res.json({ success: true, message: 'RPC провайдер удалён' });
} catch (error) {
logger.error('Ошибка при удалении RPC:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении RPC' });
} }
}); });
// Получение токенов для аутентификации // Получение токенов для аутентификации
router.get('/auth-tokens', requireAdmin, async (req, res) => { router.get('/auth-tokens', requireAdmin, async (req, res) => {
try { try {
const authTokens = readSettingsFile(AUTH_TOKENS_PATH); const authTokens = await authTokenService.getAllAuthTokens();
res.json({ success: true, data: authTokens }); res.json({ success: true, data: authTokens });
} catch (error) { } catch (error) {
logger.error('Ошибка при получении токенов аутентификации:', error); logger.error('Ошибка при получении токенов аутентификации:', error);
@@ -92,24 +72,44 @@ router.get('/auth-tokens', requireAdmin, async (req, res) => {
router.post('/auth-tokens', requireAdmin, async (req, res) => { router.post('/auth-tokens', requireAdmin, async (req, res) => {
try { try {
const { authTokens } = req.body; const { authTokens } = req.body;
if (!Array.isArray(authTokens)) { if (!Array.isArray(authTokens)) {
return res.status(400).json({ success: false, error: 'Неверный формат данных' }); return res.status(400).json({ success: false, error: 'Неверный формат данных' });
} }
await authTokenService.saveAllAuthTokens(authTokens);
const success = await writeSettingsFile(AUTH_TOKENS_PATH, authTokens); res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
if (success) {
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
} else {
res.status(500).json({ success: false, error: 'Ошибка при сохранении токенов аутентификации' });
}
} catch (error) { } catch (error) {
logger.error('Ошибка при сохранении токенов аутентификации:', error); logger.error('Ошибка при сохранении токенов аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' }); res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' });
} }
}); });
// Добавление/обновление одного токена
router.post('/auth-token', requireAdmin, async (req, res) => {
try {
const { name, address, network, minBalance } = req.body;
if (!name || !address || !network) {
return res.status(400).json({ success: false, error: 'name, address и network обязательны' });
}
await authTokenService.upsertAuthToken({ name, address, network, minBalance });
res.json({ success: true, message: 'Токен аутентификации сохранён' });
} catch (error) {
logger.error('Ошибка при сохранении токена аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токена' });
}
});
// Удаление одного токена
router.delete('/auth-token/:address/:network', requireAdmin, async (req, res) => {
try {
const { address, network } = req.params;
await authTokenService.deleteAuthToken(address, network);
res.json({ success: true, message: 'Токен аутентификации удалён' });
} catch (error) {
logger.error('Ошибка при удалении токена аутентификации:', error);
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении токена' });
}
});
// Тестирование RPC соединения // Тестирование RPC соединения
router.post('/rpc-test', requireAdmin, async (req, res) => { router.post('/rpc-test', requireAdmin, async (req, res) => {
try { try {
@@ -123,7 +123,12 @@ router.post('/rpc-test', requireAdmin, async (req, res) => {
try { try {
// Пробуем создать провайдера и получить номер последнего блока (обновлено для ethers v6) // Пробуем создать провайдера и получить номер последнего блока (обновлено для ethers v6)
const provider = new ethers.JsonRpcProvider(rpcUrl); let provider;
if (rpcUrl.startsWith('ws://') || rpcUrl.startsWith('wss://')) {
provider = new ethers.WebSocketProvider(rpcUrl);
} else {
provider = new ethers.JsonRpcProvider(rpcUrl);
}
// Устанавливаем таймаут для соединения // Устанавливаем таймаут для соединения
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>

View File

@@ -1,29 +1,21 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { requireAuth } = require('../middleware/auth');
const authService = require('../services/auth-service');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const authService = require('../services/auth-service');
// Получение балансов токенов // Получение балансов токенов пользователя по токенам из базы
router.get('/balances', requireAuth, async (req, res) => { router.get('/balances', async (req, res) => {
try { try {
const { address } = req.session; const address = req.query.address;
if (!address) { if (!address) {
return res.status(400).json({ return res.status(400).json({ success: false, error: 'Не указан адрес кошелька' });
error: 'No wallet address in session',
});
} }
logger.info(`Fetching token balances for address: ${address}`); logger.info(`Fetching token balances for address: ${address}`);
const balances = await authService.getTokenBalances(address); const balances = await authService.getUserTokenBalances(address);
res.json({ success: true, data: balances });
res.json(balances);
} catch (error) { } catch (error) {
logger.error('Error fetching token balances:', error); logger.error('Error fetching token balances:', error);
res.status(500).json({ res.status(500).json({ success: false, error: 'Failed to fetch token balances' });
error: 'Failed to fetch token balances',
});
} }
}); });

View File

@@ -5,26 +5,13 @@ const crypto = require('crypto');
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
const verificationService = require('./verification-service'); // Используем сервис верификации const verificationService = require('./verification-service'); // Используем сервис верификации
const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ
const authTokenService = require('./authTokenService');
const ADMIN_CONTRACTS = [ const rpcProviderService = require('./rpcProviderService');
{ address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'ethereum' },
{ address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc' },
{ address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum' },
{ address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'polygon' },
];
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)']; const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
class AuthService { class AuthService {
constructor() { constructor() {}
// Используем существующие переменные окружения с префиксом RPC_URL_
this.providers = {
ethereum: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH), // Используем RPC_URL_ETH для ethereum
polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON),
bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC),
arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM),
};
}
// Проверка подписи // Проверка подписи
async verifySignature(message, signature, address) { async verifySignature(message, signature, address) {
@@ -127,100 +114,86 @@ class AuthService {
*/ */
async checkAdminRole(address) { async checkAdminRole(address) {
if (!address) return false; if (!address) return false;
logger.info(`Checking admin role for address: ${address}`); logger.info(`Checking admin role for address: ${address}`);
let foundTokens = false; let foundTokens = false;
let errorCount = 0; let errorCount = 0;
const balances = {}; const balances = {};
const totalNetworks = ADMIN_CONTRACTS.length; // Получаем токены и RPC из базы
const tokens = await authTokenService.getAllAuthTokens();
// Создаем массив промисов для параллельной проверки балансов const rpcProviders = await rpcProviderService.getAllRpcProviders();
const checkPromises = ADMIN_CONTRACTS.map(async (contract) => { const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
}
const checkPromises = tokens.map(async (token) => {
try { try {
const provider = this.providers[contract.network]; const rpcUrl = rpcMap[token.network];
if (!provider) { if (!rpcUrl) {
logger.error(`No provider available for network ${contract.network}`); logger.error(`No RPC URL for network ${token.network}`);
balances[contract.network] = 'Error: No provider'; balances[token.network] = 'Error: No RPC URL';
errorCount++; errorCount++;
return null; return null;
} }
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Проверяем доступность провайдера // Проверяем доступность сети с таймаутом
try { try {
// Проверка доступности сети с таймаутом
const networkCheckPromise = provider.getNetwork(); const networkCheckPromise = provider.getNetwork();
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Network check timeout')), 3000) setTimeout(() => reject(new Error('Network check timeout')), 3000)
); );
await Promise.race([networkCheckPromise, timeoutPromise]); await Promise.race([networkCheckPromise, timeoutPromise]);
} catch (networkError) { } catch (networkError) {
logger.error( logger.error(`Provider for ${token.network} is not available: ${networkError.message}`);
`Provider for ${contract.network} is not available: ${networkError.message}` balances[token.network] = 'Error: Network unavailable';
);
balances[contract.network] = 'Error: Network unavailable';
errorCount++; errorCount++;
return null; return null;
} }
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
const tokenContract = new ethers.Contract(contract.address, ERC20_ABI, provider);
// Создаем промис с таймаутом
const balancePromise = tokenContract.balanceOf(address); const balancePromise = tokenContract.balanceOf(address);
const timeoutPromise = new Promise((_, reject) => const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 3000) setTimeout(() => reject(new Error('Timeout')), 3000)
); );
// Ждем первый выполненный промис
const balance = await Promise.race([balancePromise, timeoutPromise]); const balance = await Promise.race([balancePromise, timeoutPromise]);
const formattedBalance = ethers.formatUnits(balance, 18); const formattedBalance = ethers.formatUnits(balance, 18);
balances[contract.network] = formattedBalance; balances[token.network] = formattedBalance;
logger.info(`Token balance on ${token.network}:`, {
logger.info(`Token balance on ${contract.network}:`, {
address, address,
contract: contract.address, contract: token.address,
balance: formattedBalance, balance: formattedBalance,
hasTokens: balance > 0, minBalance: token.min_balance,
hasTokens: parseFloat(formattedBalance) >= parseFloat(token.min_balance),
}); });
if (parseFloat(formattedBalance) >= parseFloat(token.min_balance)) {
if (parseFloat(formattedBalance) > 0) { logger.info(`Found admin tokens on ${token.network}`);
logger.info(`Found admin tokens on ${contract.network}`);
foundTokens = true; foundTokens = true;
} }
return { network: token.network, balance: formattedBalance };
return { network: contract.network, balance: formattedBalance };
} catch (error) { } catch (error) {
logger.error(`Error checking balance in ${contract.network}:`, { logger.error(`Error checking balance in ${token.network}:`, {
address, address,
contract: contract.address, contract: token.address,
error: error.message || 'Unknown error', error: error.message || 'Unknown error',
}); });
balances[contract.network] = 'Error'; balances[token.network] = 'Error';
errorCount++; errorCount++;
return null; return null;
} }
}); });
// Ждем выполнения всех проверок
await Promise.all(checkPromises); await Promise.all(checkPromises);
if (errorCount === tokens.length) {
// Если все запросы завершились с ошибкой, считаем, что проверка не удалась
if (errorCount === totalNetworks) {
logger.error(`All network checks for ${address} failed. Cannot verify admin status.`); logger.error(`All network checks for ${address} failed. Cannot verify admin status.`);
return false; return false;
} }
if (foundTokens) { if (foundTokens) {
logger.info(`Admin role summary for ${address}:`, { logger.info(`Admin role summary for ${address}:`, {
networks: Object.keys(balances).filter( networks: Object.keys(balances).filter(
(net) => balances[net] > 0 && balances[net] !== 'Error' (net) => parseFloat(balances[net]) > 0 && balances[net] !== 'Error'
), ),
balances, balances,
}); });
logger.info(`Admin role granted for ${address}`); logger.info(`Admin role granted for ${address}`);
return true; return true;
} }
logger.info(`Admin role denied - no tokens found for ${address}`); logger.info(`Admin role denied - no tokens found for ${address}`);
return false; return false;
} }
@@ -238,6 +211,7 @@ class AuthService {
bsc: '0', bsc: '0',
arbitrum: '0', arbitrum: '0',
polygon: '0', polygon: '0',
sepolia: '0',
}; };
} }
@@ -889,6 +863,46 @@ class AuthService {
throw new Error('Ошибка обработки верификации Email'); throw new Error('Ошибка обработки верификации Email');
} }
} }
/**
* Получение балансов токенов пользователя только по токенам из базы
* @param {string} address - адрес кошелька
* @returns {Promise<Array>} - массив объектов с балансами
*/
async getUserTokenBalances(address) {
if (!address) return [];
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
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 results = [];
for (const token of tokens) {
const rpcUrl = rpcMap[token.network];
if (!rpcUrl) continue;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
let balance = '0';
try {
const rawBalance = await tokenContract.balanceOf(address);
balance = ethers.formatUnits(rawBalance, 18);
if (!balance || isNaN(Number(balance))) balance = '0';
} catch (e) {
logger.error(`[getUserTokenBalances] Ошибка получения баланса для ${token.name} (${token.address}) в сети ${token.network}:`, e);
balance = '0';
}
results.push({
network: token.network,
tokenAddress: token.address,
tokenName: token.name,
symbol: token.symbol || '',
balance,
});
}
return results;
}
} }
// Создаем и экспортируем единственный экземпляр // Создаем и экспортируем единственный экземпляр

View File

@@ -0,0 +1,32 @@
const db = require('../db');
async function getAllAuthTokens() {
const { rows } = await db.query('SELECT * FROM auth_tokens ORDER BY id');
return rows;
}
async function saveAllAuthTokens(authTokens) {
await db.query('DELETE FROM auth_tokens');
for (const token of authTokens) {
await db.query(
'INSERT INTO auth_tokens (name, address, network, min_balance) VALUES ($1, $2, $3, $4)',
[token.name, token.address, token.network, token.minBalance]
);
}
}
async function upsertAuthToken(token) {
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
await db.query(
`INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES ($1, $2, $3, $4)
ON CONFLICT (address, network) DO UPDATE SET name=EXCLUDED.name, min_balance=EXCLUDED.min_balance`,
[token.name, token.address, token.network, minBalance]
);
}
async function deleteAuthToken(address, network) {
await db.query('DELETE FROM auth_tokens WHERE address = $1 AND network = $2', [address, network]);
}
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };

View File

@@ -0,0 +1,31 @@
const db = require('../db');
async function getAllRpcProviders() {
const { rows } = await db.query('SELECT * FROM rpc_providers ORDER BY id');
return rows;
}
async function saveAllRpcProviders(rpcConfigs) {
await db.query('DELETE FROM rpc_providers');
for (const cfg of rpcConfigs) {
await db.query(
'INSERT INTO rpc_providers (network_id, rpc_url, chain_id) VALUES ($1, $2, $3)',
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
}
}
async function upsertRpcProvider(cfg) {
await db.query(
`INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
VALUES ($1, $2, $3)
ON CONFLICT (network_id) DO UPDATE SET rpc_url=EXCLUDED.rpc_url, chain_id=EXCLUDED.chain_id`,
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
}
async function deleteRpcProvider(networkId) {
await db.query('DELETE FROM rpc_providers WHERE network_id = $1', [networkId]);
}
module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider };

View File

@@ -18,7 +18,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted, computed } from 'vue'; import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { useAuth } from './composables/useAuth'; import { useAuth } from './composables/useAuth';
import { fetchTokenBalances } from './services/tokens'; import { fetchTokenBalances } from './services/tokens';
@@ -142,6 +142,21 @@
if (auth.isAuthenticated.value) { if (auth.isAuthenticated.value) {
refreshTokenBalances(); refreshTokenBalances();
} }
// Подписываемся на событие изменения настроек аутентификации
const unsubscribe = eventBus.on('auth-settings-saved', () => {
console.log('[App] Получено событие сохранения настроек аутентификации, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
// Отписываемся при размонтировании компонента
onUnmounted(() => {
if (unsubscribe) {
unsubscribe();
}
});
}); });
</script> </script>

View File

@@ -62,29 +62,16 @@
<div v-if="isLoadingTokens" class="token-loading"> <div v-if="isLoadingTokens" class="token-loading">
Загрузка балансов... Загрузка балансов...
</div> </div>
<div v-else-if="!tokenBalances || Object.keys(tokenBalances).length === 0" class="token-no-data"> <div v-else-if="!tokenBalances || tokenBalances.length === 0" class="token-no-data">
Баланс не доступен Баланс не доступен
</div> </div>
<div v-else> <div v-else>
<div v-if="tokenBalances.eth" class="token-balance"> <div class="token-balance-header">
<span class="token-name">ETH:</span>
<span class="token-amount">{{ Number(tokenBalances.eth).toLocaleString() }}</span>
<span class="token-symbol">{{ TOKEN_CONTRACTS.eth.symbol }}</span>
</div> </div>
<div v-if="tokenBalances.bsc" class="token-balance"> <div v-for="(token, index) in tokenBalances.data || []" :key="token.tokenAddress ? token.tokenAddress : 'token-' + index" class="token-balance-row">
<span class="token-name">BSC:</span> <span class="token-name">{{ token.tokenName }}</span>
<span class="token-amount">{{ Number(tokenBalances.bsc).toLocaleString() }}</span> <span class="token-network">{{ token.network }}</span>
<span class="token-symbol">{{ TOKEN_CONTRACTS.bsc.symbol }}</span> <span class="token-amount">{{ isNaN(Number(token.balance)) ? '' : Number(token.balance).toLocaleString() }}</span>
</div>
<div v-if="tokenBalances.arbitrum" class="token-balance">
<span class="token-name">ARB:</span>
<span class="token-amount">{{ Number(tokenBalances.arbitrum).toLocaleString() }}</span>
<span class="token-symbol">{{ TOKEN_CONTRACTS.arbitrum.symbol }}</span>
</div>
<div v-if="tokenBalances.polygon" class="token-balance">
<span class="token-name">POL:</span>
<span class="token-amount">{{ Number(tokenBalances.polygon).toLocaleString() }}</span>
<span class="token-symbol">{{ TOKEN_CONTRACTS.polygon.symbol }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -96,7 +83,6 @@
<script setup> <script setup>
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, watch } from 'vue'; import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { TOKEN_CONTRACTS } from '../services/tokens';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import eventBus from '../utils/eventBus'; import eventBus from '../utils/eventBus';
@@ -346,11 +332,6 @@ h3 {
flex: 1; flex: 1;
} }
.token-symbol {
color: var(--color-text-light);
margin-left: var(--spacing-xs);
}
.token-no-data, .token-no-data,
.user-info-empty { .user-info-empty {
color: var(--color-text-light); color: var(--color-text-light);
@@ -432,4 +413,31 @@ h3 {
padding: 0 10px; padding: 0 10px;
} }
} }
.token-balance-header {
display: flex;
font-weight: bold;
color: var(--color-primary, #4caf50);
gap: 10px;
margin-bottom: 6px;
}
.token-balance-row {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 4px;
}
.token-name {
min-width: 80px;
font-weight: 500;
}
.token-network {
min-width: 70px;
color: var(--color-dark, #333);
}
.token-amount {
min-width: 80px;
text-align: right;
font-variant-numeric: tabular-nums;
}
</style> </style>

View File

@@ -6,34 +6,6 @@ import axios from 'axios';
* Предоставляет списки доступных сетей, URL RPC и функции для работы с ними * Предоставляет списки доступных сетей, URL RPC и функции для работы с ними
*/ */
export default function useBlockchainNetworks() { export default function useBlockchainNetworks() {
// Список стандартных URL для популярных сетей
const defaultRpcUrls = {
ethereum: 'https://mainnet.infura.io/v3/YOUR_API_KEY',
sepolia: 'https://sepolia.infura.io/v3/YOUR_API_KEY',
goerli: 'https://goerli.infura.io/v3/YOUR_API_KEY',
holesky: 'https://holesky.infura.io/v3/YOUR_API_KEY',
bsc: 'https://bsc-dataseed1.binance.org',
'bsc-testnet': 'https://data-seed-prebsc-1-s1.binance.org:8545',
polygon: 'https://polygon-rpc.com',
mumbai: 'https://rpc-mumbai.maticvigil.com',
arbitrum: 'https://arb1.arbitrum.io/rpc',
'arbitrum-goerli': 'https://goerli-rollup.arbitrum.io/rpc',
optimism: 'https://mainnet.optimism.io',
'optimism-goerli': 'https://goerli.optimism.io',
avalanche: 'https://api.avax.network/ext/bc/C/rpc',
'avalanche-fuji': 'https://api.avax-test.network/ext/bc/C/rpc',
gnosis: 'https://rpc.gnosischain.com',
celo: 'https://forno.celo.org',
fantom: 'https://rpc.ftm.tools',
'fantom-testnet': 'https://rpc.testnet.fantom.network',
harmony: 'https://api.harmony.one',
metis: 'https://andromeda.metis.io/?owner=1088',
aurora: 'https://mainnet.aurora.dev',
cronos: 'https://evm.cronos.org',
localhost: 'http://localhost:8545',
ganache: 'http://localhost:7545'
};
// Группы сетей для отображения в интерфейсе // Группы сетей для отображения в интерфейсе
const networkGroups = [ const networkGroups = [
{ {
@@ -96,21 +68,6 @@ export default function useBlockchainNetworks() {
customChainId: null customChainId: null
}); });
// Вычисляемое свойство для предложения URL на основе выбранной сети
const defaultRpcUrlSuggestion = computed(() => {
if (networkEntry.value.networkId && defaultRpcUrls[networkEntry.value.networkId]) {
return defaultRpcUrls[networkEntry.value.networkId];
}
return '';
});
// Функция для использования предложенного URL
const useDefaultRpcUrl = () => {
if (defaultRpcUrlSuggestion.value) {
networkEntry.value.rpcUrl = defaultRpcUrlSuggestion.value;
}
};
// Функция для получения chainId по networkId // Функция для получения chainId по networkId
const getChainIdByNetworkId = (networkId) => { const getChainIdByNetworkId = (networkId) => {
for (const group of networkGroups) { for (const group of networkGroups) {
@@ -215,16 +172,13 @@ export default function useBlockchainNetworks() {
return { return {
// Данные // Данные
defaultRpcUrls,
networkGroups, networkGroups,
networkEntry, networkEntry,
defaultRpcUrlSuggestion,
testingRpc, testingRpc,
testingRpcId, testingRpcId,
networks, // Экспортируем плоский список сетей networks, // Экспортируем плоский список сетей
// Методы // Методы
useDefaultRpcUrl,
getChainIdByNetworkId, getChainIdByNetworkId,
validateAndPrepareNetworkConfig, validateAndPrepareNetworkConfig,
resetNetworkEntry, resetNetworkEntry,

View File

@@ -1,15 +1,12 @@
import { ref, watch, onUnmounted } from 'vue'; import { ref, watch, onUnmounted } from 'vue';
import { fetchTokenBalances } from '../services/tokens'; import { fetchTokenBalances } from '../services/tokens';
import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
import eventBus from '../utils/eventBus';
export function useTokenBalances() { export function useTokenBalances() {
const auth = useAuth(); // Получаем доступ к состоянию аутентификации const auth = useAuth(); // Получаем доступ к состоянию аутентификации
const tokenBalances = ref({ const tokenBalances = ref([]); // теперь массив объектов
eth: '0', const isLoadingTokens = ref(false);
bsc: '0',
arbitrum: '0',
polygon: '0',
});
let balanceUpdateInterval = null; let balanceUpdateInterval = null;
const getIdentityValue = (type) => { const getIdentityValue = (type) => {
@@ -23,28 +20,25 @@ export function useTokenBalances() {
const walletAddress = auth.address?.value || getIdentityValue('wallet'); const walletAddress = auth.address?.value || getIdentityValue('wallet');
if (walletAddress) { if (walletAddress) {
try { try {
isLoadingTokens.value = true;
console.log('[useTokenBalances] Запрос балансов для адреса:', walletAddress); console.log('[useTokenBalances] Запрос балансов для адреса:', walletAddress);
const balances = await fetchTokenBalances(walletAddress); const response = await fetchTokenBalances(walletAddress);
console.log('[useTokenBalances] Полученные балансы:', balances); // Ожидаем, что response — это массив объектов
tokenBalances.value = { tokenBalances.value = Array.isArray(response) ? response : (response?.data || []);
eth: balances.eth || '0',
bsc: balances.bsc || '0',
arbitrum: balances.arbitrum || '0',
polygon: balances.polygon || '0',
};
console.log('[useTokenBalances] Обновленные балансы:', tokenBalances.value); console.log('[useTokenBalances] Обновленные балансы:', tokenBalances.value);
} catch (error) { } catch (error) {
console.error('[useTokenBalances] Ошибка при обновлении балансов:', error); console.error('[useTokenBalances] Ошибка при обновлении балансов:', error);
// Возможно, стоит сбросить балансы при ошибке tokenBalances.value = [];
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; } finally {
isLoadingTokens.value = false;
} }
} else { } else {
console.log('[useTokenBalances] Не найден адрес кошелька для запроса балансов.'); console.log('[useTokenBalances] Не найден адрес кошелька для запроса балансов.');
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; tokenBalances.value = [];
} }
} else { } else {
console.log('[useTokenBalances] Пользователь не аутентифицирован, сброс балансов.'); console.log('[useTokenBalances] Пользователь не аутентифицирован, сброс балансов.');
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; tokenBalances.value = [];
} }
}; };
@@ -79,21 +73,31 @@ export function useTokenBalances() {
// Если пользователь вышел, отвязал кошелек, или не аутентифицирован // Если пользователь вышел, отвязал кошелек, или не аутентифицирован
stopBalanceUpdates(); stopBalanceUpdates();
// Сбрасываем балансы // Сбрасываем балансы
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' }; tokenBalances.value = [];
} }
}, },
{ immediate: true } // Запустить проверку сразу при инициализации { immediate: true } // Запустить проверку сразу при инициализации
); );
// Остановка интервала при размонтировании // Подписываемся на событие для обновления баланса после сохранения настроек
const unsubscribe = eventBus.on('auth-settings-saved', () => {
console.log('[useTokenBalances] Получено событие сохранения настроек, обновляем балансы');
updateBalances();
});
// Остановка интервала и отписки при размонтировании
onUnmounted(() => { onUnmounted(() => {
stopBalanceUpdates(); stopBalanceUpdates();
if (unsubscribe) {
unsubscribe();
}
}); });
return { return {
tokenBalances, tokenBalances,
isLoadingTokens,
updateBalances, updateBalances,
startBalanceUpdates, // Можно не экспортировать, если управление полностью автоматическое startBalanceUpdates,
stopBalanceUpdates, // Можно не экспортировать stopBalanceUpdates,
}; };
} }

View File

@@ -1,29 +1,5 @@
import api from '../api/axios'; import api from '../api/axios';
// Адреса смарт-контрактов токенов HB3A
export const TOKEN_CONTRACTS = {
eth: {
address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c',
symbol: 'HB3A',
network: 'Ethereum',
},
bsc: {
address: '0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D',
symbol: 'HB3A',
network: 'BSC',
},
arbitrum: {
address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0',
symbol: 'HB3A',
network: 'Arbitrum',
},
polygon: {
address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d',
symbol: 'HB3A',
network: 'Polygon',
},
};
// Получение балансов токенов // Получение балансов токенов
export const fetchTokenBalances = async (address = null) => { export const fetchTokenBalances = async (address = null) => {
try { try {
@@ -43,6 +19,7 @@ export const fetchTokenBalances = async (address = null) => {
bsc: '0', bsc: '0',
arbitrum: '0', arbitrum: '0',
polygon: '0', polygon: '0',
sepolia: '0',
}; };
} }
}; };

View File

@@ -8,7 +8,6 @@
<div class="setting-form"> <div class="setting-form">
<p>Здесь будут настройки для конфигурации промптов</p> <p>Здесь будут настройки для конфигурации промптов</p>
<textarea v-model="settings.prompt" placeholder="Базовый промпт для ИИ..." rows="5" class="form-control"></textarea> <textarea v-model="settings.prompt" placeholder="Базовый промпт для ИИ..." rows="5" class="form-control"></textarea>
<button class="btn btn-primary" @click="saveSettings('prompt')">Сохранить</button>
</div> </div>
</div> </div>
@@ -23,7 +22,6 @@
Включить RAG Включить RAG
</label> </label>
</div> </div>
<button class="btn btn-primary" @click="saveSettings('rag')">Сохранить</button>
</div> </div>
</div> </div>
@@ -44,7 +42,6 @@
Email Email
</label> </label>
</div> </div>
<button class="btn btn-primary" @click="saveSettings('channels')">Сохранить</button>
</div> </div>
</div> </div>
@@ -62,7 +59,6 @@
<option value="gpt-4o">GPT-4o</option> <option value="gpt-4o">GPT-4o</option>
</select> </select>
</div> </div>
<button class="btn btn-primary" @click="saveSettings('models')">Сохранить</button>
</div> </div>
</div> </div>
</div> </div>
@@ -169,10 +165,6 @@ h3 {
max-width: 500px; /* Ограничим ширину для select/textarea */ max-width: 500px; /* Ограничим ширину для select/textarea */
} }
.btn-primary {
align-self: flex-start;
}
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }

View File

@@ -0,0 +1,98 @@
<template>
<div class="auth-tokens-settings">
<h4>Токены аутентификации</h4>
<div v-if="authTokens.length > 0" class="tokens-list">
<div v-for="(token, index) in authTokens" :key="token.address + token.network" class="token-entry">
<span><strong>Название:</strong> {{ token.name }}</span>
<span><strong>Адрес:</strong> {{ token.address }}</span>
<span><strong>Сеть:</strong> {{ getNetworkLabel(token.network) }}</span>
<span><strong>Мин. баланс:</strong> {{ token.minBalance }}</span>
<button class="btn btn-danger btn-sm" @click="removeToken(index)">Удалить</button>
</div>
</div>
<p v-else>Нет добавленных токенов аутентификации.</p>
<div class="add-token-form">
<h5>Добавить новый токен:</h5>
<div class="form-group">
<label>Название:</label>
<input type="text" v-model="newToken.name" class="form-control" placeholder="test2">
</div>
<div class="form-group">
<label>Адрес:</label>
<input type="text" v-model="newToken.address" class="form-control" placeholder="0x...">
</div>
<div class="form-group">
<label>Сеть:</label>
<select v-model="newToken.network" class="form-control">
<option value="">-- Выберите сеть --</option>
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
<option v-for="option in group.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</optgroup>
</select>
</div>
<div class="form-group">
<label>Мин. баланс:</label>
<input type="number" v-model.number="newToken.minBalance" class="form-control" placeholder="0">
</div>
<button class="btn btn-secondary" @click="addToken">Добавить токен</button>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import api from '@/api/axios';
const props = defineProps({
authTokens: { type: Array, required: true }
});
const emit = defineEmits(['update']);
const newToken = reactive({ name: '', address: '', network: '', minBalance: 0 });
const { networkGroups, networks } = useBlockchainNetworks();
async function addToken() {
if (!newToken.name || !newToken.address || !newToken.network) {
alert('Все поля обязательны');
return;
}
try {
await api.post('/api/settings/auth-token', {
...newToken,
minBalance: Number(newToken.minBalance) || 0
});
emit('update');
newToken.name = '';
newToken.address = '';
newToken.network = '';
newToken.minBalance = 0;
} catch (e) {
alert('Ошибка при добавлении токена: ' + (e.response?.data?.error || e.message));
}
}
async function removeToken(index) {
const token = props.authTokens[index];
if (!token) return;
if (!confirm(`Удалить токен ${token.name} (${token.address})?`)) return;
try {
await api.delete(`/api/settings/auth-token/${token.address}/${token.network}`);
emit('update');
} catch (e) {
alert('Ошибка при удалении токена: ' + (e.response?.data?.error || e.message));
}
}
function getNetworkLabel(networkId) {
const found = networks.value.find(n => n.value === networkId);
return found ? found.label : networkId;
}
</script>
<style scoped>
.tokens-list { margin-bottom: 1rem; }
.token-entry { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.5rem; }
.add-token-form { margin-top: 1rem; }
</style>

View File

@@ -235,14 +235,6 @@
<button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button> <button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button>
</div> </div>
<!-- Кнопка сохранения настроек RPC -->
<div class="save-rpc-actions mt-3">
<button class="btn btn-primary" @click="saveRpcSettingsWithFeedback" :disabled="isSavingRpc">
<i class="fas" :class="isSavingRpc ? 'fa-spinner fa-spin' : 'fa-save'"></i>
{{ isSavingRpc ? 'Сохранение...' : 'Сохранить RPC настройки' }}
</button>
</div>
<!-- 8. Выбор сети для деплоя --> <!-- 8. Выбор сети для деплоя -->
<h4>Сеть для деплоя</h4> <h4>Сеть для деплоя</h4>
<div class="form-group"> <div class="form-group">
@@ -918,20 +910,24 @@ const toggleShowDeployerKey = () => {
const loadRpcSettings = async () => { const loadRpcSettings = async () => {
try { try {
const response = await axios.get('/api/settings/rpc'); const response = await axios.get('/api/settings/rpc');
console.log('Ответ сервера на /api/settings/rpc:', response.data);
if (response.data && response.data.success) { if (response.data && response.data.success) {
securitySettings.rpcConfigs = response.data.data || []; securitySettings.rpcConfigs = (response.data.data || []).map(rpc => ({
networkId: rpc.network_id,
rpcUrl: rpc.rpc_url,
chainId: rpc.chain_id
}));
console.log('[BlockchainSettingsView] RPC конфигурации успешно загружены:', securitySettings.rpcConfigs); console.log('[BlockchainSettingsView] RPC конфигурации успешно загружены:', securitySettings.rpcConfigs);
} }
} catch (error) { } catch (error) {
console.error('[BlockchainSettingsView] Ошибка при загрузке RPC конфигураций:', error); console.error('[BlockchainSettingsView] Ошибка при загрузке RPC конфигураций:', error);
// Если нужно, установить дефолтные RPC
// setDefaultRpcConfigs();
} }
}; };
// Функция сохранения настроек RPC на сервер // Функция сохранения настроек RPC на сервер
const saveRpcSettings = async () => { const saveRpcSettings = async () => {
try { try {
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
const response = await axios.post('/api/settings/rpc', { const response = await axios.post('/api/settings/rpc', {
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs)) rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
}); });

View File

@@ -15,7 +15,6 @@
<!-- Добавить другие языки по необходимости --> <!-- Добавить другие языки по необходимости -->
</select> </select>
</div> </div>
<button class="btn btn-primary" @click="saveLanguageSetting">Сохранить язык</button>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,138 @@
<template>
<div class="rpc-providers-settings">
<h4>RPC Провайдеры</h4>
<div v-if="Array.isArray(rpcConfigs) && rpcConfigs.length > 0" class="rpc-list">
<div v-for="(rpc, index) in rpcConfigs" :key="rpc.networkId" class="rpc-entry">
<span><strong>ID Сети:</strong> {{ rpc.networkId }}</span>
<span><strong>URL:</strong> {{ rpc.rpcUrl }}</span>
<span v-if="rpc.chainId"><strong>Chain ID:</strong> {{ rpc.chainId }}</span>
<button class="btn btn-info btn-sm" @click="testRpc(rpc)" :disabled="testingRpc && testingRpcId === rpc.networkId">
<i class="fas" :class="testingRpc && testingRpcId === rpc.networkId ? 'fa-spinner fa-spin' : 'fa-check-circle'"></i>
{{ testingRpc && testingRpcId === rpc.networkId ? 'Проверка...' : 'Тест' }}
</button>
<button class="btn btn-danger btn-sm" @click="removeRpc(index)">Удалить</button>
</div>
</div>
<p v-else>Нет добавленных RPC конфигураций.</p>
<div class="add-rpc-form">
<h5>Добавить новую RPC конфигурацию:</h5>
<div class="form-group">
<label>ID Сети:</label>
<select v-model="networkEntry.networkId" class="form-control">
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
<option v-for="option in group.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</optgroup>
</select>
<div v-if="networkEntry.networkId === 'custom'" class="mt-2">
<label>Пользовательский ID:</label>
<input type="text" v-model="networkEntry.customNetworkId" class="form-control" placeholder="Введите ID сети">
<label class="mt-2">Chain ID:</label>
<input type="number" v-model.number="networkEntry.customChainId" class="form-control" placeholder="Например, 1 для Ethereum">
<small>Chain ID - уникальный идентификатор блокчейн-сети (целое число)</small>
</div>
</div>
<div class="form-group">
<label>RPC URL:</label>
<input type="text" v-model="networkEntry.rpcUrl" class="form-control" placeholder="https://...">
</div>
<button class="btn btn-secondary" @click="addRpc">Добавить RPC</button>
</div>
</div>
</template>
<script setup>
import { ref, toRefs } from 'vue';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import api from '@/api/axios';
const props = defineProps({
rpcConfigs: { type: Array, required: true, default: () => [] }
});
const emit = defineEmits(['update', 'test']);
const {
networkGroups,
networkEntry,
validateAndPrepareNetworkConfig,
resetNetworkEntry,
testRpcConnection,
testingRpc,
testingRpcId
} = useBlockchainNetworks();
async function addRpc() {
const result = validateAndPrepareNetworkConfig();
if (!result.valid) {
alert(result.error);
return;
}
const { networkId, rpcUrl, chainId } = result.networkConfig;
if (props.rpcConfigs.some(rpc => rpc.networkId === networkId)) {
alert(`Ошибка: RPC конфигурация для сети с ID '${networkId}' уже существует.`);
return;
}
try {
await api.post('/api/settings/rpc', { networkId, rpcUrl, chainId });
emit('update'); // сигнал родителю перезагрузить список
resetNetworkEntry();
} catch (e) {
alert('Ошибка при добавлении RPC: ' + (e.response?.data?.error || e.message));
}
}
async function removeRpc(index) {
const rpc = props.rpcConfigs[index];
if (!rpc) return;
if (!confirm(`Удалить RPC для сети ${rpc.networkId}?`)) return;
try {
await api.delete(`/api/settings/rpc/${rpc.networkId}`);
emit('update');
} catch (e) {
alert('Ошибка при удалении RPC: ' + (e.response?.data?.error || e.message));
}
}
async function testRpc(rpc) {
if (!rpc.networkId || !rpc.rpcUrl) {
alert('Для теста RPC нужно указать и ID сети, и URL');
return;
}
const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl);
if (result.success) {
alert(result.message);
} else {
alert(`Ошибка при подключении к ${rpc.networkId}: ${result.error}`);
}
}
</script>
<style scoped>
.rpc-list { margin-bottom: 1rem; }
.rpc-entry { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.5rem; }
.add-rpc-form { margin-top: 1rem; }
.suggestion {
background-color: rgba(76, 175, 80, 0.1);
border-left: 3px solid var(--color-primary, #4caf50);
padding: 6px 10px;
margin-top: 8px;
border-radius: 0 4px 4px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-link {
background: none;
border: none;
padding: 0;
color: var(--color-primary, #4caf50);
text-decoration: underline;
cursor: pointer;
font-size: 0.875rem;
}
.btn-link:hover {
color: var(--color-primary-dark, #388e3c);
text-decoration: none;
}
.mt-2 { margin-top: 10px; }
</style>

View File

@@ -38,134 +38,27 @@
</div> </div>
</div> </div>
<!-- Раскрывающиеся настройки RPC --> <RpcProvidersSettings
<div v-if="showRpcSettings" class="detail-panel"> v-if="showRpcSettings"
<h3>Настройки RPC Провайдеров</h3> :rpcConfigs="securitySettings.rpcConfigs"
<p class="env-note">Эти настройки сохраняются в .env файле на бэкенде.</p> @update="loadSettings"
@test="testRpcHandler"
<!-- Список добавленных RPC --> />
<div v-if="securitySettings.rpcConfigs.length > 0" class="configs-list"> <AuthTokensSettings
<h4>Добавленные RPC конфигурации:</h4> v-if="showAuthSettings"
<div v-for="(rpc, index) in securitySettings.rpcConfigs" :key="index" class="config-item"> :authTokens="securitySettings.authTokens"
<div class="config-details"> @update="loadSettings"
<div><strong>ID Сети:</strong> {{ rpc.networkId }}</div> />
<div><strong>URL:</strong> {{ rpc.rpcUrl }}</div>
<div v-if="rpc.chainId"><strong>Chain ID:</strong> {{ rpc.chainId }}</div>
</div>
<div class="config-actions">
<button class="btn btn-info" @click="testRpcHandler(rpc)" :disabled="testingRpc && testingRpcId === rpc.networkId">
<i class="fas" :class="testingRpc && testingRpcId === rpc.networkId ? 'fa-spinner fa-spin' : 'fa-check-circle'"></i>
{{ testingRpc && testingRpcId === rpc.networkId ? 'Проверка...' : 'Тест' }}
</button>
<button class="btn btn-danger" @click="removeRpcConfig(index)">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
</div>
<p v-else>Нет добавленных RPC конфигураций.</p>
<!-- Форма добавления нового RPC -->
<div class="setting-form add-form">
<h4>Добавить новую RPC конфигурацию:</h4>
<div class="form-group">
<label class="form-label" for="newRpcNetworkId">ID Сети:</label>
<select id="newRpcNetworkId" v-model="networkEntry.networkId" class="form-control">
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
<option v-for="option in group.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</optgroup>
</select>
<div v-if="networkEntry.networkId === 'custom'" class="mt-2">
<label class="form-label" for="customNetworkId">Пользовательский ID:</label>
<input type="text" id="customNetworkId" v-model="networkEntry.customNetworkId" class="form-control" placeholder="Введите ID сети">
<label class="form-label mt-2" for="customChainId">Chain ID:</label>
<input type="number" id="customChainId" v-model="networkEntry.customChainId" class="form-control" placeholder="Например, 1 для Ethereum">
<small>Chain ID - уникальный идентификатор блокчейн-сети (целое число)</small>
</div>
<small>ID сети должен совпадать со значением в выпадающем списке сетей при создании DLE</small>
</div>
<div class="form-group">
<label class="form-label" for="newRpcUrl">RPC URL:</label>
<input type="text" id="newRpcUrl" v-model="networkEntry.rpcUrl" class="form-control" placeholder="https://...">
<!-- Предложение URL на основе выбранной сети -->
<small v-if="defaultRpcUrlSuggestion" class="suggestion">
Предложение: {{ defaultRpcUrlSuggestion }}
<button class="btn-link" @click="useDefaultRpcUrl">Использовать</button>
</small>
</div>
<button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button>
</div>
</div>
<!-- Раскрывающиеся настройки Аутентификации -->
<div v-if="showAuthSettings" class="detail-panel">
<h3>Настройки Аутентификации</h3>
<p class="env-note">Эти настройки сохраняются в .env файле на бэкенде.</p>
<!-- Список токенов для проверки баланса -->
<div v-if="securitySettings.authTokens.length > 0" class="configs-list">
<h4>Токены для проверки при авторизации:</h4>
<div v-for="(token, index) in securitySettings.authTokens" :key="index" class="config-item">
<div class="config-details">
<div><strong>Название:</strong> {{ token.name }}</div>
<div><strong>Адрес:</strong> {{ token.address }}</div>
<div><strong>Сеть:</strong> {{ token.network }}</div>
<div><strong>Мин. баланс:</strong> {{ token.minBalance }}</div>
</div>
<button class="btn btn-danger" @click="removeAuthToken(index)">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
<p v-else>Нет добавленных токенов для проверки при авторизации.</p>
<!-- Форма добавления нового токена для авторизации -->
<div class="setting-form add-form">
<h4>Добавить новый токен для проверки при авторизации:</h4>
<div class="form-group">
<label class="form-label" for="newTokenName">Название токена:</label>
<input type="text" id="newTokenName" v-model="newAuthToken.name" class="form-control" placeholder="Например, MyToken">
</div>
<div class="form-group">
<label class="form-label" for="newTokenAddress">Адрес контракта:</label>
<input type="text" id="newTokenAddress" v-model="newAuthToken.address" class="form-control" placeholder="0x...">
</div>
<div class="form-group">
<label class="form-label" for="newTokenNetwork">Сеть:</label>
<select id="newTokenNetwork" v-model="newAuthToken.network" class="form-control">
<option v-for="option in networkOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="newTokenMinBalance">Минимальный баланс:</label>
<input type="text" id="newTokenMinBalance" v-model="newAuthToken.minBalance" class="form-control" placeholder="1.0">
<small>Минимальный баланс токена для успешной авторизации</small>
</div>
<button class="btn btn-secondary" @click="addAuthToken">Добавить токен</button>
</div>
</div>
<div class="sub-settings-panel save-panel">
<button class="btn btn-primary btn-lg" @click="saveSecuritySettings" :disabled="isSaving">
<span v-if="isSaving">
<i class="fas fa-spinner fa-spin"></i>
Сохранение...
</span>
<span v-else>Сохранить все настройки</span>
</button>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, ref, onMounted, computed } from 'vue'; import { reactive, ref, onMounted, computed, provide } from 'vue';
import api from '@/api/axios'; import api from '@/api/axios';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import eventBus from '@/utils/eventBus';
import RpcProvidersSettings from './RpcProvidersSettings.vue';
import AuthTokensSettings from './AuthTokensSettings.vue';
// Состояние для отображения/скрытия дополнительных настроек // Состояние для отображения/скрытия дополнительных настроек
const showRpcSettings = ref(false); const showRpcSettings = ref(false);
@@ -183,8 +76,6 @@ const securitySettings = reactive({
const { const {
networkGroups, networkGroups,
networkEntry, networkEntry,
defaultRpcUrlSuggestion,
useDefaultRpcUrl,
validateAndPrepareNetworkConfig, validateAndPrepareNetworkConfig,
resetNetworkEntry, resetNetworkEntry,
testRpcConnection, testRpcConnection,
@@ -239,13 +130,22 @@ const loadSettings = async () => {
// Загрузка RPC конфигураций // Загрузка RPC конфигураций
const rpcResponse = await api.get('/api/settings/rpc'); const rpcResponse = await api.get('/api/settings/rpc');
if (rpcResponse.data && rpcResponse.data.success) { if (rpcResponse.data && rpcResponse.data.success) {
securitySettings.rpcConfigs = rpcResponse.data.data || []; securitySettings.rpcConfigs = (rpcResponse.data.data || []).map(rpc => ({
networkId: rpc.network_id,
rpcUrl: rpc.rpc_url,
chainId: rpc.chain_id
}));
} }
// Загрузка токенов для аутентификации // Загрузка токенов для аутентификации
const authResponse = await api.get('/api/settings/auth-tokens'); const authResponse = await api.get('/api/settings/auth-tokens');
if (authResponse.data && authResponse.data.success) { if (authResponse.data && authResponse.data.success) {
securitySettings.authTokens = authResponse.data.data || []; securitySettings.authTokens = (authResponse.data.data || []).map(token => ({
name: token.name,
address: token.address,
network: token.network,
minBalance: token.min_balance
}));
} }
console.log('[SecuritySettingsView] Настройки успешно загружены с бэкенда'); console.log('[SecuritySettingsView] Настройки успешно загружены с бэкенда');
@@ -263,79 +163,8 @@ const loadSettings = async () => {
// Установка дефолтных значений // Установка дефолтных значений
const setDefaultSettings = () => { const setDefaultSettings = () => {
securitySettings.rpcConfigs = [ securitySettings.rpcConfigs = [];
{ networkId: 'ethereum', rpcUrl: 'https://mainnet.infura.io/v3/YOUR_API_KEY' }, securitySettings.authTokens = [];
{ networkId: 'sepolia', rpcUrl: 'https://sepolia.infura.io/v3/YOUR_API_KEY' },
{ networkId: 'bsc', rpcUrl: 'https://bsc-dataseed1.binance.org' },
{ networkId: 'arbitrum', rpcUrl: 'https://arb1.arbitrum.io/rpc' },
{ networkId: 'polygon', rpcUrl: 'https://polygon-rpc.com' }
];
securitySettings.authTokens = [
{ name: 'Ethereum Token', address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'eth', minBalance: '1.0' },
{ name: 'BSC Token', address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc', minBalance: '10.0' },
{ name: 'Arbitrum Token', address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum', minBalance: '0.5' },
{ name: 'Custom Token', address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'eth', minBalance: '5.0' }
];
};
// Функция добавления новой RPC конфигурации
const addRpcConfig = () => {
const result = validateAndPrepareNetworkConfig();
if (!result.valid) {
alert(result.error);
return;
}
const { networkId, rpcUrl, chainId } = result.networkConfig;
// Проверка на дубликат ID
if (securitySettings.rpcConfigs.some(rpc => rpc.networkId === networkId)) {
alert(`Ошибка: RPC конфигурация для сети с ID '${networkId}' уже существует.`);
return;
}
securitySettings.rpcConfigs.push({ networkId, rpcUrl, chainId });
// Очистка полей ввода
resetNetworkEntry();
};
// Функция удаления RPC конфигурации
const removeRpcConfig = (index) => {
securitySettings.rpcConfigs.splice(index, 1);
};
// Функция добавления нового токена для авторизации
const addAuthToken = () => {
const name = newAuthToken.name.trim();
const address = newAuthToken.address.trim();
const minBalance = newAuthToken.minBalance.trim();
const network = newAuthToken.network;
if (name && address) {
// Проверка на дубликат адреса
if (securitySettings.authTokens.some(token => token.address.toLowerCase() === address.toLowerCase())) {
alert(`Ошибка: Токен с адресом '${address}' уже добавлен.`);
return;
}
securitySettings.authTokens.push({ name, address, minBalance, network });
// Очистка полей ввода
newAuthToken.name = '';
newAuthToken.address = '';
newAuthToken.minBalance = '1.0';
newAuthToken.network = 'eth';
} else {
alert('Пожалуйста, заполните название и адрес токена.');
}
};
// Функция удаления токена авторизации
const removeAuthToken = (index) => {
securitySettings.authTokens.splice(index, 1);
}; };
// Сохранение всех настроек безопасности на бэкенд // Сохранение всех настроек безопасности на бэкенд
@@ -343,8 +172,12 @@ const saveSecuritySettings = async () => {
isSaving.value = true; isSaving.value = true;
try { try {
// Подготовка данных для отправки // Подготовка данных для отправки
const validRpcConfigs = securitySettings.rpcConfigs.filter(
c => c.networkId && c.rpcUrl
);
const settingsData = { const settingsData = {
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs)), rpcConfigs: validRpcConfigs,
authTokens: JSON.parse(JSON.stringify(securitySettings.authTokens)) authTokens: JSON.parse(JSON.stringify(securitySettings.authTokens))
}; };
@@ -362,6 +195,8 @@ const saveSecuritySettings = async () => {
if (rpcResponse.data.success && authResponse.data.success) { if (rpcResponse.data.success && authResponse.data.success) {
alert('Все настройки безопасности успешно сохранены.'); alert('Все настройки безопасности успешно сохранены.');
eventBus.emit('settings-updated');
eventBus.emit('auth-settings-saved', { tokens: settingsData.authTokens });
} else { } else {
alert('Произошла ошибка при сохранении настроек. Проверьте консоль разработчика.'); alert('Произошла ошибка при сохранении настроек. Проверьте консоль разработчика.');
} }
@@ -373,7 +208,31 @@ const saveSecuritySettings = async () => {
} }
}; };
// Функция-обработчик для тестирования RPC соединения // Загрузка настроек при монтировании компонента
onMounted(() => {
loadSettings();
});
// --- Методы для RPC ---
const addRpcConfig = () => {
const result = validateAndPrepareNetworkConfig();
if (!result.valid) {
alert(result.error);
return;
}
const { networkId, rpcUrl, chainId } = result.networkConfig;
if (securitySettings.rpcConfigs.some(rpc => rpc.networkId === networkId)) {
alert(`Ошибка: RPC конфигурация для сети с ID '${networkId}' уже существует.`);
return;
}
securitySettings.rpcConfigs.push({ networkId, rpcUrl, chainId });
resetNetworkEntry();
};
const removeRpcConfig = (index) => {
securitySettings.rpcConfigs.splice(index, 1);
};
const testRpcHandler = async (rpc) => { const testRpcHandler = async (rpc) => {
try { try {
const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl); const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl);
@@ -388,10 +247,47 @@ const testRpcHandler = async (rpc) => {
} }
}; };
// Загрузка настроек при монтировании компонента // --- Методы для токенов аутентификации ---
onMounted(() => { const addAuthToken = () => {
loadSettings(); const name = newAuthToken.name.trim();
}); const address = newAuthToken.address.trim();
const minBalance = newAuthToken.minBalance.trim();
const network = newAuthToken.network;
if (name && address) {
if (securitySettings.authTokens.some(token => token.address.toLowerCase() === address.toLowerCase())) {
alert(`Ошибка: Токен с адресом '${address}' уже добавлен.`);
return;
}
securitySettings.authTokens.push({ name, address, minBalance, network });
newAuthToken.name = '';
newAuthToken.address = '';
newAuthToken.minBalance = '1.0';
newAuthToken.network = 'eth';
} else {
alert('Пожалуйста, заполните название и адрес токена.');
}
};
const removeAuthToken = (index) => {
securitySettings.authTokens.splice(index, 1);
};
// provide для дочерних компонентов
provide('rpcConfigs', securitySettings.rpcConfigs);
provide('removeRpcConfig', removeRpcConfig);
provide('addRpcConfig', addRpcConfig);
provide('testRpcHandler', testRpcHandler);
provide('testingRpc', testingRpc);
provide('testingRpcId', testingRpcId);
provide('networkGroups', networkGroups);
provide('networkEntry', networkEntry);
provide('validateAndPrepareNetworkConfig', validateAndPrepareNetworkConfig);
provide('resetNetworkEntry', resetNetworkEntry);
provide('authTokens', securitySettings.authTokens);
provide('removeAuthToken', removeAuthToken);
provide('addAuthToken', addAuthToken);
provide('newAuthToken', newAuthToken);
provide('networks', networks);
</script> </script>
<style scoped> <style scoped>
@@ -609,14 +505,6 @@ small {
text-decoration: none; text-decoration: none;
} }
.save-panel {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-lg, 20px);
padding-top: var(--spacing-md, 15px);
border-top: 1px solid var(--color-grey-light, #eee);
}
.loading-overlay { .loading-overlay {
position: absolute; position: absolute;
top: 0; top: 0;