ваше сообщение коммита
This commit is contained in:
19
.cursor/rules/context7-mcp.mdc
Normal file
19
.cursor/rules/context7-mcp.mdc
Normal 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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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)
|
||||
);
|
||||
@@ -14,7 +14,7 @@ module.exports = {
|
||||
},
|
||||
networks: {
|
||||
sepolia: {
|
||||
url: process.env.ETHEREUM_NETWORK_URL,
|
||||
url: process.env.RPC_URL_ETH,
|
||||
accounts: [process.env.PRIVATE_KEY],
|
||||
},
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"}
|
||||
|
||||
@@ -104,7 +104,7 @@ async function requireAdmin(req, res, next) {
|
||||
|
||||
// Проверка через кошелек
|
||||
if (req.session.address) {
|
||||
const isAdmin = await authService.checkAdminToken(req.session.address);
|
||||
const isAdmin = await authService.checkAdminTokens(req.session.address);
|
||||
if (isAdmin) {
|
||||
// Обновляем сессию
|
||||
req.session.isAdmin = true;
|
||||
|
||||
@@ -2,52 +2,17 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { ethers } = require('ethers');
|
||||
const rpcProviderService = require('../services/rpcProviderService');
|
||||
const authTokenService = require('../services/authTokenService');
|
||||
|
||||
// Логируем версию ethers для отладки
|
||||
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 настроек
|
||||
router.get('/rpc', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const rpcConfigs = readSettingsFile(RPC_CONFIG_PATH);
|
||||
const rpcConfigs = await rpcProviderService.getAllRpcProviders();
|
||||
res.json({ success: true, data: rpcConfigs });
|
||||
} catch (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) => {
|
||||
try {
|
||||
const { rpcConfigs } = req.body;
|
||||
|
||||
if (!Array.isArray(rpcConfigs)) {
|
||||
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
||||
// Если пришёл массив rpcConfigs — bulk-режим
|
||||
if (Array.isArray(req.body.rpcConfigs)) {
|
||||
const rpcConfigs = req.body.rpcConfigs;
|
||||
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);
|
||||
|
||||
if (success) {
|
||||
res.json({ success: true, message: 'RPC настройки успешно сохранены' });
|
||||
} else {
|
||||
res.status(500).json({ success: false, error: 'Ошибка при сохранении RPC настроек' });
|
||||
// Иначе — одиночный режим (старый)
|
||||
const { networkId, rpcUrl, chainId } = req.body;
|
||||
if (!networkId || !rpcUrl) {
|
||||
return res.status(400).json({ success: false, error: 'networkId и rpcUrl обязательны' });
|
||||
}
|
||||
await rpcProviderService.upsertRpcProvider({ networkId, rpcUrl, chainId });
|
||||
res.json({ success: true, message: 'RPC провайдер сохранён' });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при сохранении RPC настроек:', error);
|
||||
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении настроек RPC' });
|
||||
logger.error('Ошибка при сохранении RPC:', error);
|
||||
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) => {
|
||||
try {
|
||||
const authTokens = readSettingsFile(AUTH_TOKENS_PATH);
|
||||
const authTokens = await authTokenService.getAllAuthTokens();
|
||||
res.json({ success: true, data: authTokens });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при получении токенов аутентификации:', error);
|
||||
@@ -92,24 +72,44 @@ router.get('/auth-tokens', requireAdmin, async (req, res) => {
|
||||
router.post('/auth-tokens', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { authTokens } = req.body;
|
||||
|
||||
if (!Array.isArray(authTokens)) {
|
||||
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
||||
}
|
||||
|
||||
const success = await writeSettingsFile(AUTH_TOKENS_PATH, authTokens);
|
||||
|
||||
if (success) {
|
||||
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
|
||||
} else {
|
||||
res.status(500).json({ success: false, error: 'Ошибка при сохранении токенов аутентификации' });
|
||||
}
|
||||
await authTokenService.saveAllAuthTokens(authTokens);
|
||||
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при сохранении токенов аутентификации:', 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 соединения
|
||||
router.post('/rpc-test', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
@@ -123,7 +123,12 @@ router.post('/rpc-test', requireAdmin, async (req, res) => {
|
||||
|
||||
try {
|
||||
// Пробуем создать провайдера и получить номер последнего блока (обновлено для 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) =>
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const authService = require('../services/auth-service');
|
||||
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 {
|
||||
const { address } = req.session;
|
||||
|
||||
const address = req.query.address;
|
||||
if (!address) {
|
||||
return res.status(400).json({
|
||||
error: 'No wallet address in session',
|
||||
});
|
||||
return res.status(400).json({ success: false, error: 'Не указан адрес кошелька' });
|
||||
}
|
||||
|
||||
logger.info(`Fetching token balances for address: ${address}`);
|
||||
const balances = await authService.getTokenBalances(address);
|
||||
|
||||
res.json(balances);
|
||||
const balances = await authService.getUserTokenBalances(address);
|
||||
res.json({ success: true, data: balances });
|
||||
} catch (error) {
|
||||
logger.error('Error fetching token balances:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch token balances',
|
||||
});
|
||||
res.status(500).json({ success: false, error: 'Failed to fetch token balances' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,26 +5,13 @@ const crypto = require('crypto');
|
||||
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
|
||||
const verificationService = require('./verification-service'); // Используем сервис верификации
|
||||
const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ
|
||||
|
||||
const ADMIN_CONTRACTS = [
|
||||
{ address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'ethereum' },
|
||||
{ address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc' },
|
||||
{ address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum' },
|
||||
{ address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'polygon' },
|
||||
];
|
||||
const authTokenService = require('./authTokenService');
|
||||
const rpcProviderService = require('./rpcProviderService');
|
||||
|
||||
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
|
||||
|
||||
class AuthService {
|
||||
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),
|
||||
};
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
// Проверка подписи
|
||||
async verifySignature(message, signature, address) {
|
||||
@@ -127,100 +114,86 @@ class AuthService {
|
||||
*/
|
||||
async checkAdminRole(address) {
|
||||
if (!address) return false;
|
||||
|
||||
logger.info(`Checking admin role for address: ${address}`);
|
||||
let foundTokens = false;
|
||||
let errorCount = 0;
|
||||
const balances = {};
|
||||
const totalNetworks = ADMIN_CONTRACTS.length;
|
||||
|
||||
// Создаем массив промисов для параллельной проверки балансов
|
||||
const checkPromises = ADMIN_CONTRACTS.map(async (contract) => {
|
||||
// Получаем токены и RPC из базы
|
||||
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 checkPromises = tokens.map(async (token) => {
|
||||
try {
|
||||
const provider = this.providers[contract.network];
|
||||
if (!provider) {
|
||||
logger.error(`No provider available for network ${contract.network}`);
|
||||
balances[contract.network] = 'Error: No provider';
|
||||
const rpcUrl = rpcMap[token.network];
|
||||
if (!rpcUrl) {
|
||||
logger.error(`No RPC URL for network ${token.network}`);
|
||||
balances[token.network] = 'Error: No RPC URL';
|
||||
errorCount++;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Проверяем доступность провайдера
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
// Проверяем доступность сети с таймаутом
|
||||
try {
|
||||
// Проверка доступности сети с таймаутом
|
||||
const networkCheckPromise = provider.getNetwork();
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Network check timeout')), 3000)
|
||||
);
|
||||
|
||||
await Promise.race([networkCheckPromise, timeoutPromise]);
|
||||
} catch (networkError) {
|
||||
logger.error(
|
||||
`Provider for ${contract.network} is not available: ${networkError.message}`
|
||||
);
|
||||
balances[contract.network] = 'Error: Network unavailable';
|
||||
logger.error(`Provider for ${token.network} is not available: ${networkError.message}`);
|
||||
balances[token.network] = 'Error: Network unavailable';
|
||||
errorCount++;
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenContract = new ethers.Contract(contract.address, ERC20_ABI, provider);
|
||||
|
||||
// Создаем промис с таймаутом
|
||||
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
|
||||
const balancePromise = tokenContract.balanceOf(address);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 3000)
|
||||
);
|
||||
|
||||
// Ждем первый выполненный промис
|
||||
const balance = await Promise.race([balancePromise, timeoutPromise]);
|
||||
const formattedBalance = ethers.formatUnits(balance, 18);
|
||||
balances[contract.network] = formattedBalance;
|
||||
|
||||
logger.info(`Token balance on ${contract.network}:`, {
|
||||
balances[token.network] = formattedBalance;
|
||||
logger.info(`Token balance on ${token.network}:`, {
|
||||
address,
|
||||
contract: contract.address,
|
||||
contract: token.address,
|
||||
balance: formattedBalance,
|
||||
hasTokens: balance > 0,
|
||||
minBalance: token.min_balance,
|
||||
hasTokens: parseFloat(formattedBalance) >= parseFloat(token.min_balance),
|
||||
});
|
||||
|
||||
if (parseFloat(formattedBalance) > 0) {
|
||||
logger.info(`Found admin tokens on ${contract.network}`);
|
||||
if (parseFloat(formattedBalance) >= parseFloat(token.min_balance)) {
|
||||
logger.info(`Found admin tokens on ${token.network}`);
|
||||
foundTokens = true;
|
||||
}
|
||||
|
||||
return { network: contract.network, balance: formattedBalance };
|
||||
return { network: token.network, balance: formattedBalance };
|
||||
} catch (error) {
|
||||
logger.error(`Error checking balance in ${contract.network}:`, {
|
||||
logger.error(`Error checking balance in ${token.network}:`, {
|
||||
address,
|
||||
contract: contract.address,
|
||||
contract: token.address,
|
||||
error: error.message || 'Unknown error',
|
||||
});
|
||||
balances[contract.network] = 'Error';
|
||||
balances[token.network] = 'Error';
|
||||
errorCount++;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Ждем выполнения всех проверок
|
||||
await Promise.all(checkPromises);
|
||||
|
||||
// Если все запросы завершились с ошибкой, считаем, что проверка не удалась
|
||||
if (errorCount === totalNetworks) {
|
||||
if (errorCount === tokens.length) {
|
||||
logger.error(`All network checks for ${address} failed. Cannot verify admin status.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (foundTokens) {
|
||||
logger.info(`Admin role summary for ${address}:`, {
|
||||
networks: Object.keys(balances).filter(
|
||||
(net) => balances[net] > 0 && balances[net] !== 'Error'
|
||||
(net) => parseFloat(balances[net]) > 0 && balances[net] !== 'Error'
|
||||
),
|
||||
balances,
|
||||
});
|
||||
logger.info(`Admin role granted for ${address}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info(`Admin role denied - no tokens found for ${address}`);
|
||||
return false;
|
||||
}
|
||||
@@ -238,6 +211,7 @@ class AuthService {
|
||||
bsc: '0',
|
||||
arbitrum: '0',
|
||||
polygon: '0',
|
||||
sepolia: '0',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -889,6 +863,46 @@ class AuthService {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем и экспортируем единственный экземпляр
|
||||
|
||||
32
backend/services/authTokenService.js
Normal file
32
backend/services/authTokenService.js
Normal 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 };
|
||||
31
backend/services/rpcProviderService.js
Normal file
31
backend/services/rpcProviderService.js
Normal 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 };
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useAuth } from './composables/useAuth';
|
||||
import { fetchTokenBalances } from './services/tokens';
|
||||
@@ -142,6 +142,21 @@
|
||||
if (auth.isAuthenticated.value) {
|
||||
refreshTokenBalances();
|
||||
}
|
||||
|
||||
// Подписываемся на событие изменения настроек аутентификации
|
||||
const unsubscribe = eventBus.on('auth-settings-saved', () => {
|
||||
console.log('[App] Получено событие сохранения настроек аутентификации, обновляем балансы');
|
||||
if (auth.isAuthenticated.value) {
|
||||
refreshTokenBalances();
|
||||
}
|
||||
});
|
||||
|
||||
// Отписываемся при размонтировании компонента
|
||||
onUnmounted(() => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -62,29 +62,16 @@
|
||||
<div v-if="isLoadingTokens" class="token-loading">
|
||||
Загрузка балансов...
|
||||
</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 v-else>
|
||||
<div v-if="tokenBalances.eth" class="token-balance">
|
||||
<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 class="token-balance-header">
|
||||
</div>
|
||||
<div v-if="tokenBalances.bsc" class="token-balance">
|
||||
<span class="token-name">BSC:</span>
|
||||
<span class="token-amount">{{ Number(tokenBalances.bsc).toLocaleString() }}</span>
|
||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.bsc.symbol }}</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 v-for="(token, index) in tokenBalances.data || []" :key="token.tokenAddress ? token.tokenAddress : 'token-' + index" class="token-balance-row">
|
||||
<span class="token-name">{{ token.tokenName }}</span>
|
||||
<span class="token-network">{{ token.network }}</span>
|
||||
<span class="token-amount">{{ isNaN(Number(token.balance)) ? '—' : Number(token.balance).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +83,6 @@
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { TOKEN_CONTRACTS } from '../services/tokens';
|
||||
import { useRouter } from 'vue-router';
|
||||
import eventBus from '../utils/eventBus';
|
||||
|
||||
@@ -346,11 +332,6 @@ h3 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.token-symbol {
|
||||
color: var(--color-text-light);
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.token-no-data,
|
||||
.user-info-empty {
|
||||
color: var(--color-text-light);
|
||||
@@ -432,4 +413,31 @@ h3 {
|
||||
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>
|
||||
@@ -6,34 +6,6 @@ import axios from 'axios';
|
||||
* Предоставляет списки доступных сетей, URL RPC и функции для работы с ними
|
||||
*/
|
||||
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 = [
|
||||
{
|
||||
@@ -96,21 +68,6 @@ export default function useBlockchainNetworks() {
|
||||
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
|
||||
const getChainIdByNetworkId = (networkId) => {
|
||||
for (const group of networkGroups) {
|
||||
@@ -215,16 +172,13 @@ export default function useBlockchainNetworks() {
|
||||
|
||||
return {
|
||||
// Данные
|
||||
defaultRpcUrls,
|
||||
networkGroups,
|
||||
networkEntry,
|
||||
defaultRpcUrlSuggestion,
|
||||
testingRpc,
|
||||
testingRpcId,
|
||||
networks, // Экспортируем плоский список сетей
|
||||
|
||||
// Методы
|
||||
useDefaultRpcUrl,
|
||||
getChainIdByNetworkId,
|
||||
validateAndPrepareNetworkConfig,
|
||||
resetNetworkEntry,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { ref, watch, onUnmounted } from 'vue';
|
||||
import { fetchTokenBalances } from '../services/tokens';
|
||||
import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
|
||||
import eventBus from '../utils/eventBus';
|
||||
|
||||
export function useTokenBalances() {
|
||||
const auth = useAuth(); // Получаем доступ к состоянию аутентификации
|
||||
const tokenBalances = ref({
|
||||
eth: '0',
|
||||
bsc: '0',
|
||||
arbitrum: '0',
|
||||
polygon: '0',
|
||||
});
|
||||
const tokenBalances = ref([]); // теперь массив объектов
|
||||
const isLoadingTokens = ref(false);
|
||||
let balanceUpdateInterval = null;
|
||||
|
||||
const getIdentityValue = (type) => {
|
||||
@@ -23,28 +20,25 @@ export function useTokenBalances() {
|
||||
const walletAddress = auth.address?.value || getIdentityValue('wallet');
|
||||
if (walletAddress) {
|
||||
try {
|
||||
isLoadingTokens.value = true;
|
||||
console.log('[useTokenBalances] Запрос балансов для адреса:', walletAddress);
|
||||
const balances = await fetchTokenBalances(walletAddress);
|
||||
console.log('[useTokenBalances] Полученные балансы:', balances);
|
||||
tokenBalances.value = {
|
||||
eth: balances.eth || '0',
|
||||
bsc: balances.bsc || '0',
|
||||
arbitrum: balances.arbitrum || '0',
|
||||
polygon: balances.polygon || '0',
|
||||
};
|
||||
const response = await fetchTokenBalances(walletAddress);
|
||||
// Ожидаем, что response — это массив объектов
|
||||
tokenBalances.value = Array.isArray(response) ? response : (response?.data || []);
|
||||
console.log('[useTokenBalances] Обновленные балансы:', tokenBalances.value);
|
||||
} catch (error) {
|
||||
console.error('[useTokenBalances] Ошибка при обновлении балансов:', error);
|
||||
// Возможно, стоит сбросить балансы при ошибке
|
||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
||||
tokenBalances.value = [];
|
||||
} finally {
|
||||
isLoadingTokens.value = false;
|
||||
}
|
||||
} else {
|
||||
console.log('[useTokenBalances] Не найден адрес кошелька для запроса балансов.');
|
||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
||||
tokenBalances.value = [];
|
||||
}
|
||||
} else {
|
||||
console.log('[useTokenBalances] Пользователь не аутентифицирован, сброс балансов.');
|
||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
||||
tokenBalances.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,21 +73,31 @@ export function useTokenBalances() {
|
||||
// Если пользователь вышел, отвязал кошелек, или не аутентифицирован
|
||||
stopBalanceUpdates();
|
||||
// Сбрасываем балансы
|
||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
||||
tokenBalances.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true } // Запустить проверку сразу при инициализации
|
||||
);
|
||||
|
||||
// Остановка интервала при размонтировании
|
||||
// Подписываемся на событие для обновления баланса после сохранения настроек
|
||||
const unsubscribe = eventBus.on('auth-settings-saved', () => {
|
||||
console.log('[useTokenBalances] Получено событие сохранения настроек, обновляем балансы');
|
||||
updateBalances();
|
||||
});
|
||||
|
||||
// Остановка интервала и отписки при размонтировании
|
||||
onUnmounted(() => {
|
||||
stopBalanceUpdates();
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
tokenBalances,
|
||||
isLoadingTokens,
|
||||
updateBalances,
|
||||
startBalanceUpdates, // Можно не экспортировать, если управление полностью автоматическое
|
||||
stopBalanceUpdates, // Можно не экспортировать
|
||||
startBalanceUpdates,
|
||||
stopBalanceUpdates,
|
||||
};
|
||||
}
|
||||
@@ -1,29 +1,5 @@
|
||||
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) => {
|
||||
try {
|
||||
@@ -43,6 +19,7 @@ export const fetchTokenBalances = async (address = null) => {
|
||||
bsc: '0',
|
||||
arbitrum: '0',
|
||||
polygon: '0',
|
||||
sepolia: '0',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<div class="setting-form">
|
||||
<p>Здесь будут настройки для конфигурации промптов</p>
|
||||
<textarea v-model="settings.prompt" placeholder="Базовый промпт для ИИ..." rows="5" class="form-control"></textarea>
|
||||
<button class="btn btn-primary" @click="saveSettings('prompt')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +22,6 @@
|
||||
Включить RAG
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('rag')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +42,6 @@
|
||||
Email
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('channels')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +59,6 @@
|
||||
<option value="gpt-4o">GPT-4o</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveSettings('models')">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,10 +165,6 @@ h3 {
|
||||
max-width: 500px; /* Ограничим ширину для select/textarea */
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
|
||||
98
frontend/src/views/settings/AuthTokensSettings.vue
Normal file
98
frontend/src/views/settings/AuthTokensSettings.vue
Normal 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>
|
||||
@@ -235,14 +235,6 @@
|
||||
<button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button>
|
||||
</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. Выбор сети для деплоя -->
|
||||
<h4>Сеть для деплоя</h4>
|
||||
<div class="form-group">
|
||||
@@ -918,20 +910,24 @@ const toggleShowDeployerKey = () => {
|
||||
const loadRpcSettings = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/settings/rpc');
|
||||
console.log('Ответ сервера на /api/settings/rpc:', response.data);
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[BlockchainSettingsView] Ошибка при загрузке RPC конфигураций:', error);
|
||||
// Если нужно, установить дефолтные RPC
|
||||
// setDefaultRpcConfigs();
|
||||
}
|
||||
};
|
||||
|
||||
// Функция сохранения настроек RPC на сервер
|
||||
const saveRpcSettings = async () => {
|
||||
try {
|
||||
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
|
||||
const response = await axios.post('/api/settings/rpc', {
|
||||
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<!-- Добавить другие языки по необходимости -->
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="saveLanguageSetting">Сохранить язык</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
138
frontend/src/views/settings/RpcProvidersSettings.vue
Normal file
138
frontend/src/views/settings/RpcProvidersSettings.vue
Normal 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>
|
||||
@@ -38,134 +38,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Раскрывающиеся настройки RPC -->
|
||||
<div v-if="showRpcSettings" class="detail-panel">
|
||||
<h3>Настройки RPC Провайдеров</h3>
|
||||
<p class="env-note">Эти настройки сохраняются в .env файле на бэкенде.</p>
|
||||
|
||||
<!-- Список добавленных RPC -->
|
||||
<div v-if="securitySettings.rpcConfigs.length > 0" class="configs-list">
|
||||
<h4>Добавленные RPC конфигурации:</h4>
|
||||
<div v-for="(rpc, index) in securitySettings.rpcConfigs" :key="index" class="config-item">
|
||||
<div class="config-details">
|
||||
<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>
|
||||
<RpcProvidersSettings
|
||||
v-if="showRpcSettings"
|
||||
:rpcConfigs="securitySettings.rpcConfigs"
|
||||
@update="loadSettings"
|
||||
@test="testRpcHandler"
|
||||
/>
|
||||
<AuthTokensSettings
|
||||
v-if="showAuthSettings"
|
||||
:authTokens="securitySettings.authTokens"
|
||||
@update="loadSettings"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted, computed } from 'vue';
|
||||
import { reactive, ref, onMounted, computed, provide } from 'vue';
|
||||
import api from '@/api/axios';
|
||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import RpcProvidersSettings from './RpcProvidersSettings.vue';
|
||||
import AuthTokensSettings from './AuthTokensSettings.vue';
|
||||
|
||||
// Состояние для отображения/скрытия дополнительных настроек
|
||||
const showRpcSettings = ref(false);
|
||||
@@ -183,8 +76,6 @@ const securitySettings = reactive({
|
||||
const {
|
||||
networkGroups,
|
||||
networkEntry,
|
||||
defaultRpcUrlSuggestion,
|
||||
useDefaultRpcUrl,
|
||||
validateAndPrepareNetworkConfig,
|
||||
resetNetworkEntry,
|
||||
testRpcConnection,
|
||||
@@ -239,13 +130,22 @@ const loadSettings = async () => {
|
||||
// Загрузка RPC конфигураций
|
||||
const rpcResponse = await api.get('/api/settings/rpc');
|
||||
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');
|
||||
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] Настройки успешно загружены с бэкенда');
|
||||
@@ -263,79 +163,8 @@ const loadSettings = async () => {
|
||||
|
||||
// Установка дефолтных значений
|
||||
const setDefaultSettings = () => {
|
||||
securitySettings.rpcConfigs = [
|
||||
{ networkId: 'ethereum', rpcUrl: 'https://mainnet.infura.io/v3/YOUR_API_KEY' },
|
||||
{ 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);
|
||||
securitySettings.rpcConfigs = [];
|
||||
securitySettings.authTokens = [];
|
||||
};
|
||||
|
||||
// Сохранение всех настроек безопасности на бэкенд
|
||||
@@ -343,8 +172,12 @@ const saveSecuritySettings = async () => {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
// Подготовка данных для отправки
|
||||
const validRpcConfigs = securitySettings.rpcConfigs.filter(
|
||||
c => c.networkId && c.rpcUrl
|
||||
);
|
||||
|
||||
const settingsData = {
|
||||
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs)),
|
||||
rpcConfigs: validRpcConfigs,
|
||||
authTokens: JSON.parse(JSON.stringify(securitySettings.authTokens))
|
||||
};
|
||||
|
||||
@@ -362,6 +195,8 @@ const saveSecuritySettings = async () => {
|
||||
|
||||
if (rpcResponse.data.success && authResponse.data.success) {
|
||||
alert('Все настройки безопасности успешно сохранены.');
|
||||
eventBus.emit('settings-updated');
|
||||
eventBus.emit('auth-settings-saved', { tokens: settingsData.authTokens });
|
||||
} else {
|
||||
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) => {
|
||||
try {
|
||||
const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl);
|
||||
@@ -388,10 +247,47 @@ const testRpcHandler = async (rpc) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Загрузка настроек при монтировании компонента
|
||||
onMounted(() => {
|
||||
loadSettings();
|
||||
});
|
||||
// --- Методы для токенов аутентификации ---
|
||||
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);
|
||||
};
|
||||
|
||||
// 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>
|
||||
|
||||
<style scoped>
|
||||
@@ -609,14 +505,6 @@ small {
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
Reference in New Issue
Block a user