ваше сообщение коммита
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: {
|
networks: {
|
||||||
sepolia: {
|
sepolia: {
|
||||||
url: process.env.ETHEREUM_NETWORK_URL,
|
url: process.env.RPC_URL_ETH,
|
||||||
accounts: [process.env.PRIVATE_KEY],
|
accounts: [process.env.PRIVATE_KEY],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
if (req.session.address) {
|
||||||
const isAdmin = await authService.checkAdminToken(req.session.address);
|
const isAdmin = await authService.checkAdminTokens(req.session.address);
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
// Обновляем сессию
|
// Обновляем сессию
|
||||||
req.session.isAdmin = true;
|
req.session.isAdmin = true;
|
||||||
|
|||||||
@@ -2,52 +2,17 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireAdmin } = require('../middleware/auth');
|
const { requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
|
const rpcProviderService = require('../services/rpcProviderService');
|
||||||
|
const authTokenService = require('../services/authTokenService');
|
||||||
|
|
||||||
// Логируем версию ethers для отладки
|
// Логируем версию ethers для отладки
|
||||||
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
|
||||||
|
|
||||||
// Путь к файлу с настройками
|
|
||||||
const RPC_CONFIG_PATH = path.join(__dirname, '../config/rpc-settings.json');
|
|
||||||
const AUTH_TOKENS_PATH = path.join(__dirname, '../config/auth-tokens.json');
|
|
||||||
|
|
||||||
// Вспомогательная функция для чтения настроек из файла
|
|
||||||
const readSettingsFile = (filePath, defaultValue = []) => {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
const data = fs.readFileSync(filePath, 'utf8');
|
|
||||||
return JSON.parse(data);
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Ошибка при чтении файла настроек ${filePath}:`, error);
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Вспомогательная функция для записи настроек в файл
|
|
||||||
const writeSettingsFile = async (filePath, data) => {
|
|
||||||
try {
|
|
||||||
// Создаем директорию, если не существует
|
|
||||||
const dirname = path.dirname(filePath);
|
|
||||||
if (!fs.existsSync(dirname)) {
|
|
||||||
fs.mkdirSync(dirname, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Ошибка при записи файла настроек ${filePath}:`, error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение RPC настроек
|
// Получение RPC настроек
|
||||||
router.get('/rpc', requireAdmin, async (req, res) => {
|
router.get('/rpc', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rpcConfigs = readSettingsFile(RPC_CONFIG_PATH);
|
const rpcConfigs = await rpcProviderService.getAllRpcProviders();
|
||||||
res.json({ success: true, data: rpcConfigs });
|
res.json({ success: true, data: rpcConfigs });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при получении RPC настроек:', error);
|
logger.error('Ошибка при получении RPC настроек:', error);
|
||||||
@@ -55,32 +20,47 @@ router.get('/rpc', requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сохранение RPC настроек
|
// Добавление/обновление одного или нескольких RPC
|
||||||
router.post('/rpc', requireAdmin, async (req, res) => {
|
router.post('/rpc', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { rpcConfigs } = req.body;
|
// Если пришёл массив rpcConfigs — bulk-режим
|
||||||
|
if (Array.isArray(req.body.rpcConfigs)) {
|
||||||
if (!Array.isArray(rpcConfigs)) {
|
const rpcConfigs = req.body.rpcConfigs;
|
||||||
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
if (!rpcConfigs.length) {
|
||||||
|
return res.status(400).json({ success: false, error: 'rpcConfigs не может быть пустым массивом' });
|
||||||
}
|
}
|
||||||
|
await rpcProviderService.saveAllRpcProviders(rpcConfigs);
|
||||||
const success = await writeSettingsFile(RPC_CONFIG_PATH, rpcConfigs);
|
return res.json({ success: true, message: 'RPC провайдеры успешно сохранены (bulk)' });
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при сохранении RPC настроек:', error);
|
logger.error('Ошибка при сохранении RPC:', error);
|
||||||
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении настроек RPC' });
|
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении RPC' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление одного RPC
|
||||||
|
router.delete('/rpc/:networkId', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { networkId } = req.params;
|
||||||
|
await rpcProviderService.deleteRpcProvider(networkId);
|
||||||
|
res.json({ success: true, message: 'RPC провайдер удалён' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при удалении RPC:', error);
|
||||||
|
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении RPC' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получение токенов для аутентификации
|
// Получение токенов для аутентификации
|
||||||
router.get('/auth-tokens', requireAdmin, async (req, res) => {
|
router.get('/auth-tokens', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const authTokens = readSettingsFile(AUTH_TOKENS_PATH);
|
const authTokens = await authTokenService.getAllAuthTokens();
|
||||||
res.json({ success: true, data: authTokens });
|
res.json({ success: true, data: authTokens });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при получении токенов аутентификации:', error);
|
logger.error('Ошибка при получении токенов аутентификации:', error);
|
||||||
@@ -92,24 +72,44 @@ router.get('/auth-tokens', requireAdmin, async (req, res) => {
|
|||||||
router.post('/auth-tokens', requireAdmin, async (req, res) => {
|
router.post('/auth-tokens', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { authTokens } = req.body;
|
const { authTokens } = req.body;
|
||||||
|
|
||||||
if (!Array.isArray(authTokens)) {
|
if (!Array.isArray(authTokens)) {
|
||||||
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
return res.status(400).json({ success: false, error: 'Неверный формат данных' });
|
||||||
}
|
}
|
||||||
|
await authTokenService.saveAllAuthTokens(authTokens);
|
||||||
const success = await writeSettingsFile(AUTH_TOKENS_PATH, authTokens);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
|
res.json({ success: true, message: 'Токены аутентификации успешно сохранены' });
|
||||||
} else {
|
|
||||||
res.status(500).json({ success: false, error: 'Ошибка при сохранении токенов аутентификации' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при сохранении токенов аутентификации:', error);
|
logger.error('Ошибка при сохранении токенов аутентификации:', error);
|
||||||
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' });
|
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Добавление/обновление одного токена
|
||||||
|
router.post('/auth-token', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, address, network, minBalance } = req.body;
|
||||||
|
if (!name || !address || !network) {
|
||||||
|
return res.status(400).json({ success: false, error: 'name, address и network обязательны' });
|
||||||
|
}
|
||||||
|
await authTokenService.upsertAuthToken({ name, address, network, minBalance });
|
||||||
|
res.json({ success: true, message: 'Токен аутентификации сохранён' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при сохранении токена аутентификации:', error);
|
||||||
|
res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токена' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление одного токена
|
||||||
|
router.delete('/auth-token/:address/:network', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address, network } = req.params;
|
||||||
|
await authTokenService.deleteAuthToken(address, network);
|
||||||
|
res.json({ success: true, message: 'Токен аутентификации удалён' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при удалении токена аутентификации:', error);
|
||||||
|
res.status(500).json({ success: false, error: 'Ошибка сервера при удалении токена' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Тестирование RPC соединения
|
// Тестирование RPC соединения
|
||||||
router.post('/rpc-test', requireAdmin, async (req, res) => {
|
router.post('/rpc-test', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -123,7 +123,12 @@ router.post('/rpc-test', requireAdmin, async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Пробуем создать провайдера и получить номер последнего блока (обновлено для ethers v6)
|
// Пробуем создать провайдера и получить номер последнего блока (обновлено для ethers v6)
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
let provider;
|
||||||
|
if (rpcUrl.startsWith('ws://') || rpcUrl.startsWith('wss://')) {
|
||||||
|
provider = new ethers.WebSocketProvider(rpcUrl);
|
||||||
|
} else {
|
||||||
|
provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// Устанавливаем таймаут для соединения
|
// Устанавливаем таймаут для соединения
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { requireAuth } = require('../middleware/auth');
|
|
||||||
const authService = require('../services/auth-service');
|
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
const authService = require('../services/auth-service');
|
||||||
|
|
||||||
// Получение балансов токенов
|
// Получение балансов токенов пользователя по токенам из базы
|
||||||
router.get('/balances', requireAuth, async (req, res) => {
|
router.get('/balances', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { address } = req.session;
|
const address = req.query.address;
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({ success: false, error: 'Не указан адрес кошелька' });
|
||||||
error: 'No wallet address in session',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Fetching token balances for address: ${address}`);
|
logger.info(`Fetching token balances for address: ${address}`);
|
||||||
const balances = await authService.getTokenBalances(address);
|
const balances = await authService.getUserTokenBalances(address);
|
||||||
|
res.json({ success: true, data: balances });
|
||||||
res.json(balances);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching token balances:', error);
|
logger.error('Error fetching token balances:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({ success: false, error: 'Failed to fetch token balances' });
|
||||||
error: 'Failed to fetch token balances',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,26 +5,13 @@ const crypto = require('crypto');
|
|||||||
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
|
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
|
||||||
const verificationService = require('./verification-service'); // Используем сервис верификации
|
const verificationService = require('./verification-service'); // Используем сервис верификации
|
||||||
const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ
|
const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ
|
||||||
|
const authTokenService = require('./authTokenService');
|
||||||
const ADMIN_CONTRACTS = [
|
const rpcProviderService = require('./rpcProviderService');
|
||||||
{ address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'ethereum' },
|
|
||||||
{ address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc' },
|
|
||||||
{ address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum' },
|
|
||||||
{ address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'polygon' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
|
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
constructor() {
|
constructor() {}
|
||||||
// Используем существующие переменные окружения с префиксом RPC_URL_
|
|
||||||
this.providers = {
|
|
||||||
ethereum: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH), // Используем RPC_URL_ETH для ethereum
|
|
||||||
polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON),
|
|
||||||
bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC),
|
|
||||||
arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка подписи
|
// Проверка подписи
|
||||||
async verifySignature(message, signature, address) {
|
async verifySignature(message, signature, address) {
|
||||||
@@ -127,100 +114,86 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async checkAdminRole(address) {
|
async checkAdminRole(address) {
|
||||||
if (!address) return false;
|
if (!address) return false;
|
||||||
|
|
||||||
logger.info(`Checking admin role for address: ${address}`);
|
logger.info(`Checking admin role for address: ${address}`);
|
||||||
let foundTokens = false;
|
let foundTokens = false;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
const balances = {};
|
const balances = {};
|
||||||
const totalNetworks = ADMIN_CONTRACTS.length;
|
// Получаем токены и RPC из базы
|
||||||
|
const tokens = await authTokenService.getAllAuthTokens();
|
||||||
// Создаем массив промисов для параллельной проверки балансов
|
const rpcProviders = await rpcProviderService.getAllRpcProviders();
|
||||||
const checkPromises = ADMIN_CONTRACTS.map(async (contract) => {
|
const rpcMap = {};
|
||||||
|
for (const rpc of rpcProviders) {
|
||||||
|
rpcMap[rpc.network_id] = rpc.rpc_url;
|
||||||
|
}
|
||||||
|
const checkPromises = tokens.map(async (token) => {
|
||||||
try {
|
try {
|
||||||
const provider = this.providers[contract.network];
|
const rpcUrl = rpcMap[token.network];
|
||||||
if (!provider) {
|
if (!rpcUrl) {
|
||||||
logger.error(`No provider available for network ${contract.network}`);
|
logger.error(`No RPC URL for network ${token.network}`);
|
||||||
balances[contract.network] = 'Error: No provider';
|
balances[token.network] = 'Error: No RPC URL';
|
||||||
errorCount++;
|
errorCount++;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
// Проверяем доступность провайдера
|
// Проверяем доступность сети с таймаутом
|
||||||
try {
|
try {
|
||||||
// Проверка доступности сети с таймаутом
|
|
||||||
const networkCheckPromise = provider.getNetwork();
|
const networkCheckPromise = provider.getNetwork();
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Network check timeout')), 3000)
|
setTimeout(() => reject(new Error('Network check timeout')), 3000)
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.race([networkCheckPromise, timeoutPromise]);
|
await Promise.race([networkCheckPromise, timeoutPromise]);
|
||||||
} catch (networkError) {
|
} catch (networkError) {
|
||||||
logger.error(
|
logger.error(`Provider for ${token.network} is not available: ${networkError.message}`);
|
||||||
`Provider for ${contract.network} is not available: ${networkError.message}`
|
balances[token.network] = 'Error: Network unavailable';
|
||||||
);
|
|
||||||
balances[contract.network] = 'Error: Network unavailable';
|
|
||||||
errorCount++;
|
errorCount++;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
|
||||||
const tokenContract = new ethers.Contract(contract.address, ERC20_ABI, provider);
|
|
||||||
|
|
||||||
// Создаем промис с таймаутом
|
|
||||||
const balancePromise = tokenContract.balanceOf(address);
|
const balancePromise = tokenContract.balanceOf(address);
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('Timeout')), 3000)
|
setTimeout(() => reject(new Error('Timeout')), 3000)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ждем первый выполненный промис
|
|
||||||
const balance = await Promise.race([balancePromise, timeoutPromise]);
|
const balance = await Promise.race([balancePromise, timeoutPromise]);
|
||||||
const formattedBalance = ethers.formatUnits(balance, 18);
|
const formattedBalance = ethers.formatUnits(balance, 18);
|
||||||
balances[contract.network] = formattedBalance;
|
balances[token.network] = formattedBalance;
|
||||||
|
logger.info(`Token balance on ${token.network}:`, {
|
||||||
logger.info(`Token balance on ${contract.network}:`, {
|
|
||||||
address,
|
address,
|
||||||
contract: contract.address,
|
contract: token.address,
|
||||||
balance: formattedBalance,
|
balance: formattedBalance,
|
||||||
hasTokens: balance > 0,
|
minBalance: token.min_balance,
|
||||||
|
hasTokens: parseFloat(formattedBalance) >= parseFloat(token.min_balance),
|
||||||
});
|
});
|
||||||
|
if (parseFloat(formattedBalance) >= parseFloat(token.min_balance)) {
|
||||||
if (parseFloat(formattedBalance) > 0) {
|
logger.info(`Found admin tokens on ${token.network}`);
|
||||||
logger.info(`Found admin tokens on ${contract.network}`);
|
|
||||||
foundTokens = true;
|
foundTokens = true;
|
||||||
}
|
}
|
||||||
|
return { network: token.network, balance: formattedBalance };
|
||||||
return { network: contract.network, balance: formattedBalance };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error checking balance in ${contract.network}:`, {
|
logger.error(`Error checking balance in ${token.network}:`, {
|
||||||
address,
|
address,
|
||||||
contract: contract.address,
|
contract: token.address,
|
||||||
error: error.message || 'Unknown error',
|
error: error.message || 'Unknown error',
|
||||||
});
|
});
|
||||||
balances[contract.network] = 'Error';
|
balances[token.network] = 'Error';
|
||||||
errorCount++;
|
errorCount++;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ждем выполнения всех проверок
|
|
||||||
await Promise.all(checkPromises);
|
await Promise.all(checkPromises);
|
||||||
|
if (errorCount === tokens.length) {
|
||||||
// Если все запросы завершились с ошибкой, считаем, что проверка не удалась
|
|
||||||
if (errorCount === totalNetworks) {
|
|
||||||
logger.error(`All network checks for ${address} failed. Cannot verify admin status.`);
|
logger.error(`All network checks for ${address} failed. Cannot verify admin status.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundTokens) {
|
if (foundTokens) {
|
||||||
logger.info(`Admin role summary for ${address}:`, {
|
logger.info(`Admin role summary for ${address}:`, {
|
||||||
networks: Object.keys(balances).filter(
|
networks: Object.keys(balances).filter(
|
||||||
(net) => balances[net] > 0 && balances[net] !== 'Error'
|
(net) => parseFloat(balances[net]) > 0 && balances[net] !== 'Error'
|
||||||
),
|
),
|
||||||
balances,
|
balances,
|
||||||
});
|
});
|
||||||
logger.info(`Admin role granted for ${address}`);
|
logger.info(`Admin role granted for ${address}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Admin role denied - no tokens found for ${address}`);
|
logger.info(`Admin role denied - no tokens found for ${address}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -238,6 +211,7 @@ class AuthService {
|
|||||||
bsc: '0',
|
bsc: '0',
|
||||||
arbitrum: '0',
|
arbitrum: '0',
|
||||||
polygon: '0',
|
polygon: '0',
|
||||||
|
sepolia: '0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,6 +863,46 @@ class AuthService {
|
|||||||
throw new Error('Ошибка обработки верификации Email');
|
throw new Error('Ошибка обработки верификации Email');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение балансов токенов пользователя только по токенам из базы
|
||||||
|
* @param {string} address - адрес кошелька
|
||||||
|
* @returns {Promise<Array>} - массив объектов с балансами
|
||||||
|
*/
|
||||||
|
async getUserTokenBalances(address) {
|
||||||
|
if (!address) return [];
|
||||||
|
const tokens = await authTokenService.getAllAuthTokens();
|
||||||
|
const rpcProviders = await rpcProviderService.getAllRpcProviders();
|
||||||
|
const rpcMap = {};
|
||||||
|
for (const rpc of rpcProviders) {
|
||||||
|
rpcMap[rpc.network_id] = rpc.rpc_url;
|
||||||
|
}
|
||||||
|
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
|
||||||
|
const results = [];
|
||||||
|
for (const token of tokens) {
|
||||||
|
const rpcUrl = rpcMap[token.network];
|
||||||
|
if (!rpcUrl) continue;
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
|
||||||
|
let balance = '0';
|
||||||
|
try {
|
||||||
|
const rawBalance = await tokenContract.balanceOf(address);
|
||||||
|
balance = ethers.formatUnits(rawBalance, 18);
|
||||||
|
if (!balance || isNaN(Number(balance))) balance = '0';
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`[getUserTokenBalances] Ошибка получения баланса для ${token.name} (${token.address}) в сети ${token.network}:`, e);
|
||||||
|
balance = '0';
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
network: token.network,
|
||||||
|
tokenAddress: token.address,
|
||||||
|
tokenName: token.name,
|
||||||
|
symbol: token.symbol || '',
|
||||||
|
balance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем и экспортируем единственный экземпляр
|
// Создаем и экспортируем единственный экземпляр
|
||||||
|
|||||||
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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, computed } from 'vue';
|
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import { useAuth } from './composables/useAuth';
|
import { useAuth } from './composables/useAuth';
|
||||||
import { fetchTokenBalances } from './services/tokens';
|
import { fetchTokenBalances } from './services/tokens';
|
||||||
@@ -142,6 +142,21 @@
|
|||||||
if (auth.isAuthenticated.value) {
|
if (auth.isAuthenticated.value) {
|
||||||
refreshTokenBalances();
|
refreshTokenBalances();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подписываемся на событие изменения настроек аутентификации
|
||||||
|
const unsubscribe = eventBus.on('auth-settings-saved', () => {
|
||||||
|
console.log('[App] Получено событие сохранения настроек аутентификации, обновляем балансы');
|
||||||
|
if (auth.isAuthenticated.value) {
|
||||||
|
refreshTokenBalances();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отписываемся при размонтировании компонента
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unsubscribe) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -62,29 +62,16 @@
|
|||||||
<div v-if="isLoadingTokens" class="token-loading">
|
<div v-if="isLoadingTokens" class="token-loading">
|
||||||
Загрузка балансов...
|
Загрузка балансов...
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!tokenBalances || Object.keys(tokenBalances).length === 0" class="token-no-data">
|
<div v-else-if="!tokenBalances || tokenBalances.length === 0" class="token-no-data">
|
||||||
Баланс не доступен
|
Баланс не доступен
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-if="tokenBalances.eth" class="token-balance">
|
<div class="token-balance-header">
|
||||||
<span class="token-name">ETH:</span>
|
|
||||||
<span class="token-amount">{{ Number(tokenBalances.eth).toLocaleString() }}</span>
|
|
||||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.eth.symbol }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tokenBalances.bsc" class="token-balance">
|
<div v-for="(token, index) in tokenBalances.data || []" :key="token.tokenAddress ? token.tokenAddress : 'token-' + index" class="token-balance-row">
|
||||||
<span class="token-name">BSC:</span>
|
<span class="token-name">{{ token.tokenName }}</span>
|
||||||
<span class="token-amount">{{ Number(tokenBalances.bsc).toLocaleString() }}</span>
|
<span class="token-network">{{ token.network }}</span>
|
||||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.bsc.symbol }}</span>
|
<span class="token-amount">{{ isNaN(Number(token.balance)) ? '—' : Number(token.balance).toLocaleString() }}</span>
|
||||||
</div>
|
|
||||||
<div v-if="tokenBalances.arbitrum" class="token-balance">
|
|
||||||
<span class="token-name">ARB:</span>
|
|
||||||
<span class="token-amount">{{ Number(tokenBalances.arbitrum).toLocaleString() }}</span>
|
|
||||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.arbitrum.symbol }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="tokenBalances.polygon" class="token-balance">
|
|
||||||
<span class="token-name">POL:</span>
|
|
||||||
<span class="token-amount">{{ Number(tokenBalances.polygon).toLocaleString() }}</span>
|
|
||||||
<span class="token-symbol">{{ TOKEN_CONTRACTS.polygon.symbol }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +83,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import { TOKEN_CONTRACTS } from '../services/tokens';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
|
|
||||||
@@ -346,11 +332,6 @@ h3 {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-symbol {
|
|
||||||
color: var(--color-text-light);
|
|
||||||
margin-left: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-no-data,
|
.token-no-data,
|
||||||
.user-info-empty {
|
.user-info-empty {
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
@@ -432,4 +413,31 @@ h3 {
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.token-balance-header {
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-primary, #4caf50);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.token-balance-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.token-name {
|
||||||
|
min-width: 80px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.token-network {
|
||||||
|
min-width: 70px;
|
||||||
|
color: var(--color-dark, #333);
|
||||||
|
}
|
||||||
|
.token-amount {
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -6,34 +6,6 @@ import axios from 'axios';
|
|||||||
* Предоставляет списки доступных сетей, URL RPC и функции для работы с ними
|
* Предоставляет списки доступных сетей, URL RPC и функции для работы с ними
|
||||||
*/
|
*/
|
||||||
export default function useBlockchainNetworks() {
|
export default function useBlockchainNetworks() {
|
||||||
// Список стандартных URL для популярных сетей
|
|
||||||
const defaultRpcUrls = {
|
|
||||||
ethereum: 'https://mainnet.infura.io/v3/YOUR_API_KEY',
|
|
||||||
sepolia: 'https://sepolia.infura.io/v3/YOUR_API_KEY',
|
|
||||||
goerli: 'https://goerli.infura.io/v3/YOUR_API_KEY',
|
|
||||||
holesky: 'https://holesky.infura.io/v3/YOUR_API_KEY',
|
|
||||||
bsc: 'https://bsc-dataseed1.binance.org',
|
|
||||||
'bsc-testnet': 'https://data-seed-prebsc-1-s1.binance.org:8545',
|
|
||||||
polygon: 'https://polygon-rpc.com',
|
|
||||||
mumbai: 'https://rpc-mumbai.maticvigil.com',
|
|
||||||
arbitrum: 'https://arb1.arbitrum.io/rpc',
|
|
||||||
'arbitrum-goerli': 'https://goerli-rollup.arbitrum.io/rpc',
|
|
||||||
optimism: 'https://mainnet.optimism.io',
|
|
||||||
'optimism-goerli': 'https://goerli.optimism.io',
|
|
||||||
avalanche: 'https://api.avax.network/ext/bc/C/rpc',
|
|
||||||
'avalanche-fuji': 'https://api.avax-test.network/ext/bc/C/rpc',
|
|
||||||
gnosis: 'https://rpc.gnosischain.com',
|
|
||||||
celo: 'https://forno.celo.org',
|
|
||||||
fantom: 'https://rpc.ftm.tools',
|
|
||||||
'fantom-testnet': 'https://rpc.testnet.fantom.network',
|
|
||||||
harmony: 'https://api.harmony.one',
|
|
||||||
metis: 'https://andromeda.metis.io/?owner=1088',
|
|
||||||
aurora: 'https://mainnet.aurora.dev',
|
|
||||||
cronos: 'https://evm.cronos.org',
|
|
||||||
localhost: 'http://localhost:8545',
|
|
||||||
ganache: 'http://localhost:7545'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Группы сетей для отображения в интерфейсе
|
// Группы сетей для отображения в интерфейсе
|
||||||
const networkGroups = [
|
const networkGroups = [
|
||||||
{
|
{
|
||||||
@@ -96,21 +68,6 @@ export default function useBlockchainNetworks() {
|
|||||||
customChainId: null
|
customChainId: null
|
||||||
});
|
});
|
||||||
|
|
||||||
// Вычисляемое свойство для предложения URL на основе выбранной сети
|
|
||||||
const defaultRpcUrlSuggestion = computed(() => {
|
|
||||||
if (networkEntry.value.networkId && defaultRpcUrls[networkEntry.value.networkId]) {
|
|
||||||
return defaultRpcUrls[networkEntry.value.networkId];
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция для использования предложенного URL
|
|
||||||
const useDefaultRpcUrl = () => {
|
|
||||||
if (defaultRpcUrlSuggestion.value) {
|
|
||||||
networkEntry.value.rpcUrl = defaultRpcUrlSuggestion.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для получения chainId по networkId
|
// Функция для получения chainId по networkId
|
||||||
const getChainIdByNetworkId = (networkId) => {
|
const getChainIdByNetworkId = (networkId) => {
|
||||||
for (const group of networkGroups) {
|
for (const group of networkGroups) {
|
||||||
@@ -215,16 +172,13 @@ export default function useBlockchainNetworks() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
// Данные
|
// Данные
|
||||||
defaultRpcUrls,
|
|
||||||
networkGroups,
|
networkGroups,
|
||||||
networkEntry,
|
networkEntry,
|
||||||
defaultRpcUrlSuggestion,
|
|
||||||
testingRpc,
|
testingRpc,
|
||||||
testingRpcId,
|
testingRpcId,
|
||||||
networks, // Экспортируем плоский список сетей
|
networks, // Экспортируем плоский список сетей
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
useDefaultRpcUrl,
|
|
||||||
getChainIdByNetworkId,
|
getChainIdByNetworkId,
|
||||||
validateAndPrepareNetworkConfig,
|
validateAndPrepareNetworkConfig,
|
||||||
resetNetworkEntry,
|
resetNetworkEntry,
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { ref, watch, onUnmounted } from 'vue';
|
import { ref, watch, onUnmounted } from 'vue';
|
||||||
import { fetchTokenBalances } from '../services/tokens';
|
import { fetchTokenBalances } from '../services/tokens';
|
||||||
import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
|
import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
|
||||||
|
import eventBus from '../utils/eventBus';
|
||||||
|
|
||||||
export function useTokenBalances() {
|
export function useTokenBalances() {
|
||||||
const auth = useAuth(); // Получаем доступ к состоянию аутентификации
|
const auth = useAuth(); // Получаем доступ к состоянию аутентификации
|
||||||
const tokenBalances = ref({
|
const tokenBalances = ref([]); // теперь массив объектов
|
||||||
eth: '0',
|
const isLoadingTokens = ref(false);
|
||||||
bsc: '0',
|
|
||||||
arbitrum: '0',
|
|
||||||
polygon: '0',
|
|
||||||
});
|
|
||||||
let balanceUpdateInterval = null;
|
let balanceUpdateInterval = null;
|
||||||
|
|
||||||
const getIdentityValue = (type) => {
|
const getIdentityValue = (type) => {
|
||||||
@@ -23,28 +20,25 @@ export function useTokenBalances() {
|
|||||||
const walletAddress = auth.address?.value || getIdentityValue('wallet');
|
const walletAddress = auth.address?.value || getIdentityValue('wallet');
|
||||||
if (walletAddress) {
|
if (walletAddress) {
|
||||||
try {
|
try {
|
||||||
|
isLoadingTokens.value = true;
|
||||||
console.log('[useTokenBalances] Запрос балансов для адреса:', walletAddress);
|
console.log('[useTokenBalances] Запрос балансов для адреса:', walletAddress);
|
||||||
const balances = await fetchTokenBalances(walletAddress);
|
const response = await fetchTokenBalances(walletAddress);
|
||||||
console.log('[useTokenBalances] Полученные балансы:', balances);
|
// Ожидаем, что response — это массив объектов
|
||||||
tokenBalances.value = {
|
tokenBalances.value = Array.isArray(response) ? response : (response?.data || []);
|
||||||
eth: balances.eth || '0',
|
|
||||||
bsc: balances.bsc || '0',
|
|
||||||
arbitrum: balances.arbitrum || '0',
|
|
||||||
polygon: balances.polygon || '0',
|
|
||||||
};
|
|
||||||
console.log('[useTokenBalances] Обновленные балансы:', tokenBalances.value);
|
console.log('[useTokenBalances] Обновленные балансы:', tokenBalances.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[useTokenBalances] Ошибка при обновлении балансов:', error);
|
console.error('[useTokenBalances] Ошибка при обновлении балансов:', error);
|
||||||
// Возможно, стоит сбросить балансы при ошибке
|
tokenBalances.value = [];
|
||||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
} finally {
|
||||||
|
isLoadingTokens.value = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[useTokenBalances] Не найден адрес кошелька для запроса балансов.');
|
console.log('[useTokenBalances] Не найден адрес кошелька для запроса балансов.');
|
||||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
tokenBalances.value = [];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[useTokenBalances] Пользователь не аутентифицирован, сброс балансов.');
|
console.log('[useTokenBalances] Пользователь не аутентифицирован, сброс балансов.');
|
||||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
tokenBalances.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,21 +73,31 @@ export function useTokenBalances() {
|
|||||||
// Если пользователь вышел, отвязал кошелек, или не аутентифицирован
|
// Если пользователь вышел, отвязал кошелек, или не аутентифицирован
|
||||||
stopBalanceUpdates();
|
stopBalanceUpdates();
|
||||||
// Сбрасываем балансы
|
// Сбрасываем балансы
|
||||||
tokenBalances.value = { eth: '0', bsc: '0', arbitrum: '0', polygon: '0' };
|
tokenBalances.value = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true } // Запустить проверку сразу при инициализации
|
{ immediate: true } // Запустить проверку сразу при инициализации
|
||||||
);
|
);
|
||||||
|
|
||||||
// Остановка интервала при размонтировании
|
// Подписываемся на событие для обновления баланса после сохранения настроек
|
||||||
|
const unsubscribe = eventBus.on('auth-settings-saved', () => {
|
||||||
|
console.log('[useTokenBalances] Получено событие сохранения настроек, обновляем балансы');
|
||||||
|
updateBalances();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Остановка интервала и отписки при размонтировании
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopBalanceUpdates();
|
stopBalanceUpdates();
|
||||||
|
if (unsubscribe) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokenBalances,
|
tokenBalances,
|
||||||
|
isLoadingTokens,
|
||||||
updateBalances,
|
updateBalances,
|
||||||
startBalanceUpdates, // Можно не экспортировать, если управление полностью автоматическое
|
startBalanceUpdates,
|
||||||
stopBalanceUpdates, // Можно не экспортировать
|
stopBalanceUpdates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,5 @@
|
|||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
|
|
||||||
// Адреса смарт-контрактов токенов HB3A
|
|
||||||
export const TOKEN_CONTRACTS = {
|
|
||||||
eth: {
|
|
||||||
address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c',
|
|
||||||
symbol: 'HB3A',
|
|
||||||
network: 'Ethereum',
|
|
||||||
},
|
|
||||||
bsc: {
|
|
||||||
address: '0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D',
|
|
||||||
symbol: 'HB3A',
|
|
||||||
network: 'BSC',
|
|
||||||
},
|
|
||||||
arbitrum: {
|
|
||||||
address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0',
|
|
||||||
symbol: 'HB3A',
|
|
||||||
network: 'Arbitrum',
|
|
||||||
},
|
|
||||||
polygon: {
|
|
||||||
address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d',
|
|
||||||
symbol: 'HB3A',
|
|
||||||
network: 'Polygon',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получение балансов токенов
|
// Получение балансов токенов
|
||||||
export const fetchTokenBalances = async (address = null) => {
|
export const fetchTokenBalances = async (address = null) => {
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +19,7 @@ export const fetchTokenBalances = async (address = null) => {
|
|||||||
bsc: '0',
|
bsc: '0',
|
||||||
arbitrum: '0',
|
arbitrum: '0',
|
||||||
polygon: '0',
|
polygon: '0',
|
||||||
|
sepolia: '0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<div class="setting-form">
|
<div class="setting-form">
|
||||||
<p>Здесь будут настройки для конфигурации промптов</p>
|
<p>Здесь будут настройки для конфигурации промптов</p>
|
||||||
<textarea v-model="settings.prompt" placeholder="Базовый промпт для ИИ..." rows="5" class="form-control"></textarea>
|
<textarea v-model="settings.prompt" placeholder="Базовый промпт для ИИ..." rows="5" class="form-control"></textarea>
|
||||||
<button class="btn btn-primary" @click="saveSettings('prompt')">Сохранить</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
Включить RAG
|
Включить RAG
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" @click="saveSettings('rag')">Сохранить</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,7 +42,6 @@
|
|||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" @click="saveSettings('channels')">Сохранить</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +59,6 @@
|
|||||||
<option value="gpt-4o">GPT-4o</option>
|
<option value="gpt-4o">GPT-4o</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" @click="saveSettings('models')">Сохранить</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,10 +165,6 @@ h3 {
|
|||||||
max-width: 500px; /* Ограничим ширину для select/textarea */
|
max-width: 500px; /* Ограничим ширину для select/textarea */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
|
|||||||
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>
|
<button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Кнопка сохранения настроек RPC -->
|
|
||||||
<div class="save-rpc-actions mt-3">
|
|
||||||
<button class="btn btn-primary" @click="saveRpcSettingsWithFeedback" :disabled="isSavingRpc">
|
|
||||||
<i class="fas" :class="isSavingRpc ? 'fa-spinner fa-spin' : 'fa-save'"></i>
|
|
||||||
{{ isSavingRpc ? 'Сохранение...' : 'Сохранить RPC настройки' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 8. Выбор сети для деплоя -->
|
<!-- 8. Выбор сети для деплоя -->
|
||||||
<h4>Сеть для деплоя</h4>
|
<h4>Сеть для деплоя</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -918,20 +910,24 @@ const toggleShowDeployerKey = () => {
|
|||||||
const loadRpcSettings = async () => {
|
const loadRpcSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/settings/rpc');
|
const response = await axios.get('/api/settings/rpc');
|
||||||
|
console.log('Ответ сервера на /api/settings/rpc:', response.data);
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
securitySettings.rpcConfigs = response.data.data || [];
|
securitySettings.rpcConfigs = (response.data.data || []).map(rpc => ({
|
||||||
|
networkId: rpc.network_id,
|
||||||
|
rpcUrl: rpc.rpc_url,
|
||||||
|
chainId: rpc.chain_id
|
||||||
|
}));
|
||||||
console.log('[BlockchainSettingsView] RPC конфигурации успешно загружены:', securitySettings.rpcConfigs);
|
console.log('[BlockchainSettingsView] RPC конфигурации успешно загружены:', securitySettings.rpcConfigs);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BlockchainSettingsView] Ошибка при загрузке RPC конфигураций:', error);
|
console.error('[BlockchainSettingsView] Ошибка при загрузке RPC конфигураций:', error);
|
||||||
// Если нужно, установить дефолтные RPC
|
|
||||||
// setDefaultRpcConfigs();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция сохранения настроек RPC на сервер
|
// Функция сохранения настроек RPC на сервер
|
||||||
const saveRpcSettings = async () => {
|
const saveRpcSettings = async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
|
||||||
const response = await axios.post('/api/settings/rpc', {
|
const response = await axios.post('/api/settings/rpc', {
|
||||||
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
|
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<!-- Добавить другие языки по необходимости -->
|
<!-- Добавить другие языки по необходимости -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" @click="saveLanguageSetting">Сохранить язык</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Раскрывающиеся настройки RPC -->
|
<RpcProvidersSettings
|
||||||
<div v-if="showRpcSettings" class="detail-panel">
|
v-if="showRpcSettings"
|
||||||
<h3>Настройки RPC Провайдеров</h3>
|
:rpcConfigs="securitySettings.rpcConfigs"
|
||||||
<p class="env-note">Эти настройки сохраняются в .env файле на бэкенде.</p>
|
@update="loadSettings"
|
||||||
|
@test="testRpcHandler"
|
||||||
<!-- Список добавленных RPC -->
|
/>
|
||||||
<div v-if="securitySettings.rpcConfigs.length > 0" class="configs-list">
|
<AuthTokensSettings
|
||||||
<h4>Добавленные RPC конфигурации:</h4>
|
v-if="showAuthSettings"
|
||||||
<div v-for="(rpc, index) in securitySettings.rpcConfigs" :key="index" class="config-item">
|
:authTokens="securitySettings.authTokens"
|
||||||
<div class="config-details">
|
@update="loadSettings"
|
||||||
<div><strong>ID Сети:</strong> {{ rpc.networkId }}</div>
|
/>
|
||||||
<div><strong>URL:</strong> {{ rpc.rpcUrl }}</div>
|
|
||||||
<div v-if="rpc.chainId"><strong>Chain ID:</strong> {{ rpc.chainId }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="config-actions">
|
|
||||||
<button class="btn btn-info" @click="testRpcHandler(rpc)" :disabled="testingRpc && testingRpcId === rpc.networkId">
|
|
||||||
<i class="fas" :class="testingRpc && testingRpcId === rpc.networkId ? 'fa-spinner fa-spin' : 'fa-check-circle'"></i>
|
|
||||||
{{ testingRpc && testingRpcId === rpc.networkId ? 'Проверка...' : 'Тест' }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" @click="removeRpcConfig(index)">
|
|
||||||
<i class="fas fa-trash"></i> Удалить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else>Нет добавленных RPC конфигураций.</p>
|
|
||||||
|
|
||||||
<!-- Форма добавления нового RPC -->
|
|
||||||
<div class="setting-form add-form">
|
|
||||||
<h4>Добавить новую RPC конфигурацию:</h4>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="newRpcNetworkId">ID Сети:</label>
|
|
||||||
<select id="newRpcNetworkId" v-model="networkEntry.networkId" class="form-control">
|
|
||||||
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
|
|
||||||
<option v-for="option in group.options" :key="option.value" :value="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
<div v-if="networkEntry.networkId === 'custom'" class="mt-2">
|
|
||||||
<label class="form-label" for="customNetworkId">Пользовательский ID:</label>
|
|
||||||
<input type="text" id="customNetworkId" v-model="networkEntry.customNetworkId" class="form-control" placeholder="Введите ID сети">
|
|
||||||
|
|
||||||
<label class="form-label mt-2" for="customChainId">Chain ID:</label>
|
|
||||||
<input type="number" id="customChainId" v-model="networkEntry.customChainId" class="form-control" placeholder="Например, 1 для Ethereum">
|
|
||||||
<small>Chain ID - уникальный идентификатор блокчейн-сети (целое число)</small>
|
|
||||||
</div>
|
|
||||||
<small>ID сети должен совпадать со значением в выпадающем списке сетей при создании DLE</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="newRpcUrl">RPC URL:</label>
|
|
||||||
<input type="text" id="newRpcUrl" v-model="networkEntry.rpcUrl" class="form-control" placeholder="https://...">
|
|
||||||
<!-- Предложение URL на основе выбранной сети -->
|
|
||||||
<small v-if="defaultRpcUrlSuggestion" class="suggestion">
|
|
||||||
Предложение: {{ defaultRpcUrlSuggestion }}
|
|
||||||
<button class="btn-link" @click="useDefaultRpcUrl">Использовать</button>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Раскрывающиеся настройки Аутентификации -->
|
|
||||||
<div v-if="showAuthSettings" class="detail-panel">
|
|
||||||
<h3>Настройки Аутентификации</h3>
|
|
||||||
<p class="env-note">Эти настройки сохраняются в .env файле на бэкенде.</p>
|
|
||||||
|
|
||||||
<!-- Список токенов для проверки баланса -->
|
|
||||||
<div v-if="securitySettings.authTokens.length > 0" class="configs-list">
|
|
||||||
<h4>Токены для проверки при авторизации:</h4>
|
|
||||||
<div v-for="(token, index) in securitySettings.authTokens" :key="index" class="config-item">
|
|
||||||
<div class="config-details">
|
|
||||||
<div><strong>Название:</strong> {{ token.name }}</div>
|
|
||||||
<div><strong>Адрес:</strong> {{ token.address }}</div>
|
|
||||||
<div><strong>Сеть:</strong> {{ token.network }}</div>
|
|
||||||
<div><strong>Мин. баланс:</strong> {{ token.minBalance }}</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-danger" @click="removeAuthToken(index)">
|
|
||||||
<i class="fas fa-trash"></i> Удалить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else>Нет добавленных токенов для проверки при авторизации.</p>
|
|
||||||
|
|
||||||
<!-- Форма добавления нового токена для авторизации -->
|
|
||||||
<div class="setting-form add-form">
|
|
||||||
<h4>Добавить новый токен для проверки при авторизации:</h4>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="newTokenName">Название токена:</label>
|
|
||||||
<input type="text" id="newTokenName" v-model="newAuthToken.name" class="form-control" placeholder="Например, MyToken">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="newTokenAddress">Адрес контракта:</label>
|
|
||||||
<input type="text" id="newTokenAddress" v-model="newAuthToken.address" class="form-control" placeholder="0x...">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="newTokenNetwork">Сеть:</label>
|
|
||||||
<select id="newTokenNetwork" v-model="newAuthToken.network" class="form-control">
|
|
||||||
<option v-for="option in networkOptions" :key="option.value" :value="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="newTokenMinBalance">Минимальный баланс:</label>
|
|
||||||
<input type="text" id="newTokenMinBalance" v-model="newAuthToken.minBalance" class="form-control" placeholder="1.0">
|
|
||||||
<small>Минимальный баланс токена для успешной авторизации</small>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary" @click="addAuthToken">Добавить токен</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sub-settings-panel save-panel">
|
|
||||||
<button class="btn btn-primary btn-lg" @click="saveSecuritySettings" :disabled="isSaving">
|
|
||||||
<span v-if="isSaving">
|
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
|
||||||
Сохранение...
|
|
||||||
</span>
|
|
||||||
<span v-else>Сохранить все настройки</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted, computed } from 'vue';
|
import { reactive, ref, onMounted, computed, provide } from 'vue';
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
|
||||||
|
import eventBus from '@/utils/eventBus';
|
||||||
|
import RpcProvidersSettings from './RpcProvidersSettings.vue';
|
||||||
|
import AuthTokensSettings from './AuthTokensSettings.vue';
|
||||||
|
|
||||||
// Состояние для отображения/скрытия дополнительных настроек
|
// Состояние для отображения/скрытия дополнительных настроек
|
||||||
const showRpcSettings = ref(false);
|
const showRpcSettings = ref(false);
|
||||||
@@ -183,8 +76,6 @@ const securitySettings = reactive({
|
|||||||
const {
|
const {
|
||||||
networkGroups,
|
networkGroups,
|
||||||
networkEntry,
|
networkEntry,
|
||||||
defaultRpcUrlSuggestion,
|
|
||||||
useDefaultRpcUrl,
|
|
||||||
validateAndPrepareNetworkConfig,
|
validateAndPrepareNetworkConfig,
|
||||||
resetNetworkEntry,
|
resetNetworkEntry,
|
||||||
testRpcConnection,
|
testRpcConnection,
|
||||||
@@ -239,13 +130,22 @@ const loadSettings = async () => {
|
|||||||
// Загрузка RPC конфигураций
|
// Загрузка RPC конфигураций
|
||||||
const rpcResponse = await api.get('/api/settings/rpc');
|
const rpcResponse = await api.get('/api/settings/rpc');
|
||||||
if (rpcResponse.data && rpcResponse.data.success) {
|
if (rpcResponse.data && rpcResponse.data.success) {
|
||||||
securitySettings.rpcConfigs = rpcResponse.data.data || [];
|
securitySettings.rpcConfigs = (rpcResponse.data.data || []).map(rpc => ({
|
||||||
|
networkId: rpc.network_id,
|
||||||
|
rpcUrl: rpc.rpc_url,
|
||||||
|
chainId: rpc.chain_id
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка токенов для аутентификации
|
// Загрузка токенов для аутентификации
|
||||||
const authResponse = await api.get('/api/settings/auth-tokens');
|
const authResponse = await api.get('/api/settings/auth-tokens');
|
||||||
if (authResponse.data && authResponse.data.success) {
|
if (authResponse.data && authResponse.data.success) {
|
||||||
securitySettings.authTokens = authResponse.data.data || [];
|
securitySettings.authTokens = (authResponse.data.data || []).map(token => ({
|
||||||
|
name: token.name,
|
||||||
|
address: token.address,
|
||||||
|
network: token.network,
|
||||||
|
minBalance: token.min_balance
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[SecuritySettingsView] Настройки успешно загружены с бэкенда');
|
console.log('[SecuritySettingsView] Настройки успешно загружены с бэкенда');
|
||||||
@@ -263,79 +163,8 @@ const loadSettings = async () => {
|
|||||||
|
|
||||||
// Установка дефолтных значений
|
// Установка дефолтных значений
|
||||||
const setDefaultSettings = () => {
|
const setDefaultSettings = () => {
|
||||||
securitySettings.rpcConfigs = [
|
securitySettings.rpcConfigs = [];
|
||||||
{ networkId: 'ethereum', rpcUrl: 'https://mainnet.infura.io/v3/YOUR_API_KEY' },
|
securitySettings.authTokens = [];
|
||||||
{ networkId: 'sepolia', rpcUrl: 'https://sepolia.infura.io/v3/YOUR_API_KEY' },
|
|
||||||
{ networkId: 'bsc', rpcUrl: 'https://bsc-dataseed1.binance.org' },
|
|
||||||
{ networkId: 'arbitrum', rpcUrl: 'https://arb1.arbitrum.io/rpc' },
|
|
||||||
{ networkId: 'polygon', rpcUrl: 'https://polygon-rpc.com' }
|
|
||||||
];
|
|
||||||
|
|
||||||
securitySettings.authTokens = [
|
|
||||||
{ name: 'Ethereum Token', address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', network: 'eth', minBalance: '1.0' },
|
|
||||||
{ name: 'BSC Token', address: '0x4B294265720B09ca39BFBA18c7E368413c0f68eB', network: 'bsc', minBalance: '10.0' },
|
|
||||||
{ name: 'Arbitrum Token', address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0', network: 'arbitrum', minBalance: '0.5' },
|
|
||||||
{ name: 'Custom Token', address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', network: 'eth', minBalance: '5.0' }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция добавления новой RPC конфигурации
|
|
||||||
const addRpcConfig = () => {
|
|
||||||
const result = validateAndPrepareNetworkConfig();
|
|
||||||
|
|
||||||
if (!result.valid) {
|
|
||||||
alert(result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { networkId, rpcUrl, chainId } = result.networkConfig;
|
|
||||||
|
|
||||||
// Проверка на дубликат ID
|
|
||||||
if (securitySettings.rpcConfigs.some(rpc => rpc.networkId === networkId)) {
|
|
||||||
alert(`Ошибка: RPC конфигурация для сети с ID '${networkId}' уже существует.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
securitySettings.rpcConfigs.push({ networkId, rpcUrl, chainId });
|
|
||||||
|
|
||||||
// Очистка полей ввода
|
|
||||||
resetNetworkEntry();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция удаления RPC конфигурации
|
|
||||||
const removeRpcConfig = (index) => {
|
|
||||||
securitySettings.rpcConfigs.splice(index, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция добавления нового токена для авторизации
|
|
||||||
const addAuthToken = () => {
|
|
||||||
const name = newAuthToken.name.trim();
|
|
||||||
const address = newAuthToken.address.trim();
|
|
||||||
const minBalance = newAuthToken.minBalance.trim();
|
|
||||||
const network = newAuthToken.network;
|
|
||||||
|
|
||||||
if (name && address) {
|
|
||||||
// Проверка на дубликат адреса
|
|
||||||
if (securitySettings.authTokens.some(token => token.address.toLowerCase() === address.toLowerCase())) {
|
|
||||||
alert(`Ошибка: Токен с адресом '${address}' уже добавлен.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
securitySettings.authTokens.push({ name, address, minBalance, network });
|
|
||||||
|
|
||||||
// Очистка полей ввода
|
|
||||||
newAuthToken.name = '';
|
|
||||||
newAuthToken.address = '';
|
|
||||||
newAuthToken.minBalance = '1.0';
|
|
||||||
newAuthToken.network = 'eth';
|
|
||||||
} else {
|
|
||||||
alert('Пожалуйста, заполните название и адрес токена.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция удаления токена авторизации
|
|
||||||
const removeAuthToken = (index) => {
|
|
||||||
securitySettings.authTokens.splice(index, 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сохранение всех настроек безопасности на бэкенд
|
// Сохранение всех настроек безопасности на бэкенд
|
||||||
@@ -343,8 +172,12 @@ const saveSecuritySettings = async () => {
|
|||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
// Подготовка данных для отправки
|
// Подготовка данных для отправки
|
||||||
|
const validRpcConfigs = securitySettings.rpcConfigs.filter(
|
||||||
|
c => c.networkId && c.rpcUrl
|
||||||
|
);
|
||||||
|
|
||||||
const settingsData = {
|
const settingsData = {
|
||||||
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs)),
|
rpcConfigs: validRpcConfigs,
|
||||||
authTokens: JSON.parse(JSON.stringify(securitySettings.authTokens))
|
authTokens: JSON.parse(JSON.stringify(securitySettings.authTokens))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -362,6 +195,8 @@ const saveSecuritySettings = async () => {
|
|||||||
|
|
||||||
if (rpcResponse.data.success && authResponse.data.success) {
|
if (rpcResponse.data.success && authResponse.data.success) {
|
||||||
alert('Все настройки безопасности успешно сохранены.');
|
alert('Все настройки безопасности успешно сохранены.');
|
||||||
|
eventBus.emit('settings-updated');
|
||||||
|
eventBus.emit('auth-settings-saved', { tokens: settingsData.authTokens });
|
||||||
} else {
|
} else {
|
||||||
alert('Произошла ошибка при сохранении настроек. Проверьте консоль разработчика.');
|
alert('Произошла ошибка при сохранении настроек. Проверьте консоль разработчика.');
|
||||||
}
|
}
|
||||||
@@ -373,7 +208,31 @@ const saveSecuritySettings = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция-обработчик для тестирования RPC соединения
|
// Загрузка настроек при монтировании компонента
|
||||||
|
onMounted(() => {
|
||||||
|
loadSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Методы для RPC ---
|
||||||
|
const addRpcConfig = () => {
|
||||||
|
const result = validateAndPrepareNetworkConfig();
|
||||||
|
if (!result.valid) {
|
||||||
|
alert(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { networkId, rpcUrl, chainId } = result.networkConfig;
|
||||||
|
if (securitySettings.rpcConfigs.some(rpc => rpc.networkId === networkId)) {
|
||||||
|
alert(`Ошибка: RPC конфигурация для сети с ID '${networkId}' уже существует.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
securitySettings.rpcConfigs.push({ networkId, rpcUrl, chainId });
|
||||||
|
resetNetworkEntry();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRpcConfig = (index) => {
|
||||||
|
securitySettings.rpcConfigs.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
const testRpcHandler = async (rpc) => {
|
const testRpcHandler = async (rpc) => {
|
||||||
try {
|
try {
|
||||||
const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl);
|
const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl);
|
||||||
@@ -388,10 +247,47 @@ const testRpcHandler = async (rpc) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Загрузка настроек при монтировании компонента
|
// --- Методы для токенов аутентификации ---
|
||||||
onMounted(() => {
|
const addAuthToken = () => {
|
||||||
loadSettings();
|
const name = newAuthToken.name.trim();
|
||||||
});
|
const address = newAuthToken.address.trim();
|
||||||
|
const minBalance = newAuthToken.minBalance.trim();
|
||||||
|
const network = newAuthToken.network;
|
||||||
|
if (name && address) {
|
||||||
|
if (securitySettings.authTokens.some(token => token.address.toLowerCase() === address.toLowerCase())) {
|
||||||
|
alert(`Ошибка: Токен с адресом '${address}' уже добавлен.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
securitySettings.authTokens.push({ name, address, minBalance, network });
|
||||||
|
newAuthToken.name = '';
|
||||||
|
newAuthToken.address = '';
|
||||||
|
newAuthToken.minBalance = '1.0';
|
||||||
|
newAuthToken.network = 'eth';
|
||||||
|
} else {
|
||||||
|
alert('Пожалуйста, заполните название и адрес токена.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAuthToken = (index) => {
|
||||||
|
securitySettings.authTokens.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// provide для дочерних компонентов
|
||||||
|
provide('rpcConfigs', securitySettings.rpcConfigs);
|
||||||
|
provide('removeRpcConfig', removeRpcConfig);
|
||||||
|
provide('addRpcConfig', addRpcConfig);
|
||||||
|
provide('testRpcHandler', testRpcHandler);
|
||||||
|
provide('testingRpc', testingRpc);
|
||||||
|
provide('testingRpcId', testingRpcId);
|
||||||
|
provide('networkGroups', networkGroups);
|
||||||
|
provide('networkEntry', networkEntry);
|
||||||
|
provide('validateAndPrepareNetworkConfig', validateAndPrepareNetworkConfig);
|
||||||
|
provide('resetNetworkEntry', resetNetworkEntry);
|
||||||
|
provide('authTokens', securitySettings.authTokens);
|
||||||
|
provide('removeAuthToken', removeAuthToken);
|
||||||
|
provide('addAuthToken', addAuthToken);
|
||||||
|
provide('newAuthToken', newAuthToken);
|
||||||
|
provide('networks', networks);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -609,14 +505,6 @@ small {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-panel {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: var(--spacing-lg, 20px);
|
|
||||||
padding-top: var(--spacing-md, 15px);
|
|
||||||
border-top: 1px solid var(--color-grey-light, #eee);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user