обновление

This commit is contained in:
2025-08-15 16:46:07 +03:00
parent a10810df55
commit 35e1d3bb56
30 changed files with 1788 additions and 1271 deletions

3
.gitignore vendored
View File

@@ -98,6 +98,9 @@ typings/
*.swp *.swp
*.swo *.swo
# Backup files
*.bak
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json" "buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

View File

@@ -1,4 +1,4 @@
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/998bc5520c3173891dd242df2365077b.json" "buildInfo": "../../build-info/3e12480a731f7a845287f0f150241bb4.json"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -63,7 +63,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
bytes operation; // операция для исполнения bytes operation; // операция для исполнения
uint256 governanceChainId; // сеть голосования (Single-Chain Governance) uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
uint256[] targetChains; // целевые сети для исполнения uint256[] targetChains; // целевые сети для исполнения
uint256 timelock; // earliest execution timestamp (sec)
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
mapping(address => bool) hasVoted; mapping(address => bool) hasVoted;
} }
@@ -106,7 +105,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower); event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
event ProposalExecuted(uint256 proposalId, bytes operation); event ProposalExecuted(uint256 proposalId, bytes operation);
event ProposalCancelled(uint256 proposalId, string reason); event ProposalCancelled(uint256 proposalId, string reason);
event ProposalTimelockSet(uint256 proposalId, uint256 timelock);
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains); event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId); event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
event ModuleAdded(bytes32 moduleId, address moduleAddress); event ModuleAdded(bytes32 moduleId, address moduleAddress);
@@ -192,13 +190,30 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
bytes memory _operation, bytes memory _operation,
uint256 _governanceChainId, uint256 _governanceChainId,
uint256[] memory _targetChains, uint256[] memory _targetChains,
uint256 _timelockDelay uint256 /* _timelockDelay */
) external returns (uint256) { ) external returns (uint256) {
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
require(_duration > 0, "Duration must be positive"); require(_duration > 0, "Duration must be positive");
require(supportedChains[_governanceChainId], "Chain not supported"); require(supportedChains[_governanceChainId], "Chain not supported");
require(_timelockDelay <= 365 days, "Timelock too big"); // _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
return _createProposalInternal(
_description,
_duration,
_operation,
_governanceChainId,
_targetChains,
msg.sender
);
}
function _createProposalInternal(
string memory _description,
uint256 _duration,
bytes memory _operation,
uint256 _governanceChainId,
uint256[] memory _targetChains,
address _initiator
) internal returns (uint256) {
uint256 proposalId = proposalCounter++; uint256 proposalId = proposalCounter++;
Proposal storage proposal = proposals[proposalId]; Proposal storage proposal = proposals[proposalId];
@@ -208,13 +223,14 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
proposal.againstVotes = 0; proposal.againstVotes = 0;
proposal.executed = false; proposal.executed = false;
proposal.deadline = block.timestamp + _duration; proposal.deadline = block.timestamp + _duration;
proposal.initiator = msg.sender; proposal.initiator = _initiator;
proposal.operation = _operation; proposal.operation = _operation;
proposal.governanceChainId = _governanceChainId; proposal.governanceChainId = _governanceChainId;
proposal.timelock = block.timestamp + _timelockDelay;
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке // Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
uint256 nowClock = clock(); uint256 nowClock = clock();
proposal.snapshotTimepoint = nowClock == 0 ? 0 : nowClock - 1; proposal.snapshotTimepoint = nowClock == 0 ? 0 : nowClock - 1;
// запись целевых сетей // запись целевых сетей
for (uint256 i = 0; i < _targetChains.length; i++) { for (uint256 i = 0; i < _targetChains.length; i++) {
require(supportedChains[_targetChains[i]], "Target chain not supported"); require(supportedChains[_targetChains[i]], "Target chain not supported");
@@ -222,10 +238,9 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
} }
allProposalIds.push(proposalId); allProposalIds.push(proposalId);
emit ProposalCreated(proposalId, msg.sender, _description); emit ProposalCreated(proposalId, _initiator, _description);
emit ProposalGovernanceChainSet(proposalId, _governanceChainId); emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
emit ProposalTargetsSet(proposalId, _targetChains); emit ProposalTargetsSet(proposalId, _targetChains);
emit ProposalTimelockSet(proposalId, proposal.timelock);
return proposalId; return proposalId;
} }
@@ -297,7 +312,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
"Voting not ended and quorum not reached" "Voting not ended and quorum not reached"
); );
require(passed && quorumReached, "Proposal not passed"); require(passed && quorumReached, "Proposal not passed");
require(block.timestamp >= proposal.timelock, "Timelock not expired");
proposal.executed = true; proposal.executed = true;
@@ -341,7 +355,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
require(!proposal.executed, "Proposal already executed in this chain"); require(!proposal.executed, "Proposal already executed in this chain");
require(currentChainId != proposal.governanceChainId, "Use executeProposal in governance chain"); require(currentChainId != proposal.governanceChainId, "Use executeProposal in governance chain");
require(_isTargetChain(proposal, currentChainId), "Chain not in targets"); require(_isTargetChain(proposal, currentChainId), "Chain not in targets");
require(block.timestamp >= proposal.timelock, "Timelock not expired");
require(signers.length == signatures.length, "Bad signatures"); require(signers.length == signatures.length, "Bad signatures");
bytes32 opHash = keccak256(proposal.operation); bytes32 opHash = keccak256(proposal.operation);
@@ -620,24 +633,28 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
require(!activeModules[_moduleId], "Module already exists"); require(!activeModules[_moduleId], "Module already exists");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
uint256 proposalId = proposalCounter++; // Операция добавления модуля
Proposal storage proposal = proposals[proposalId];
proposal.id = proposalId;
proposal.description = _description;
proposal.deadline = block.timestamp + _duration;
proposal.initiator = msg.sender;
// Кодируем операцию добавления модуля
bytes memory operation = abi.encodeWithSelector( bytes memory operation = abi.encodeWithSelector(
bytes4(keccak256("_addModule(bytes32,address)")), bytes4(keccak256("_addModule(bytes32,address)")),
_moduleId, _moduleId,
_moduleAddress _moduleAddress
); );
proposal.operation = operation;
emit ProposalCreated(proposalId, msg.sender, _description); // Целевые сети: по умолчанию все поддерживаемые сети
return proposalId; uint256[] memory targets = new uint256[](supportedChainIds.length);
for (uint256 i = 0; i < supportedChainIds.length; i++) {
targets[i] = supportedChainIds[i];
}
// Таймлок больше не используется в ядре; модуль Timelock будет добавлен отдельно
return _createProposalInternal(
_description,
_duration,
operation,
_chainId,
targets,
msg.sender
);
} }
/** /**
@@ -657,23 +674,27 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
require(activeModules[_moduleId], "Module does not exist"); require(activeModules[_moduleId], "Module does not exist");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
uint256 proposalId = proposalCounter++; // Операция удаления модуля
Proposal storage proposal = proposals[proposalId];
proposal.id = proposalId;
proposal.description = _description;
proposal.deadline = block.timestamp + _duration;
proposal.initiator = msg.sender;
// Кодируем операцию удаления модуля
bytes memory operation = abi.encodeWithSelector( bytes memory operation = abi.encodeWithSelector(
bytes4(keccak256("_removeModule(bytes32)")), bytes4(keccak256("_removeModule(bytes32)")),
_moduleId _moduleId
); );
proposal.operation = operation;
emit ProposalCreated(proposalId, msg.sender, _description); // Целевые сети: по умолчанию все поддерживаемые сети
return proposalId; uint256[] memory targets = new uint256[](supportedChainIds.length);
for (uint256 i = 0; i < supportedChainIds.length; i++) {
targets[i] = supportedChainIds[i];
}
// Таймлок больше не используется в ядре; модуль Timelock будет добавлен отдельно
return _createProposalInternal(
_description,
_duration,
operation,
_chainId,
targets,
msg.sender
);
} }
/** /**
@@ -753,7 +774,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
uint256 deadline, uint256 deadline,
address initiator, address initiator,
uint256 governanceChainId, uint256 governanceChainId,
uint256 timelock,
uint256 snapshotTimepoint, uint256 snapshotTimepoint,
uint256[] memory targets uint256[] memory targets
) { ) {
@@ -769,7 +790,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
p.deadline, p.deadline,
p.initiator, p.initiator,
p.governanceChainId, p.governanceChainId,
p.timelock,
p.snapshotTimepoint, p.snapshotTimepoint,
p.targetChains p.targetChains
); );
@@ -818,7 +839,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
if (p.executed) return 3; if (p.executed) return 3;
(bool passed, bool quorumReached) = checkProposalResult(_proposalId); (bool passed, bool quorumReached) = checkProposalResult(_proposalId);
bool votingOver = block.timestamp >= p.deadline; bool votingOver = block.timestamp >= p.deadline;
bool ready = passed && quorumReached && block.timestamp >= p.timelock; bool ready = passed && quorumReached;
if (ready) return 5; // ReadyForExecution if (ready) return 5; // ReadyForExecution
if (passed && (votingOver || quorumReached)) return 1; // Succeeded if (passed && (votingOver || quorumReached)) return 1; // Succeeded
if (votingOver && !passed) return 2; // Defeated if (votingOver && !passed) return 2; // Defeated

View File

@@ -1,22 +0,0 @@
-- Миграция: наполнение таблиц rpc_providers и auth_tokens начальными значениями
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- Добавление RPC-провайдеров (пропускаем, данные должны быть зашифрованы)
-- INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
-- VALUES
-- ('bsc', 'https://bsc-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 56),
-- ('ethereum', 'https://eth-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 1),
-- ('arbitrum', 'https://arb1.arbitrum.io/rpc', 42161),
-- ('polygon', 'https://polygon.drpc.org', 137),
-- ('sepolia', 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 11155111)
-- ON CONFLICT (network_id) DO NOTHING;
-- Добавление токенов для аутентификации админа (пропускаем, данные должны быть зашифрованы)
-- INSERT INTO auth_tokens (name, address, network, min_balance)
-- VALUES
-- ('HB3A', '0x4b294265720b09ca39bfba18c7e368413c0f68eb', 'bsc', 10.0),
-- ('HB3A', '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', 'ethereum', 10.0),
-- ('test2', '0xef49261169B454f191678D2aFC5E91Ad2e85dfD8', 'sepolia', 50.0),
-- ('HB3A', '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', 'polygon', 10.0),
-- ('HB3A', '0xdCe769b847a0a697239777D0B1C7dd33b6012ba0', 'arbitrum', 100.0)
-- ON CONFLICT (address, network) DO NOTHING;

View File

@@ -0,0 +1,11 @@
-- Create encrypted secrets storage
-- Stores key/value pairs where value is encrypted via encryptedDatabaseService
CREATE TABLE IF NOT EXISTS public.secrets (
key text PRIMARY KEY,
value_encrypted text,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -19,6 +19,8 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const ethers = require('ethers'); // Added ethers for private key validation const ethers = require('ethers'); // Added ethers for private key validation
const create2 = require('../utils/create2'); const create2 = require('../utils/create2');
const verificationStore = require('../services/verificationStore');
const etherscanV2 = require('../services/etherscanV2VerificationService');
/** /**
* @route POST /api/dle-v2 * @route POST /api/dle-v2
@@ -92,6 +94,47 @@ router.get('/', async (req, res, next) => {
} }
}); });
/**
* @route POST /api/dle-v2/manual-card
* @desc Ручное сохранение карточки DLE по адресу (если деплой уже был)
* @access Private (admin)
*/
router.post('/manual-card', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { dleAddress, name, symbol, location, coordinates, jurisdiction, oktmo, okvedCodes, kpp, quorumPercentage, initialPartners, initialAmounts, supportedChainIds, networks } = req.body || {};
if (!dleAddress) {
return res.status(400).json({ success: false, message: 'dleAddress обязателен' });
}
const data = {
name: name || '',
symbol: symbol || '',
location: location || '',
coordinates: coordinates || '',
jurisdiction: jurisdiction ?? 1,
oktmo: oktmo ?? null,
okvedCodes: Array.isArray(okvedCodes) ? okvedCodes : [],
kpp: kpp ?? null,
quorumPercentage: quorumPercentage ?? 51,
initialPartners: Array.isArray(initialPartners) ? initialPartners : [],
initialAmounts: Array.isArray(initialAmounts) ? initialAmounts : [],
governanceSettings: {
quorumPercentage: quorumPercentage ?? 51,
supportedChainIds: Array.isArray(supportedChainIds) ? supportedChainIds : [],
currentChainId: Array.isArray(supportedChainIds) && supportedChainIds.length ? supportedChainIds[0] : 1
},
dleAddress,
version: 'v2',
networks: Array.isArray(networks) ? networks : [],
createdAt: new Date().toISOString()
};
const savedPath = dleV2Service.saveDLEData(data);
return res.json({ success: true, data: { file: savedPath } });
} catch (e) {
logger.error('manual-card error', e);
return res.status(500).json({ success: false, message: e.message });
}
});
/** /**
* @route GET /api/dle-v2/default-params * @route GET /api/dle-v2/default-params
* @desc Получить параметры по умолчанию для создания DLE v2 * @desc Получить параметры по умолчанию для создания DLE v2
@@ -330,3 +373,144 @@ router.post('/predict-addresses', auth.requireAuth, auth.requireAdmin, async (re
return res.status(500).json({ success: false, message: 'Ошибка расчета адресов' }); return res.status(500).json({ success: false, message: 'Ошибка расчета адресов' });
} }
}); });
// Сохранить GUID верификации (если нужно отдельным вызовом)
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { address, chainId, guid } = req.body || {};
if (!address || !chainId || !guid) return res.status(400).json({ success: false, message: 'address, chainId, guid обязательны' });
const data = verificationStore.updateChain(address, chainId, { guid, status: 'submitted' });
return res.json({ success: true, data });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Получить статусы верификации по адресу DLE
router.get('/verify/status/:address', auth.requireAuth, async (req, res) => {
try {
const { address } = req.params;
const data = verificationStore.read(address);
return res.json({ success: true, data });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Обновить статусы верификации, опросив Etherscan V2
router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { address } = req.params;
let { etherscanApiKey } = req.body || {};
if (!etherscanApiKey) {
try {
const { getSecret } = require('../services/secretStore');
etherscanApiKey = await getSecret('ETHERSCAN_V2_API_KEY');
} catch(_) {}
}
const data = verificationStore.read(address);
if (!data || !data.chains) return res.json({ success: true, data });
// Если guid отсутствует или ранее была ошибка chainid — попробуем автоматически переотправить верификацию (resubmit)
const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || ''));
if (needResubmit && etherscanApiKey) {
// Найти карточку DLE
const list = dleV2Service.getAllDLEs();
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
if (card) {
const deployParams = {
name: card.name,
symbol: card.symbol,
location: card.location,
coordinates: card.coordinates,
jurisdiction: card.jurisdiction,
oktmo: card.oktmo,
okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [],
kpp: card.kpp,
quorumPercentage: card.quorumPercentage,
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1
};
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
try {
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
} catch (_) {}
}
}
// Далее — обычный опрос по имеющимся guid
const latest = verificationStore.read(address);
const chains = Object.values(latest.chains);
for (const c of chains) {
if (!c.guid || !c.chainId) continue;
try {
const st = await etherscanV2.checkStatus(c.chainId, c.guid, etherscanApiKey);
verificationStore.updateChain(address, c.chainId, { status: st?.result || st?.message || 'unknown' });
} catch (e) {
verificationStore.updateChain(address, c.chainId, { status: `error: ${e.message}` });
}
}
const updated = verificationStore.read(address);
return res.json({ success: true, data: updated });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Повторно отправить верификацию на Etherscan V2 для уже созданного DLE
router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { address } = req.params;
const { etherscanApiKey } = req.body || {};
if (!etherscanApiKey && !process.env.ETHERSCAN_API_KEY) {
return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' });
}
// Найти карточку DLE по адресу
const list = dleV2Service.getAllDLEs();
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' });
// Сформировать deployParams из карточки
const deployParams = {
name: card.name,
symbol: card.symbol,
location: card.location,
coordinates: card.coordinates,
jurisdiction: card.jurisdiction,
oktmo: card.oktmo,
okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [],
kpp: card.kpp,
quorumPercentage: card.quorumPercentage,
initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [],
initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [],
supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []),
currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1
};
// Сформировать deployResult из карточки
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
const updated = verificationStore.read(address);
return res.json({ success: true, data: updated });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
// Предварительная проверка балансов во всех выбранных сетях
router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { supportedChainIds, privateKey } = req.body || {};
if (!privateKey) return res.status(400).json({ success: false, message: 'Приватный ключ не передан' });
if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) {
return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' });
}
const result = await dleV2Service.checkBalances(supportedChainIds, privateKey);
return res.json({ success: true, data: result });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});

View File

@@ -2,25 +2,159 @@
const hre = require('hardhat'); const hre = require('hardhat');
const path = require('path'); const path = require('path');
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, factoryAddress, dleInit) { // Подбираем безопасные gas/fee для разных сетей (включая L2)
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
const fee = await provider.getFeeData();
const overrides = {};
const minPriority = (await (async () => hre.ethers.parseUnits(minPriorityGwei.toString(), 'gwei'))());
const minFee = (await (async () => hre.ethers.parseUnits(minFeeGwei.toString(), 'gwei'))());
if (fee.maxFeePerGas) {
overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas;
overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n)
? fee.maxPriorityFeePerGas
: minPriority;
} else if (fee.gasPrice) {
overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice;
}
return overrides;
}
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonce, dleInit) {
const { ethers } = hre; const { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl); const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider); const wallet = new ethers.Wallet(pk, provider);
const net = await provider.getNetwork();
// Ensure factory // DEBUG: базовая информация по сети
let faddr = factoryAddress; try {
const code = faddr ? await provider.getCode(faddr) : '0x'; const calcInitHash = ethers.keccak256(dleInit);
if (!faddr || code === '0x') { const saltLen = ethers.getBytes(salt).length;
const Factory = await hre.ethers.getContractFactory('FactoryDeployer', wallet); console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`);
const factory = await Factory.deploy(); console.log(`[MULTI_DBG] wallet=${wallet.address} targetFactoryNonce=${targetFactoryNonce}`);
await factory.waitForDeployment(); console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`);
faddr = await factory.getAddress(); console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`);
console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`);
console.log(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
} catch (e) {
console.log('[MULTI_DBG] precheck error', e?.message || e);
}
// 1) Выравнивание nonce до targetFactoryNonce нулевыми транзакциями (если нужно)
let current = await provider.getTransactionCount(wallet.address, 'pending');
if (current > targetFactoryNonce) {
throw new Error(`Current nonce ${current} > targetFactoryNonce ${targetFactoryNonce} on chainId=${Number(net.chainId)}`);
}
while (current < targetFactoryNonce) {
const overrides = await getFeeOverrides(provider);
let gasLimit = 50000; // некоторые L2 требуют >21000
let sent = false;
let lastErr = null;
for (let attempt = 0; attempt < 2 && !sent; attempt++) {
try {
const txReq = {
to: wallet.address,
value: 0n,
nonce: current,
gasLimit,
...overrides
};
const txFill = await wallet.sendTransaction(txReq);
await txFill.wait();
sent = true;
} catch (e) {
lastErr = e;
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt === 0) {
gasLimit = 100000; // поднимаем лимит и пробуем ещё раз
continue;
}
throw e;
}
}
if (!sent) throw lastErr || new Error('filler tx failed');
current++;
}
// 2) Деплой FactoryDeployer на согласованном nonce
const FactoryCF = await hre.ethers.getContractFactory('FactoryDeployer', wallet);
const feeOverrides = await getFeeOverrides(provider);
const factoryContract = await FactoryCF.deploy({ nonce: targetFactoryNonce, ...feeOverrides });
await factoryContract.waitForDeployment();
const factoryAddress = await factoryContract.getAddress();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} FactoryDeployer.address=${factoryAddress}`);
// 3) Деплой DLE через CREATE2
const Factory = await hre.ethers.getContractAt('FactoryDeployer', factoryAddress, wallet);
const n = await provider.getTransactionCount(wallet.address, 'pending');
let tx;
try {
// Предварительная проверка конструктора вне CREATE2 (даст явную причину, если он ревертится)
try {
await wallet.estimateGas({ data: dleInit });
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predeploy(estGas) ok for constructor`);
} catch (e) {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predeploy(estGas) failed: ${e?.reason || e?.shortMessage || e?.message || e}`);
if (e?.data) console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predeploy revert data: ${e.data}`);
}
// Оцениваем газ и добавляем запас
const est = await Factory.deploy.estimateGas(salt, dleInit, { nonce: n, ...feeOverrides }).catch(() => null);
// Рассчитываем доступный gasLimit из баланса
let gasLimit;
try {
const balance = await provider.getBalance(wallet.address, 'latest');
const effPrice = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n;
const reserve = hre.ethers.parseEther('0.005');
const maxByBalance = effPrice > 0n && balance > reserve ? (balance - reserve) / effPrice : 3_000_000n;
const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance);
gasLimit = est ? (est + est / 5n) : fallbackGas;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
} catch (_) {
const fallbackGas = 3_000_000n;
gasLimit = est ? (est + est / 5n) : fallbackGas;
}
// DEBUG: ожидаемый адрес через computeAddress
try {
const predicted = await Factory.computeAddress(salt, initCodeHash);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predictedAddress=${predicted}`);
// Idempotency: если уже есть код по адресу, пропускаем деплой
const code = await provider.getCode(predicted);
if (code && code !== '0x') {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} code already exists at predictedAddress, skip deploy`);
return { factory: factoryAddress, address: predicted, chainId: Number(net.chainId) };
}
} catch (e) {
console.log('[MULTI_DBG] computeAddress(before) error', e?.message || e);
}
tx = await Factory.deploy(salt, dleInit, { nonce: n, gasLimit, ...feeOverrides });
} catch (e) {
const n2 = await provider.getTransactionCount(wallet.address, 'pending');
const est2 = await Factory.deploy.estimateGas(salt, dleInit, { nonce: n2, ...feeOverrides }).catch(() => null);
let gasLimit2;
try {
const balance2 = await provider.getBalance(wallet.address, 'latest');
const effPrice2 = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n;
const reserve2 = hre.ethers.parseEther('0.005');
const maxByBalance2 = effPrice2 > 0n && balance2 > reserve2 ? (balance2 - reserve2) / effPrice2 : 3_000_000n;
const fallbackGas2 = maxByBalance2 > 5_000_000n ? 5_000_000n : (maxByBalance2 < 2_500_000n ? 2_500_000n : maxByBalance2);
gasLimit2 = est2 ? (est2 + est2 / 5n) : fallbackGas2;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} RETRY estGas=${est2?.toString?.()||'null'} effGasPrice=${effPrice2?.toString?.()||'0'} maxByBalance=${maxByBalance2.toString()} chosenGasLimit=${gasLimit2.toString()}`);
} catch (_) {
gasLimit2 = est2 ? (est2 + est2 / 5n) : 3_000_000n;
}
console.log(`[MULTI_DBG] retry deploy with nonce=${n2} gasLimit=${gasLimit2?.toString?.() || 'auto'}`);
console.log(`[MULTI_DBG] deploy error(first) ${e?.message || e}`);
tx = await Factory.deploy(salt, dleInit, { nonce: n2, gasLimit: gasLimit2, ...feeOverrides });
} }
const Factory = await hre.ethers.getContractAt('FactoryDeployer', faddr, wallet);
const tx = await Factory.deploy(salt, dleInit);
const rc = await tx.wait(); const rc = await tx.wait();
const addr = rc.logs?.[0]?.args?.addr || (await Factory.computeAddress(salt, initCodeHash)); let addr = rc.logs?.[0]?.args?.addr;
return { factory: faddr, address: addr }; if (!addr) {
try {
addr = await Factory.computeAddress(salt, initCodeHash);
} catch (e) {
console.log('[MULTI_DBG] computeAddress(after) error', e?.message || e);
}
}
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deployedAddress=${addr}`);
return { factory: factoryAddress, address: addr, chainId: Number(net.chainId) };
} }
async function main() { async function main() {
@@ -31,7 +165,10 @@ async function main() {
const networks = (process.env.MULTICHAIN_RPC_URLS || '').split(',').map(s => s.trim()).filter(Boolean); const networks = (process.env.MULTICHAIN_RPC_URLS || '').split(',').map(s => s.trim()).filter(Boolean);
const factories = (process.env.MULTICHAIN_FACTORY_ADDRESSES || '').split(',').map(s => s.trim()); const factories = (process.env.MULTICHAIN_FACTORY_ADDRESSES || '').split(',').map(s => s.trim());
if (!pk || !salt || !initCodeHash || networks.length === 0) throw new Error('Env: PRIVATE_KEY, CREATE2_SALT, INIT_CODE_HASH, MULTICHAIN_RPC_URLS'); if (!pk) throw new Error('Env: PRIVATE_KEY');
if (!salt) throw new Error('Env: CREATE2_SALT');
if (!initCodeHash) throw new Error('Env: INIT_CODE_HASH');
if (networks.length === 0) throw new Error('Env: MULTICHAIN_RPC_URLS');
// Prepare init code once // Prepare init code once
const paramsPath = path.join(__dirname, './current-params.json'); const paramsPath = path.join(__dirname, './current-params.json');
@@ -53,12 +190,33 @@ async function main() {
}; };
const deployTx = await DLE.getDeployTransaction(dleConfig, params.currentChainId); const deployTx = await DLE.getDeployTransaction(dleConfig, params.currentChainId);
const dleInit = deployTx.data; const dleInit = deployTx.data;
// DEBUG: глобальные значения
try {
const calcInitHash = ethers.keccak256(dleInit);
const saltLen = ethers.getBytes(salt).length;
console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`);
console.log(`[MULTI_DBG] GLOBAL initCodeHash(provided)=${initCodeHash}`);
console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${calcInitHash}`);
console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
} catch (e) {
console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e);
}
// Подготовим провайдеры и вычислим общий nonce для фабрики
const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u));
const wallets = providers.map(p => new hre.ethers.Wallet(pk, p));
const nonces = [];
for (let i = 0; i < providers.length; i++) {
const n = await providers[i].getTransactionCount(wallets[i].address, 'pending');
nonces.push(n);
}
const targetFactoryNonce = Math.max(...nonces);
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetFactoryNonce=${targetFactoryNonce}`);
const results = []; const results = [];
for (let i = 0; i < networks.length; i++) { for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i]; const rpcUrl = networks[i];
const factory = factories[i] || process.env.FACTORY_ADDRESS || null; const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonce, dleInit);
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, factory, dleInit);
results.push({ rpcUrl, ...r }); results.push({ rpcUrl, ...r });
} }
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(results)); console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(results));

View File

@@ -16,6 +16,8 @@ const fs = require('fs');
const { ethers } = require('ethers'); const { ethers } = require('ethers');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { getRpcUrlByChainId } = require('./rpcProviderService'); const { getRpcUrlByChainId } = require('./rpcProviderService');
const etherscanV2 = require('./etherscanV2VerificationService');
const verificationStore = require('./verificationStore');
/** /**
* Сервис для управления DLE v2 (Digital Legal Entity) * Сервис для управления DLE v2 (Digital Legal Entity)
@@ -28,6 +30,8 @@ class DLEV2Service {
* @returns {Promise<Object>} - Результат создания DLE * @returns {Promise<Object>} - Результат создания DLE
*/ */
async createDLE(dleParams) { async createDLE(dleParams) {
let paramsFile = null;
let tempParamsFile = null;
try { try {
logger.info('Начало создания DLE v2 с параметрами:', dleParams); logger.info('Начало создания DLE v2 с параметрами:', dleParams);
@@ -38,10 +42,10 @@ class DLEV2Service {
const deployParams = this.prepareDeployParams(dleParams); const deployParams = this.prepareDeployParams(dleParams);
// Сохраняем параметры во временный файл // Сохраняем параметры во временный файл
const paramsFile = this.saveParamsToFile(deployParams); paramsFile = this.saveParamsToFile(deployParams);
// Копируем параметры во временный файл с предсказуемым именем // Копируем параметры во временный файл с предсказуемым именем
const tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json'); tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json');
const deployDir = path.dirname(tempParamsFile); const deployDir = path.dirname(tempParamsFile);
if (!fs.existsSync(deployDir)) { if (!fs.existsSync(deployDir)) {
fs.mkdirSync(deployDir, { recursive: true }); fs.mkdirSync(deployDir, { recursive: true });
@@ -64,8 +68,9 @@ class DLEV2Service {
{ {
const { ethers } = require('ethers'); const { ethers } = require('ethers');
const provider = new ethers.JsonRpcProvider(rpcUrls[0]); const provider = new ethers.JsonRpcProvider(rpcUrls[0]);
const walletAddress = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey, provider).address : null; if (dleParams.privateKey) {
if (walletAddress) { const pk = dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
const walletAddress = new ethers.Wallet(pk, provider).address;
const balance = await provider.getBalance(walletAddress); const balance = await provider.getBalance(walletAddress);
const minBalance = ethers.parseEther("0.00001"); const minBalance = ethers.parseEther("0.00001");
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`); logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
@@ -85,22 +90,90 @@ class DLEV2Service {
const factoryAddresses = deployParams.supportedChainIds.map(cid => process.env[`FACTORY_ADDRESS_${cid}`] || '').join(','); const factoryAddresses = deployParams.supportedChainIds.map(cid => process.env[`FACTORY_ADDRESS_${cid}`] || '').join(',');
// Мультисетевой деплой одним вызовом // Мультисетевой деплой одним вызовом
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
const { createAndStoreNewCreate2Salt } = require('./secretStore');
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`);
const result = await this.runDeployMultichain(paramsFile, { const result = await this.runDeployMultichain(paramsFile, {
rpcUrls: rpcUrls.join(','), rpcUrls: rpcUrls.join(','),
privateKey: dleParams.privateKey, privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
salt: process.env.CREATE2_SALT, salt: create2Salt,
initCodeHash, initCodeHash,
factories: factoryAddresses factories: factoryAddresses
}); });
// Очищаем временные файлы // Сохраняем информацию о созданном DLE для отображения на странице управления
this.cleanupTempFiles(paramsFile, tempParamsFile); try {
const firstNet = Array.isArray(result?.data?.networks) && result.data.networks.length > 0 ? result.data.networks[0] : null;
const dleData = {
name: deployParams.name,
symbol: deployParams.symbol,
location: deployParams.location,
coordinates: deployParams.coordinates,
jurisdiction: deployParams.jurisdiction,
oktmo: deployParams.oktmo,
okvedCodes: deployParams.okvedCodes || [],
kpp: deployParams.kpp,
quorumPercentage: deployParams.quorumPercentage,
initialPartners: deployParams.initialPartners || [],
initialAmounts: deployParams.initialAmounts || [],
governanceSettings: {
quorumPercentage: deployParams.quorumPercentage,
supportedChainIds: deployParams.supportedChainIds,
currentChainId: deployParams.currentChainId
},
dleAddress: (result?.data?.dleAddress) || (firstNet?.address) || null,
version: 'v2',
networks: result?.data?.networks || [],
createdAt: new Date().toISOString()
};
if (dleData.dleAddress) {
this.saveDLEData(dleData);
}
} catch (e) {
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
}
// Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан
try {
if (dleParams.etherscanApiKey) {
const { setSecret } = require('./secretStore');
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
}
} catch (_) {}
// Авто-верификация через Etherscan V2 (опционально)
if (dleParams.autoVerifyAfterDeploy) {
try {
await this.autoVerifyAcrossChains({
deployParams,
deployResult: result,
apiKey: dleParams.etherscanApiKey
});
} catch (e) {
logger.warn('Авто-верификация завершилась с ошибкой:', e.message);
}
}
return result; return result;
} catch (error) { } catch (error) {
logger.error('Ошибка при создании DLE v2:', error); logger.error('Ошибка при создании DLE v2:', error);
throw error; throw error;
} finally {
try {
if (paramsFile || tempParamsFile) {
this.cleanupTempFiles(paramsFile, tempParamsFile);
}
} catch (e) {
logger.warn('Ошибка при очистке временных файлов (finally):', e.message);
}
try {
this.pruneOldTempFiles(24 * 60 * 60 * 1000);
} catch (e) {
logger.warn('Ошибка при автоочистке старых временных файлов:', e.message);
}
} }
} }
@@ -154,6 +227,56 @@ class DLEV2Service {
} }
} }
/**
* Сохраняет/обновляет локальную карточку DLE для отображения в UI
* @param {Object} dleData
* @returns {string} Путь к сохраненному файлу
*/
saveDLEData(dleData) {
try {
if (!dleData || !dleData.dleAddress) {
throw new Error('Неверные данные для сохранения карточки DLE: отсутствует dleAddress');
}
const dlesDir = path.join(__dirname, '../contracts-data/dles');
if (!fs.existsSync(dlesDir)) {
fs.mkdirSync(dlesDir, { recursive: true });
}
// Если уже есть файл с таким адресом — обновим его
let targetFile = null;
try {
const files = fs.readdirSync(dlesDir);
for (const file of files) {
if (file.endsWith('.json') && file.includes('dle-v2-')) {
const fp = path.join(dlesDir, file);
try {
const existing = JSON.parse(fs.readFileSync(fp, 'utf8'));
if (existing?.dleAddress && existing.dleAddress.toLowerCase() === dleData.dleAddress.toLowerCase()) {
targetFile = fp;
// Совмещаем данные (не удаляя существующие поля сетей/верификации, если присутствуют)
dleData = { ...existing, ...dleData };
break;
}
} catch (_) {}
}
}
} catch (_) {}
if (!targetFile) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `dle-v2-${ts}.json`;
targetFile = path.join(dlesDir, fileName);
}
fs.writeFileSync(targetFile, JSON.stringify(dleData, null, 2));
logger.info(`Карточка DLE сохранена: ${targetFile}`);
return targetFile;
} catch (e) {
logger.error('Ошибка сохранения карточки DLE:', e);
throw e;
}
}
/** /**
* Подготавливает параметры для деплоя * Подготавливает параметры для деплоя
* @param {Object} params - Параметры DLE из формы * @param {Object} params - Параметры DLE из формы
@@ -165,11 +288,27 @@ class DLEV2Service {
// Преобразуем суммы из строк или чисел в BigNumber, если нужно // Преобразуем суммы из строк или чисел в BigNumber, если нужно
if (deployParams.initialAmounts && Array.isArray(deployParams.initialAmounts)) { if (deployParams.initialAmounts && Array.isArray(deployParams.initialAmounts)) {
deployParams.initialAmounts = deployParams.initialAmounts.map(amount => { deployParams.initialAmounts = deployParams.initialAmounts.map(rawAmount => {
if (typeof amount === 'string' && !amount.startsWith('0x')) { // Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
return ethers.parseEther(amount).toString(); try {
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
return ethers.parseUnits(rawAmount.toString(), 18).toString();
}
if (typeof rawAmount === 'string') {
const a = rawAmount.trim();
if (a.startsWith('0x')) {
// Уже base units (hex BigNumber) — оставляем как есть
return BigInt(a).toString();
}
// Десятичная строка — конвертируем в base units
return ethers.parseUnits(a, 18).toString();
}
// BigInt или иные типы — приводим к строке без изменения масштаба
return rawAmount.toString();
} catch (e) {
// Фолбэк: безопасно привести к строке
return String(rawAmount);
} }
return amount.toString();
}); });
} }
@@ -294,8 +433,9 @@ class DLEV2Service {
const m = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s*(\[.*\])/s); const m = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s*(\[.*\])/s);
if (!m) throw new Error('Результат не найден'); if (!m) throw new Error('Результат не найден');
const arr = JSON.parse(m[1]); const arr = JSON.parse(m[1]);
if (!Array.isArray(arr) || arr.length === 0) throw new Error('Пустой результат деплоя');
const addr = arr[0].address; const addr = arr[0].address;
const allSame = arr.every(x => x.address.toLowerCase() === addr.toLowerCase()); const allSame = arr.every(x => x.address && x.address.toLowerCase() === addr.toLowerCase());
if (!allSame) throw new Error('Адреса отличаются между сетями'); if (!allSame) throw new Error('Адреса отличаются между сетями');
resolve({ success: true, data: { dleAddress: addr, networks: arr } }); resolve({ success: true, data: { dleAddress: addr, networks: arr } });
} catch (e) { } catch (e) {
@@ -348,6 +488,33 @@ class DLEV2Service {
} }
} }
/**
* Удаляет временные файлы параметров деплоя старше заданного возраста
* @param {number} maxAgeMs - Макс. возраст файлов в миллисекундах (по умолчанию 24ч)
*/
pruneOldTempFiles(maxAgeMs = 24 * 60 * 60 * 1000) {
const tempDir = path.join(__dirname, '../temp');
try {
if (!fs.existsSync(tempDir)) return;
const now = Date.now();
const files = fs.readdirSync(tempDir).filter(f => f.startsWith('dle-v2-params-') && f.endsWith('.json'));
for (const f of files) {
const fp = path.join(tempDir, f);
try {
const st = fs.statSync(fp);
if (now - st.mtimeMs > maxAgeMs) {
fs.unlinkSync(fp);
logger.info(`Удалён старый временный файл: ${fp}`);
}
} catch (e) {
logger.warn(`Не удалось обработать файл ${fp}: ${e.message}`);
}
}
} catch (e) {
logger.warn('Ошибка pruneOldTempFiles:', e.message);
}
}
/** /**
* Получает список всех созданных DLE v2 * Получает список всех созданных DLE v2
* @returns {Array<Object>} - Список DLE v2 * @returns {Array<Object>} - Список DLE v2
@@ -402,6 +569,191 @@ class DLEV2Service {
const initCode = deployTx.data; const initCode = deployTx.data;
return ethers.keccak256(initCode); return ethers.keccak256(initCode);
} }
/**
* Проверяет баланс деплоера во всех выбранных сетях
* @param {number[]} chainIds
* @param {string} privateKey
* @returns {Promise<{balances: Array<{chainId:number, balanceEth:string, ok:boolean, rpcUrl:string}>, insufficient:number[]}>}
*/
async checkBalances(chainIds, privateKey) {
const { ethers } = require('ethers');
const results = [];
const insufficient = [];
const normalizedPk = privateKey?.startsWith('0x') ? privateKey : `0x${privateKey}`;
for (const cid of chainIds || []) {
const rpcUrl = await getRpcUrlByChainId(cid);
if (!rpcUrl) {
results.push({ chainId: cid, balanceEth: '0', ok: false, rpcUrl: null });
insufficient.push(cid);
continue;
}
try {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(normalizedPk, provider);
const bal = await provider.getBalance(wallet.address);
// Минимум для деплоя; можно скорректировать
const min = ethers.parseEther('0.002');
const ok = bal >= min;
results.push({ chainId: cid, balanceEth: ethers.formatEther(bal), ok, rpcUrl });
if (!ok) insufficient.push(cid);
} catch (e) {
results.push({ chainId: cid, balanceEth: '0', ok: false, rpcUrl });
insufficient.push(cid);
}
}
return { balances: results, insufficient };
}
/**
* Авто-верификация контракта во всех выбранных сетях через Etherscan V2
* @param {Object} args
* @param {Object} args.deployParams
* @param {Object} args.deployResult - { success, data: { dleAddress, networks: [{rpcUrl,address}] } }
* @param {string} [args.apiKey]
*/
async autoVerifyAcrossChains({ deployParams, deployResult, apiKey }) {
if (!deployResult?.success) throw new Error('Нет результата деплоя для верификации');
// Подхватить ключ из secrets, если аргумент не передан
if (!apiKey) {
try {
const { getSecret } = require('./secretStore');
apiKey = await getSecret('ETHERSCAN_V2_API_KEY');
} catch (_) {}
}
// Получаем компилер, standard-json-input и contractName из artifacts/build-info
const { standardJson, compilerVersion, contractName, constructorArgsHex } = await this.prepareVerificationPayload(deployParams);
// Для каждой сети отправим верификацию, используя адрес из результата для соответствующего chainId
const chainIds = Array.isArray(deployParams.supportedChainIds) ? deployParams.supportedChainIds : [];
const netMap = new Map();
if (Array.isArray(deployResult.data?.networks)) {
for (const n of deployResult.data.networks) {
if (n && typeof n.chainId === 'number') netMap.set(n.chainId, n.address);
}
}
for (const cid of chainIds) {
try {
const addrForChain = netMap.get(cid);
if (!addrForChain) {
logger.warn(`[AutoVerify] Нет адреса для chainId=${cid} в результате деплоя, пропускаю`);
continue;
}
const guid = await etherscanV2.submitVerification({
chainId: cid,
contractAddress: addrForChain,
contractName,
compilerVersion,
standardJsonInput: standardJson,
constructorArgsHex,
apiKey
});
logger.info(`[AutoVerify] Отправлена верификация в chainId=${cid}, guid=${guid}`);
verificationStore.updateChain(addrForChain, cid, { guid, status: 'submitted' });
} catch (e) {
logger.warn(`[AutoVerify] Ошибка отправки верификации для chainId=${cid}: ${e.message}`);
const addrForChain = netMap.get(cid) || 'unknown';
verificationStore.updateChain(addrForChain, cid, { status: `error: ${e.message}` });
}
}
}
/**
* Формирует стандартный JSON input, compilerVersion, contractName и ABI-кодированные аргументы конструктора
*/
async prepareVerificationPayload(params) {
const hre = require('hardhat');
const path = require('path');
const fs = require('fs');
// 1) Найти самый свежий build-info
const buildInfoDir = path.join(__dirname, '..', 'artifacts', 'build-info');
let latestFile = null;
try {
const entries = fs.readdirSync(buildInfoDir).filter(f => f.endsWith('.json'));
let bestMtime = 0;
for (const f of entries) {
const fp = path.join(buildInfoDir, f);
const st = fs.statSync(fp);
if (st.mtimeMs > bestMtime) { bestMtime = st.mtimeMs; latestFile = fp; }
}
} catch (e) {
logger.warn('Артефакты build-info не найдены:', e.message);
}
let standardJson = null;
let compilerVersion = null;
let sourcePathForDLE = 'contracts/DLE.sol';
let contractName = 'contracts/DLE.sol:DLE';
if (latestFile) {
try {
const buildInfo = JSON.parse(fs.readFileSync(latestFile, 'utf8'));
// input — это стандартный JSON input для solc
standardJson = buildInfo.input || null;
// Версия компилятора
const long = buildInfo.solcLongVersion || buildInfo.solcVersion || hre.config.solidity?.version;
compilerVersion = long ? (long.startsWith('v') ? long : `v${long}`) : undefined;
// Найти путь контракта DLE
if (buildInfo.output && buildInfo.output.contracts) {
for (const [filePathKey, contractsMap] of Object.entries(buildInfo.output.contracts)) {
if (contractsMap && contractsMap['DLE']) {
sourcePathForDLE = filePathKey;
contractName = `${filePathKey}:DLE`;
break;
}
}
}
} catch (e) {
logger.warn('Не удалось прочитать build-info:', e.message);
}
}
// Если не нашли — fallback на config
if (!compilerVersion) compilerVersion = `v${hre.config.solidity.compilers?.[0]?.version || hre.config.solidity.version}`;
if (!standardJson) {
// fallback минимальная структура
standardJson = {
language: 'Solidity',
sources: { [sourcePathForDLE]: { content: '' } },
settings: { optimizer: { enabled: true, runs: 200 } }
};
}
// 2) Посчитать ABI-код аргументов конструктора через сравнение с bytecode
// Конструктор: (dleConfig, currentChainId)
const Factory = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates,
jurisdiction: params.jurisdiction,
oktmo: params.oktmo,
okvedCodes: params.okvedCodes || [],
kpp: params.kpp,
quorumPercentage: params.quorumPercentage,
initialPartners: params.initialPartners,
initialAmounts: params.initialAmounts,
supportedChainIds: params.supportedChainIds
};
const deployTx = await Factory.getDeployTransaction(dleConfig, params.currentChainId);
const fullData = deployTx.data; // 0x + creation bytecode + encoded args
const bytecode = Factory.bytecode; // 0x + creation bytecode
let constructorArgsHex;
try {
if (fullData && bytecode && fullData.startsWith(bytecode)) {
constructorArgsHex = '0x' + fullData.slice(bytecode.length);
}
} catch (e) {
logger.warn('Не удалось выделить constructorArguments из deployTx.data:', e.message);
}
return { standardJson, compilerVersion, contractName, constructorArgsHex };
}
} }
module.exports = new DLEV2Service(); module.exports = new DLEV2Service();

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const axios = require('axios');
const logger = require('../utils/logger');
const ETHERSCAN_V2_ENDPOINT = 'https://api.etherscan.io/v2/api';
class EtherscanV2VerificationService {
/**
* Отправить исходники контракта на верификацию (V2)
* Документация: https://docs.etherscan.io/etherscan-v2/contract-verification/multichain-verification
* @param {Object} opts
* @param {number} opts.chainId
* @param {string} opts.contractAddress
* @param {string} opts.contractName - формат "contracts/DLE.sol:DLE"
* @param {string} opts.compilerVersion - например, "v0.8.24+commit.e11b9ed9"
* @param {Object|string} opts.standardJsonInput - стандартный JSON input (рекомендуется)
* @param {string} [opts.constructorArgsHex]
* @param {string} [opts.apiKey]
* @returns {Promise<string>} guid
*/
async submitVerification({ chainId, contractAddress, contractName, compilerVersion, standardJsonInput, constructorArgsHex, apiKey }) {
const key = apiKey || process.env.ETHERSCAN_API_KEY;
if (!key) throw new Error('ETHERSCAN_API_KEY не задан');
if (!chainId) throw new Error('chainId обязателен');
if (!contractAddress) throw new Error('contractAddress обязателен');
if (!contractName) throw new Error('contractName обязателен');
if (!compilerVersion) throw new Error('compilerVersion обязателен');
if (!standardJsonInput) throw new Error('standardJsonInput обязателен');
const payload = new URLSearchParams();
// Согласно V2, chainid должен передаваться в query, а не в теле формы
payload.set('module', 'contract');
payload.set('action', 'verifysourcecode');
payload.set('apikey', key);
payload.set('codeformat', 'solidity-standard-json-input');
payload.set('sourceCode', typeof standardJsonInput === 'string' ? standardJsonInput : JSON.stringify(standardJsonInput));
payload.set('contractaddress', contractAddress);
payload.set('contractname', contractName);
payload.set('compilerversion', compilerVersion);
if (constructorArgsHex) {
const no0x = constructorArgsHex.startsWith('0x') ? constructorArgsHex.slice(2) : constructorArgsHex;
payload.set('constructorArguments', no0x);
}
const url = `${ETHERSCAN_V2_ENDPOINT}?chainid=${encodeURIComponent(String(chainId))}`;
const { data } = await axios.post(url, payload.toString(), {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
logger.info('[EtherscanV2] verifysourcecode response', data);
if (data && data.status === '1' && data.result) return data.result; // guid
throw new Error(data?.result || data?.message || 'Etherscan V2 verifysourcecode error');
}
/**
* Проверить статус верификации по guid
* @param {number} chainId
* @param {string} guid
* @param {string} [apiKey]
* @returns {Promise<{status:string,message:string,result:string}>}
*/
async checkStatus(chainId, guid, apiKey) {
const key = apiKey || process.env.ETHERSCAN_API_KEY;
if (!key) throw new Error('ETHERSCAN_API_KEY не задан');
const params = new URLSearchParams();
params.set('chainid', String(chainId));
params.set('module', 'contract');
params.set('action', 'checkverifystatus');
params.set('guid', guid);
params.set('apikey', key);
const { data } = await axios.get(`${ETHERSCAN_V2_ENDPOINT}?${params.toString()}`);
logger.info('[EtherscanV2] checkverifystatus response', data);
return data;
}
}
module.exports = new EtherscanV2VerificationService();

View File

@@ -12,6 +12,14 @@
const encryptedDb = require('./encryptedDatabaseService'); const encryptedDb = require('./encryptedDatabaseService');
function normalizeNetworkId(networkId) {
if (!networkId || typeof networkId !== 'string') return networkId;
const v = networkId.trim().toLowerCase();
// Common normalizations
if (v === 'base sepolia testnet' || v === 'base sepolia') return 'base-sepolia';
return v.replace(/\s+/g, '-');
}
async function getAllRpcProviders() { async function getAllRpcProviders() {
const providers = await encryptedDb.getData('rpc_providers', {}, null, 'id'); const providers = await encryptedDb.getData('rpc_providers', {}, null, 'id');
return providers; return providers;
@@ -24,7 +32,7 @@ async function saveAllRpcProviders(rpcConfigs) {
// Сохраняем новые провайдеры // Сохраняем новые провайдеры
for (const cfg of rpcConfigs) { for (const cfg of rpcConfigs) {
await encryptedDb.saveData('rpc_providers', { await encryptedDb.saveData('rpc_providers', {
network_id: cfg.networkId, network_id: normalizeNetworkId(cfg.networkId),
rpc_url: cfg.rpcUrl, rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null chain_id: cfg.chainId || null
}); });
@@ -41,12 +49,12 @@ async function upsertRpcProvider(cfg) {
rpc_url: cfg.rpcUrl, rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null chain_id: cfg.chainId || null
}, { }, {
network_id: cfg.networkId network_id: normalizeNetworkId(cfg.networkId)
}); });
} else { } else {
// Создаем новый провайдер // Создаем новый провайдер
await encryptedDb.saveData('rpc_providers', { await encryptedDb.saveData('rpc_providers', {
network_id: cfg.networkId, network_id: normalizeNetworkId(cfg.networkId),
rpc_url: cfg.rpcUrl, rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null chain_id: cfg.chainId || null
}); });
@@ -58,8 +66,14 @@ async function deleteRpcProvider(networkId) {
} }
async function getRpcUrlByNetworkId(networkId) { async function getRpcUrlByNetworkId(networkId) {
const providers = await encryptedDb.getData('rpc_providers', { network_id: networkId }, 1); // Сначала пробуем точное совпадение (для обратной совместимости)
return providers[0]?.rpc_url || null; let providers = await encryptedDb.getData('rpc_providers', { network_id: networkId }, 1);
if (providers.length > 0) return providers[0].rpc_url || null;
// Затем ищем по нормализованному ключу среди всех записей
const all = await encryptedDb.getData('rpc_providers', {}, null, 'id');
const norm = normalizeNetworkId(networkId);
const found = all.find(p => normalizeNetworkId(p.network_id) === norm);
return found ? found.rpc_url : null;
} }
async function getRpcUrlByChainId(chainId) { async function getRpcUrlByChainId(chainId) {

View File

@@ -0,0 +1,56 @@
/**
* Lightweight encrypted secret store over encryptedDatabaseService
*/
const crypto = require('crypto');
const encryptedDb = require('./encryptedDatabaseService');
const TABLE = 'secrets';
async function getSecret(key) {
const rows = await encryptedDb.getData(TABLE, { key }, 1);
return rows && rows[0] ? rows[0].value : null;
}
async function setSecret(key, value) {
const existing = await encryptedDb.getData(TABLE, { key }, 1);
const payload = { key, value, updated_at: new Date() };
if (existing && existing.length) {
await encryptedDb.saveData(TABLE, payload, { key });
} else {
payload.created_at = new Date();
await encryptedDb.saveData(TABLE, payload);
}
return value;
}
async function getOrCreateCreate2Salt() {
let salt = await getSecret('CREATE2_SALT');
if (salt && /^0x[0-9a-fA-F]{64}$/.test(salt)) return salt;
const hex = crypto.randomBytes(32).toString('hex');
salt = '0x' + hex;
await setSecret('CREATE2_SALT', salt);
return salt;
}
/**
* Генерирует одноразовый CREATE2 salt (0x + 32 байта) и сохраняет в secrets с уникальным ключом
* @param {Object} [opts]
* @param {string} [opts.prefix] Префикс ключа (по умолчанию CREATE2_SALT)
* @param {string} [opts.label] Доп. метка (например, имя DLE)
* @returns {Promise<{ salt: string, key: string }>}
*/
async function createAndStoreNewCreate2Salt(opts = {}) {
const prefix = opts.prefix || 'CREATE2_SALT';
const label = (opts.label || '').replace(/[^a-zA-Z0-9_.:-]/g, '').slice(0, 40);
const hex = crypto.randomBytes(32).toString('hex');
const salt = '0x' + hex;
const rand = crypto.randomBytes(2).toString('hex');
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const key = [prefix, label, ts, rand].filter(Boolean).join(':');
await setSecret(key, salt);
return { salt, key };
}
module.exports = { getSecret, setSecret, getOrCreateCreate2Salt, createAndStoreNewCreate2Salt };

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*/
const path = require('path');
const fs = require('fs');
const baseDir = path.join(__dirname, '../contracts-data/verifications');
function ensureDir() {
if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });
}
function getFilePath(address) {
ensureDir();
const key = String(address || '').toLowerCase();
return path.join(baseDir, `${key}.json`);
}
function read(address) {
const fp = getFilePath(address);
if (!fs.existsSync(fp)) return { address: String(address).toLowerCase(), chains: {} };
try {
return JSON.parse(fs.readFileSync(fp, 'utf8'));
} catch {
return { address: String(address).toLowerCase(), chains: {} };
}
}
function write(address, data) {
const fp = getFilePath(address);
fs.writeFileSync(fp, JSON.stringify(data, null, 2));
}
function updateChain(address, chainId, patch) {
const data = read(address);
if (!data.chains) data.chains = {};
const cid = String(chainId);
data.chains[cid] = { ...(data.chains[cid] || {}), ...patch, chainId: Number(chainId), updatedAt: new Date().toISOString() };
write(address, data);
return data;
}
module.exports = { read, write, updateChain };

View File

@@ -1,73 +0,0 @@
#!/bin/bash
# Скрипт для расшифровки всех таблиц
# Использование: ./decrypt-all-tables.sh
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
echo "🔓 Расшифровка всех таблиц..."
# Получаем список всех таблиц
TABLES=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;")
# Функция для расшифровки таблицы
decrypt_table() {
local table_name="$1"
echo "🔓 Расшифровка таблицы: $table_name"
# Получаем зашифрованные колонки
local encrypted_columns=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name
FROM information_schema.columns
WHERE table_name = '$table_name'
AND table_schema = 'public'
AND column_name LIKE '%_encrypted'
ORDER BY ordinal_position;")
if [ -z "$encrypted_columns" ]; then
echo " ⏭️ Нет зашифрованных колонок"
return
fi
echo " 📝 Зашифрованные колонки:"
echo "$encrypted_columns" | while read -r column_name; do
if [ -n "$column_name" ]; then
echo " $column_name"
# Определяем тип колонки
data_type=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT data_type FROM information_schema.columns
WHERE table_name = '$table_name' AND column_name = '$column_name' AND table_schema = 'public';" | xargs)
# Определяем первичный ключ для таблицы
primary_key=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name FROM information_schema.key_column_usage
WHERE table_name = '$table_name' AND constraint_name LIKE '%_pkey'
AND table_schema = 'public' LIMIT 1;" | xargs)
if [ "$data_type" = "jsonb" ] || [ "$data_type" = "json" ]; then
# Расшифровываем json/jsonb
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT $primary_key, decrypt_json($column_name, '$ENCRYPTION_KEY') as ${column_name%_encrypted}_decrypted
FROM $table_name WHERE $column_name IS NOT NULL LIMIT 5;"
else
# Расшифровываем текстовые
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT $primary_key, decrypt_text($column_name, '$ENCRYPTION_KEY') as ${column_name%_encrypted}_decrypted
FROM $table_name WHERE $column_name IS NOT NULL LIMIT 5;"
fi
fi
done
}
# Расшифровываем каждую таблицу
echo "$TABLES" | while read -r table_name; do
if [ -n "$table_name" ]; then
decrypt_table "$table_name"
fi
done
echo "✅ Расшифровка завершена!"

View File

@@ -1,307 +0,0 @@
#!/bin/bash
# Скрипт для полного шифрования всех таблиц в базе данных DLE
# Использование: ./encrypt-all-tables.sh
# Проверяем наличие OpenSSL
if ! command -v openssl &> /dev/null; then
echo "❌ OpenSSL не установлен. Установите: sudo apt-get install openssl"
exit 1
fi
# Создаём папку для ключей
mkdir -p ./ssl/keys
# Генерируем ключ шифрования (если его нет)
if [ ! -f "./ssl/keys/full_db_encryption.key" ]; then
echo "🔑 Генерация ключа шифрования для всех таблиц..."
openssl rand -base64 32 > ./ssl/keys/full_db_encryption.key
chmod 600 ./ssl/keys/full_db_encryption.key
echo "✅ Ключ создан: ./ssl/keys/full_db_encryption.key"
fi
echo "🔒 Полное шифрование всех таблиц в базе данных..."
# Проверяем подключение к БД
if ! docker exec dapp-postgres pg_isready -U dapp_user -d dapp_db > /dev/null 2>&1; then
echo "❌ Не удалось подключиться к базе данных"
exit 1
fi
# Создаём функции шифрования в PostgreSQL
echo "📝 Создание функций шифрования в PostgreSQL..."
docker exec dapp-postgres psql -U dapp_user -d dapp_db << 'EOF'
-- Создаём расширение для шифрования
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Функция для шифрования текста
CREATE OR REPLACE FUNCTION encrypt_text(data text, key text)
RETURNS text AS $$
BEGIN
IF data IS NULL THEN
RETURN NULL;
END IF;
RETURN encode(encrypt_iv(data::bytea, decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'base64');
END;
$$ LANGUAGE plpgsql;
-- Функция для расшифровки текста
CREATE OR REPLACE FUNCTION decrypt_text(encrypted_data text, key text)
RETURNS text AS $$
BEGIN
IF encrypted_data IS NULL THEN
RETURN NULL;
END IF;
RETURN convert_from(decrypt_iv(decode(encrypted_data, 'base64'), decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'utf8');
END;
$$ LANGUAGE plpgsql;
-- Функция для шифрования JSON
CREATE OR REPLACE FUNCTION encrypt_json(data jsonb, key text)
RETURNS text AS $$
BEGIN
IF data IS NULL THEN
RETURN NULL;
END IF;
RETURN encode(encrypt_iv(data::text::bytea, decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'base64');
END;
$$ LANGUAGE plpgsql;
-- Функция для расшифровки JSON
CREATE OR REPLACE FUNCTION decrypt_json(encrypted_data text, key text)
RETURNS jsonb AS $$
BEGIN
IF encrypted_data IS NULL THEN
RETURN NULL;
END IF;
RETURN convert_from(decrypt_iv(decode(encrypted_data, 'base64'), decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'utf8')::jsonb;
END;
$$ LANGUAGE plpgsql;
EOF
# Читаем ключ шифрования
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
echo "🔐 Начинаем шифрование всех таблиц..."
# Получаем список всех таблиц
TABLES=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;")
echo "📋 Найдены таблицы для шифрования:"
echo "$TABLES"
# Функция для шифрования таблицы
encrypt_table() {
local table_name="$1"
echo "🔐 Шифрование таблицы: $table_name"
# Получаем информацию о колонках
local columns=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = '$table_name'
AND table_schema = 'public'
AND data_type IN ('text', 'varchar', 'character varying', 'json', 'jsonb')
AND column_name NOT LIKE '%_encrypted'
AND column_name NOT IN ('created_at', 'updated_at', 'id')
ORDER BY ordinal_position;")
if [ -z "$columns" ]; then
echo " ⏭️ Нет текстовых колонок для шифрования"
return
fi
echo " 📝 Колонки для шифрования:"
echo "$columns" | while read -r column_info; do
if [ -n "$column_info" ]; then
echo " $column_info"
fi
done
# Создаём зашифрованные колонки и шифруем данные
echo "$columns" | while read -r column_info; do
if [ -n "$column_info" ]; then
column_name=$(echo "$column_info" | awk '{print $1}')
data_type=$(echo "$column_info" | awk '{print $2}')
echo " 🔐 Шифрование колонки: $column_name"
# Добавляем зашифрованную колонку
if [ "$data_type" = "jsonb" ] || [ "$data_type" = "json" ]; then
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
ALTER TABLE $table_name ADD COLUMN IF NOT EXISTS ${column_name}_encrypted TEXT;
UPDATE $table_name
SET ${column_name}_encrypted = encrypt_json($column_name, '$ENCRYPTION_KEY')
WHERE $column_name IS NOT NULL AND ${column_name}_encrypted IS NULL;"
else
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
ALTER TABLE $table_name ADD COLUMN IF NOT EXISTS ${column_name}_encrypted TEXT;
UPDATE $table_name
SET ${column_name}_encrypted = encrypt_text($column_name, '$ENCRYPTION_KEY')
WHERE $column_name IS NOT NULL AND ${column_name}_encrypted IS NULL;"
fi
fi
done
}
# Шифруем каждую таблицу
echo "$TABLES" | while read -r table_name; do
if [ -n "$table_name" ]; then
encrypt_table "$table_name"
fi
done
echo "✅ Шифрование всех таблиц завершено!"
# Создаём скрипт для расшифровки
cat > decrypt-all-tables.sh << 'EOF'
#!/bin/bash
# Скрипт для расшифровки всех таблиц
# Использование: ./decrypt-all-tables.sh
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
echo "🔓 Расшифровка всех таблиц..."
# Получаем список всех таблиц
TABLES=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;")
# Функция для расшифровки таблицы
decrypt_table() {
local table_name="$1"
echo "🔓 Расшифровка таблицы: $table_name"
# Получаем зашифрованные колонки
local encrypted_columns=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name
FROM information_schema.columns
WHERE table_name = '$table_name'
AND table_schema = 'public'
AND column_name LIKE '%_encrypted'
ORDER BY ordinal_position;")
if [ -z "$encrypted_columns" ]; then
echo " ⏭️ Нет зашифрованных колонок"
return
fi
echo " 📝 Зашифрованные колонки:"
echo "$encrypted_columns" | while read -r column_name; do
if [ -n "$column_name" ]; then
echo " $column_name"
# Определяем тип колонки
data_type=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT data_type FROM information_schema.columns
WHERE table_name = '$table_name' AND column_name = '$column_name' AND table_schema = 'public';" | xargs)
if [ "$data_type" = "jsonb" ] || [ "$data_type" = "json" ]; then
# Расшифровываем json/jsonb
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT id, decrypt_json($column_name, '$ENCRYPTION_KEY') as ${column_name%_encrypted}_decrypted
FROM $table_name WHERE $column_name IS NOT NULL LIMIT 5;"
else
# Расшифровываем текстовые
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT id, decrypt_text($column_name, '$ENCRYPTION_KEY') as ${column_name%_encrypted}_decrypted
FROM $table_name WHERE $column_name IS NOT NULL LIMIT 5;"
fi
fi
done
}
# Расшифровываем каждую таблицу
echo "$TABLES" | while read -r table_name; do
if [ -n "$table_name" ]; then
decrypt_table "$table_name"
fi
done
echo "✅ Расшифровка завершена!"
EOF
chmod +x decrypt-all-tables.sh
# Создаём скрипт для удаления незашифрованных колонок
cat > remove-unencrypted-columns.sh << 'EOF'
#!/bin/bash
# Скрипт для удаления незашифрованных колонок
# ВНИМАНИЕ: Это необратимая операция!
# Использование: ./remove-unencrypted-columns.sh
echo "⚠️ ВНИМАНИЕ: Это удалит все незашифрованные колонки!"
echo "Убедитесь, что шифрование работает корректно!"
read -p "Продолжить? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "❌ Операция отменена"
exit 1
fi
# Получаем список всех таблиц
TABLES=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;")
# Удаляем незашифрованные колонки
echo "$TABLES" | while read -r table_name; do
if [ -n "$table_name" ]; then
echo "🗑️ Удаление незашифрованных колонок в таблице: $table_name"
# Получаем незашифрованные колонки
local unencrypted_columns=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name
FROM information_schema.columns
WHERE table_name = '$table_name'
AND table_schema = 'public'
AND data_type IN ('text', 'varchar', 'character varying', 'json', 'jsonb')
AND column_name NOT LIKE '%_encrypted'
AND column_name NOT IN ('created_at', 'updated_at', 'id')
ORDER BY ordinal_position;")
echo "$unencrypted_columns" | while read -r column_name; do
if [ -n "$column_name" ]; then
echo " 🗑️ Удаление колонки: $column_name"
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
ALTER TABLE $table_name DROP COLUMN IF EXISTS $column_name;"
fi
done
fi
done
echo "✅ Незашифрованные колонки удалены!"
EOF
chmod +x remove-unencrypted-columns.sh
echo ""
echo "🎯 Что было сделано:"
echo "1. ✅ Создан ключ шифрования: ./ssl/keys/full_db_encryption.key"
echo "2. ✅ Добавлены функции шифрования в PostgreSQL"
echo "3. ✅ Зашифрованы ВСЕ текстовые колонки во ВСЕХ таблицах"
echo "4. ✅ Создан скрипт расшифровки: ./decrypt-all-tables.sh"
echo "5. ✅ Создан скрипт удаления: ./remove-unencrypted-columns.sh"
echo ""
echo "⚠️ ВАЖНО:"
echo "- Ключ шифрования: ./ssl/keys/full_db_encryption.key"
echo "- Храните ключ в безопасном месте!"
echo "- Сделайте резервную копию ключа!"
echo ""
echo "🔧 Следующие шаги:"
echo "1. Протестируйте расшифровку: ./decrypt-all-tables.sh"
echo "2. Обновите код приложения для работы с зашифрованными данными"
echo "3. После проверки удалите незашифрованные колонки: ./remove-unencrypted-columns.sh"

View File

@@ -75,6 +75,15 @@
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
</a> </a>
</div> </div>
<div class="detail-item" v-if="dle.networks && dle.networks.length">
<strong>Адреса по сетям:</strong>
<ul class="networks-list">
<li v-for="net in dle.networks" :key="net.chainId">
Chain {{ net.chainId }}:
<span class="address">{{ shortenAddress(net.address) }}</span>
</li>
</ul>
</div>
<div class="detail-item"> <div class="detail-item">
<strong>Местоположение:</strong> {{ dle.location }} <strong>Местоположение:</strong> {{ dle.location }}
</div> </div>
@@ -91,6 +100,15 @@
<strong>Статус:</strong> <strong>Статус:</strong>
<span class="status active">Активен</span> <span class="status active">Активен</span>
</div> </div>
<div class="detail-item" v-if="verificationStatuses[dle.dleAddress]">
<strong>Верификация:</strong>
<ul class="verify-list">
<li v-for="(info, chainId) in verificationStatuses[dle.dleAddress].chains" :key="chainId">
Chain {{ chainId }}: {{ info.status || '' }}<span v-if="info.guid"> (guid: {{ info.guid.slice(0,8) }})</span>
</li>
</ul>
<button class="details-btn btn-sm" @click.stop="refreshVerification(dle.dleAddress)">Обновить статус</button>
</div>
</div> </div>
@@ -175,7 +193,7 @@
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits, ref, onMounted } from 'vue'; import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import BaseLayout from '../components/BaseLayout.vue'; import BaseLayout from '../components/BaseLayout.vue';
import api from '@/api/axios'; import api from '@/api/axios';
@@ -197,6 +215,8 @@ const router = useRouter();
const deployedDles = ref([]); const deployedDles = ref([]);
const isLoadingDles = ref(false); const isLoadingDles = ref(false);
const selectedDle = ref(null); const selectedDle = ref(null);
const verificationStatuses = ref({}); // { [address]: { address, chains: { [chainId]: { guid, status } } } }
let verifyPollTimer = null;
@@ -307,6 +327,18 @@ async function loadDeployedDles() {
deployedDles.value = dlesWithBlockchainData; deployedDles.value = dlesWithBlockchainData;
console.log('[ManagementView] Итоговый список DLE:', deployedDles.value); console.log('[ManagementView] Итоговый список DLE:', deployedDles.value);
// Подгружаем статусы верификации для всех адресов
for (const dle of deployedDles.value) {
try {
const st = await api.get(`/dle-v2/verify/status/${dle.dleAddress}`);
if (st.data?.success && st.data.data) {
verificationStatuses.value[dle.dleAddress] = st.data.data;
}
} catch (e) {
// no-op
}
}
} else { } else {
console.error('[ManagementView] Ошибка при загрузке DLE:', response.data.message); console.error('[ManagementView] Ошибка при загрузке DLE:', response.data.message);
deployedDles.value = []; deployedDles.value = [];
@@ -339,6 +371,36 @@ function selectDle(dle) {
console.log('Выбран DLE:', dle); console.log('Выбран DLE:', dle);
} }
async function refreshVerification(address) {
try {
const resp = await api.post(`/dle-v2/verify/refresh/${address}`, {});
if (resp.data?.success && resp.data.data) {
verificationStatuses.value[address] = resp.data.data;
}
} catch (e) {
// no-op
}
}
function isTerminalStatus(status) {
if (!status) return false;
const s = String(status).toLowerCase();
return s.includes('pass') || s.includes('verified') || s.startsWith('error');
}
async function pollVerifications() {
try {
const addresses = Object.keys(verificationStatuses.value || {});
for (const addr of addresses) {
const chains = verificationStatuses.value[addr]?.chains || {};
const hasPending = Object.values(chains).some((c) => !isTerminalStatus(c.status));
if (hasPending) {
await refreshVerification(addr);
}
}
} catch {}
}
// function openMultisig() { // function openMultisig() {
// router.push('/management/multisig'); // router.push('/management/multisig');
// } // }
@@ -402,6 +464,14 @@ function openSettingsWithDle() {
onMounted(() => { onMounted(() => {
loadDeployedDles(); loadDeployedDles();
verifyPollTimer = setInterval(pollVerifications, 15000);
});
onBeforeUnmount(() => {
if (verifyPollTimer) {
clearInterval(verifyPollTimer);
verifyPollTimer = null;
}
}); });
</script> </script>

View File

@@ -558,6 +558,29 @@
</div> </div>
</div> </div>
<!-- Ключ блокчейн-скана (Etherscan V2) -->
<div v-if="selectedNetworks.length > 0" class="preview-item explorer-keys-inline">
<div class="explorer-unified-key">
<label class="explorer-key-label">Ключ блокчейн-скана (Etherscan V2, единый для всех сетей)</label>
<div class="explorer-key-input">
<input
:type="unifiedScanKeyVisible ? 'text' : 'password'"
class="form-control"
placeholder="Введите единый APIключ Etherscan V2"
v-model="etherscanApiKey"
autocomplete="off"
/>
<button type="button" class="btn btn-secondary btn-sm"
@click="unifiedScanKeyVisible = !unifiedScanKeyVisible">
{{ unifiedScanKeyVisible ? 'Скрыть' : 'Показать' }}
</button>
</div>
<div class="explorer-keys-actions">
<label><input type="checkbox" v-model="autoVerifyAfterDeploy" /> Авто-верификация после деплоя</label>
</div>
</div>
</div>
<!-- Требования к балансу --> <!-- Требования к балансу -->
<div v-if="selectedNetworks.length > 0" class="balance-requirements"> <div v-if="selectedNetworks.length > 0" class="balance-requirements">
<h5>💰 Требования к балансу:</h5> <h5>💰 Требования к балансу:</h5>
@@ -684,61 +707,10 @@
<strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }} <strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }}
</div> </div>
<!-- Предсказанные адреса (CREATE2) --> <!-- Предсказанные адреса скрыты, чтобы не создавать шум при отсутствии данных -->
<div class="preview-item predicted-addresses">
<div class="predicted-header">
<strong>📍 Предсказанные адреса DLE:</strong>
</div>
<ul class="networks-list" v-if="Object.keys(predictedAddresses).length">
<li v-for="net in selectedNetworkDetails" :key="net.chainId">
{{ net.name }} ({{ net.chainId }}):
<code class="addr">{{ predictedAddresses[net.chainId] || '—' }}</code>
<button
v-if="predictedAddresses[net.chainId]"
type="button"
class="btn btn-xs btn-outline-secondary"
@click="copyToClipboard(predictedAddresses[net.chainId])"
>Копировать</button>
</li>
</ul>
<small class="text-muted" v-else>Адреса вычисляются автоматически при выборе сетей.</small>
</div>
</div> </div>
<!-- Ключи блокчейн-сканов (опционально) -->
<div v-if="selectedNetworks.length > 0" class="preview-section explorer-keys-section">
<h4>🧩 Ключи блокчейн-сканов (опционально для авто-верификации)</h4>
<div class="explorer-keys-grid">
<div
v-for="network in selectedNetworkDetails"
:key="network.chainId"
class="explorer-key-item"
>
<label class="explorer-key-label">
{{ network.name }} (Chain ID: {{ network.chainId }})
</label>
<div class="explorer-key-input">
<input
:type="explorerKeyVisibility[network.chainId] ? 'text' : 'password'"
class="form-control"
:placeholder="`API ключ скана для ${network.name}`"
v-model="explorerApiKeys[network.chainId]"
autocomplete="off"
/>
<button type="button" class="btn btn-secondary btn-sm"
@click="explorerKeyVisibility[network.chainId] = !explorerKeyVisibility[network.chainId]">
{{ explorerKeyVisibility[network.chainId] ? 'Скрыть' : 'Показать' }}
</button>
<button type="button" class="btn btn-outline-danger btn-sm" @click="explorerApiKeys[network.chainId] = ''">
Очистить
</button>
</div>
</div>
</div>
<div class="explorer-keys-actions">
<label><input type="checkbox" v-model="persistExplorerKeys" /> Сохранить локально до конца деплоя</label>
</div>
</div>
<!-- Приватный ключ --> <!-- Приватный ключ -->
<div v-if="hasSelectedNetworks && unifiedPrivateKey" class="preview-section"> <div v-if="hasSelectedNetworks && unifiedPrivateKey" class="preview-section">
@@ -752,6 +724,8 @@
<strong>📍 Адрес кошелька:</strong> {{ keyValidation.unified.address.substring(0, 10) }}...{{ keyValidation.unified.address.substring(keyValidation.unified.address.length - 8) }} <strong>📍 Адрес кошелька:</strong> {{ keyValidation.unified.address.substring(0, 10) }}...{{ keyValidation.unified.address.substring(keyValidation.unified.address.length - 8) }}
</div> </div>
<div class="preview-item"> <div class="preview-item">
<strong>💰 Требуемый баланс:</strong> ~${{ totalDeployCost.toFixed(2) }} <strong>💰 Требуемый баланс:</strong> ~${{ totalDeployCost.toFixed(2) }}
</div> </div>
@@ -961,10 +935,11 @@ const predictedAddress = ref('');
const predictedAddresses = reactive({}); // { chainId: address } const predictedAddresses = reactive({}); // { chainId: address }
const isPredicting = ref(false); const isPredicting = ref(false);
// Ключи блокчейн-сканов (локально) // Ключ блокчейн-скана (единый Etherscan V2)
const explorerApiKeys = reactive({}); // { [chainId]: apiKey } // Единый ключ Etherscan V2 и авто-верификация
const explorerKeyVisibility = reactive({}); const etherscanApiKey = ref('');
const persistExplorerKeys = ref(false); const unifiedScanKeyVisible = ref(false);
const autoVerifyAfterDeploy = ref(true);
// Состояние для приватных ключей // Состояние для приватных ключей
const useSameKeyForAllChains = ref(true); const useSameKeyForAllChains = ref(true);
@@ -1020,32 +995,11 @@ const hasSelectedNetworks = computed(() => {
return selectedNetworks.value.length > 0; return selectedNetworks.value.length > 0;
}); });
// Инициализация полей ключей при смене выбранных сетей // Инициализация при смене выбранных сетей
watch(selectedNetworkDetails, (nets) => { watch(selectedNetworkDetails, (nets) => {
nets.forEach(n => {
if (!(n.chainId in explorerKeyVisibility)) explorerKeyVisibility[n.chainId] = false;
if (persistExplorerKeys.value) {
const saved = localStorage.getItem(`scan_key_${n.chainId}`);
if (saved && !explorerApiKeys[n.chainId]) explorerApiKeys[n.chainId] = saved;
}
});
if (nets && nets.length > 0) predictAddresses(); if (nets && nets.length > 0) predictAddresses();
}, { immediate: true }); }, { immediate: true });
watch(persistExplorerKeys, (val) => {
if (!val) return;
Object.entries(explorerApiKeys).forEach(([chainId, key]) => {
if (key) localStorage.setItem(`scan_key_${chainId}`, key);
});
});
function clearExplorerKeys() {
Object.keys(explorerApiKeys).forEach((k) => explorerApiKeys[k] = '');
Object.keys(localStorage)
.filter(k => k.startsWith('scan_key_'))
.forEach(k => localStorage.removeItem(k));
}
// Предсказание адресов (упрощенно через бэкенд) // Предсказание адресов (упрощенно через бэкенд)
async function predictAddresses() { async function predictAddresses() {
try { try {
@@ -1425,7 +1379,11 @@ const saveFormData = () => {
privateKeys: { ...privateKeys }, privateKeys: { ...privateKeys },
privateKeyVisibility: { ...privateKeyVisibility }, privateKeyVisibility: { ...privateKeyVisibility },
keyValidation: { ...keyValidation }, keyValidation: { ...keyValidation },
showUnifiedKey: showUnifiedKey.value showUnifiedKey: showUnifiedKey.value,
// Ключи сканов/автоверификация
etherscanApiKey: etherscanApiKey.value,
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value,
unifiedScanKeyVisible: unifiedScanKeyVisible.value
}; };
localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave)); localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave));
console.log('[DleDeployForm] Данные формы сохранены в localStorage'); console.log('[DleDeployForm] Данные формы сохранены в localStorage');
@@ -1496,6 +1454,11 @@ const loadFormData = () => {
Object.assign(keyValidation, parsedData.keyValidation || {}); Object.assign(keyValidation, parsedData.keyValidation || {});
showUnifiedKey.value = parsedData.showUnifiedKey || false; showUnifiedKey.value = parsedData.showUnifiedKey || false;
// Восстанавливаем ключи сканов/автопараметры
etherscanApiKey.value = parsedData.etherscanApiKey || '';
autoVerifyAfterDeploy.value = !!parsedData.autoVerifyAfterDeploy;
unifiedScanKeyVisible.value = !!parsedData.unifiedScanKeyVisible;
console.log('[DleDeployForm] Данные формы восстановлены из localStorage'); console.log('[DleDeployForm] Данные формы восстановлены из localStorage');
console.log('[DleDeployForm] Coordinates loaded:', dleSettings.coordinates); console.log('[DleDeployForm] Coordinates loaded:', dleSettings.coordinates);
return true; return true;
@@ -2218,6 +2181,14 @@ watch([selectedOkvedLevel1, selectedOkvedLevel2, postalCodeInput], () => {
}, 100); }, 100);
}); });
// Сохраняем Etherscan API ключ и флаг авто-верификации при изменении
watch(etherscanApiKey, () => {
saveFormData();
});
watch(autoVerifyAfterDeploy, () => {
saveFormData();
});
// Watcher для координат // Watcher для координат
watch(() => dleSettings.coordinates, (newCoordinates) => { watch(() => dleSettings.coordinates, (newCoordinates) => {
console.log('[Coordinates Watcher] Coordinates changed:', newCoordinates); console.log('[Coordinates Watcher] Coordinates changed:', newCoordinates);
@@ -2424,11 +2395,35 @@ const deploySmartContracts = async () => {
// Приватный ключ для деплоя // Приватный ключ для деплоя
privateKey: unifiedPrivateKey.value, privateKey: unifiedPrivateKey.value,
explorerApiKeys: explorerApiKeys // Верификация через Etherscan V2
etherscanApiKey: etherscanApiKey.value,
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
}; };
console.log('Данные для деплоя DLE:', deployData); console.log('Данные для деплоя DLE:', deployData);
// Предварительная проверка балансов во всех сетях
deployProgress.value = 20;
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
try {
const pre = await axios.post('/dle-v2/precheck', {
supportedChainIds: deployData.supportedChainIds,
privateKey: deployData.privateKey
});
const preData = pre.data?.data;
if (pre.data?.success && preData) {
const lacks = (preData.insufficient || []);
if (lacks.length > 0) {
const lines = (preData.balances || []).map(b => `- Chain ${b.chainId}: ${b.balanceEth} ETH${b.ok ? '' : ' (недостаточно)'}`);
alert('Недостаточно средств в некоторых сетях:\n' + lines.join('\n'));
showDeployProgress.value = false;
return;
}
}
} catch (e) {
// Если precheck недоступен, не блокируем — продолжаем
}
deployProgress.value = 30; deployProgress.value = 30;
deployStatus.value = 'Отправка данных на сервер...'; deployStatus.value = 'Отправка данных на сервер...';
@@ -2443,7 +2438,7 @@ const deploySmartContracts = async () => {
deployStatus.value = '✅ DLE успешно развернут!'; deployStatus.value = '✅ DLE успешно развернут!';
// Сохраняем адрес контракта // Сохраняем адрес контракта
dleSettings.predictedAddress = response.data.data?.contractAddress || 'Адрес будет доступен после деплоя'; dleSettings.predictedAddress = response.data.data?.dleAddress || 'Адрес будет доступен после деплоя';
// Небольшая задержка для показа успешного завершения // Небольшая задержка для показа успешного завершения
setTimeout(() => { setTimeout(() => {
@@ -2451,12 +2446,6 @@ const deploySmartContracts = async () => {
// Перенаправляем на главную страницу управления // Перенаправляем на главную страницу управления
router.push('/management'); router.push('/management');
}, 2000); }, 2000);
if (!persistExplorerKeys.value) {
Object.keys(explorerApiKeys).forEach((k) => explorerApiKeys[k] = '');
Object.keys(localStorage)
.filter(k => k.startsWith('scan_key_'))
.forEach(k => localStorage.removeItem(k));
}
} else { } else {
showDeployProgress.value = false; showDeployProgress.value = false;

View File

@@ -98,7 +98,7 @@ async function removeRpc(index) {
if (!rpc) return; if (!rpc) return;
if (!confirm(`Удалить RPC для сети ${rpc.networkId}?`)) return; if (!confirm(`Удалить RPC для сети ${rpc.networkId}?`)) return;
try { try {
await api.delete(`/api/settings/rpc/${rpc.networkId}`); await api.delete(`/settings/rpc/${rpc.networkId}`);
emit('update'); emit('update');
} catch (e) { } catch (e) {
alert('Ошибка при удалении RPC: ' + (e.response?.data?.error || e.message)); alert('Ошибка при удалении RPC: ' + (e.response?.data?.error || e.message));

View File

@@ -1,50 +0,0 @@
#!/bin/bash
# Скрипт для удаления незашифрованных колонок
# ВНИМАНИЕ: Это необратимая операция!
# Использование: ./remove-unencrypted-columns.sh
echo "⚠️ ВНИМАНИЕ: Это удалит все незашифрованные колонки!"
echo "Убедитесь, что шифрование работает корректно!"
read -p "Продолжить? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "❌ Операция отменена"
exit 1
fi
# Получаем список всех таблиц
TABLES=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;")
# Удаляем незашифрованные колонки
echo "$TABLES" | while read -r table_name; do
if [ -n "$table_name" ]; then
echo "🗑️ Удаление незашифрованных колонок в таблице: $table_name"
# Получаем незашифрованные колонки
unencrypted_columns=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name
FROM information_schema.columns
WHERE table_name = '$table_name'
AND table_schema = 'public'
AND data_type IN ('text', 'varchar', 'character varying', 'json', 'jsonb')
AND column_name NOT LIKE '%_encrypted'
AND column_name NOT IN ('created_at', 'updated_at', 'id')
ORDER BY ordinal_position;")
echo "$unencrypted_columns" | while read -r column_name; do
if [ -n "$column_name" ]; then
echo " 🗑️ Удаление колонки: $column_name"
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
ALTER TABLE $table_name DROP COLUMN IF EXISTS $column_name;"
fi
done
fi
done
echo "✅ Незашифрованные колонки удалены!"

View File

@@ -0,0 +1,23 @@
#!/bin/bash
if ! docker exec dapp-postgres pg_isready -U dapp_user -d dapp_db > /dev/null 2>&1; then
exit 1
fi
if [ ! -f "./ssl/keys/full_db_encryption.key" ]; then
exit 1
fi
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id)
VALUES
(encrypt_text('sepolia', '$ENCRYPTION_KEY'), encrypt_text('https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', '$ENCRYPTION_KEY'), 11155111),
(encrypt_text('holesky', '$ENCRYPTION_KEY'), encrypt_text('https://ethereum-holesky.publicnode.com', '$ENCRYPTION_KEY'), 17000)
ON CONFLICT DO NOTHING;"
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
INSERT INTO auth_tokens (name_encrypted, address_encrypted, network_encrypted, min_balance)
VALUES
(encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('holesky', '$ENCRYPTION_KEY'), 1.0),
(encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('sepolia', '$ENCRYPTION_KEY'), 1.0)
ON CONFLICT DO NOTHING;"

113
setup.sh
View File

@@ -63,40 +63,35 @@ print_no_git_instructions() {
print_yellow "4. Запустите этот скрипт: ./setup.sh" print_yellow "4. Запустите этот скрипт: ./setup.sh"
} }
# Проверка и создание .env файлов # Все настройки хранятся в зашифрованной базе данных
check_env_files() {
print_blue "Проверка наличия файлов конфигурации..."
# Проверяем backend/.env # Создание ключа шифрования
if [ ! -f backend/.env ]; then create_encryption_key() {
if [ -f backend/.env.example ]; then print_blue "Проверка ключа шифрования..."
print_yellow "Файл backend/.env не найден. Создаю из примера..."
cp backend/.env.example backend/.env # Проверяем наличие OpenSSL
print_green "Файл backend/.env создан. Рекомендуется настроить его вручную." if ! command -v openssl &> /dev/null; then
print_yellow "OpenSSL не установлен. Установка..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
sudo apt-get update && sudo apt-get install -y openssl
else else
print_red "Файл backend/.env.example не найден. Невозможно создать файл конфигурации." print_red "Пожалуйста, установите OpenSSL вручную: https://www.openssl.org/"
exit 1 exit 1
fi fi
else
print_green "Файл backend/.env уже существует."
fi fi
# Проверяем frontend/.env # Создаём папку для ключей
if [ ! -f frontend/.env ]; then mkdir -p ./ssl/keys
if [ -f frontend/.env.example ]; then
print_yellow "Файл frontend/.env не найден. Создаю из примера..."
cp frontend/.env.example frontend/.env
print_green "Файл frontend/.env создан. Рекомендуется настроить его вручную."
else
print_red "Файл frontend/.env.example не найден. Невозможно создать файл конфигурации."
exit 1
fi
else
print_green "Файл frontend/.env уже существует."
fi
print_blue "Проверка файлов конфигурации завершена." # Генерируем ключ шифрования (если его нет)
print_yellow "ВАЖНО: По соображениям безопасности используйте свои значения для паролей и ключей в .env файлах." if [ ! -f "./ssl/keys/full_db_encryption.key" ]; then
print_blue "🔑 Генерация ключа шифрования..."
openssl rand -base64 32 > ./ssl/keys/full_db_encryption.key
chmod 600 ./ssl/keys/full_db_encryption.key
print_green "✅ Ключ создан: ./ssl/keys/full_db_encryption.key"
else
print_green "✅ Ключ шифрования уже существует."
fi
} }
# Предварительная загрузка образов # Предварительная загрузка образов
@@ -116,6 +111,10 @@ pull_images() {
done done
} }
# Запуск проекта # Запуск проекта
start_project() { start_project() {
print_blue "Запуск проекта..." print_blue "Запуск проекта..."
@@ -161,6 +160,61 @@ start_project() {
print_green "✅ Модели предзагружены и останутся в памяти!" print_green "✅ Модели предзагружены и останутся в памяти!"
fi fi
# Добавляем токены аутентификации
print_blue "🔑 Добавление токенов аутентификации..."
./scripts/internal/db/db_init_helper.sh 2>/dev/null || print_yellow "Токены уже добавлены или скрипт недоступен"
# Создаём функции шифрования в PostgreSQL
print_blue "📝 Создание функций шифрования в PostgreSQL..."
docker exec dapp-postgres psql -U dapp_user -d dapp_db << 'EOF' 2>/dev/null || print_yellow "Функции шифрования уже существуют или БД не готова"
-- Создаём расширение для шифрования
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Функция для шифрования текста
CREATE OR REPLACE FUNCTION encrypt_text(data text, key text)
RETURNS text AS $$
BEGIN
IF data IS NULL THEN
RETURN NULL;
END IF;
RETURN encode(encrypt_iv(data::bytea, decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'base64');
END;
$$ LANGUAGE plpgsql;
-- Функция для расшифровки текста
CREATE OR REPLACE FUNCTION decrypt_text(encrypted_data text, key text)
RETURNS text AS $$
BEGIN
IF encrypted_data IS NULL THEN
RETURN NULL;
END IF;
RETURN convert_from(decrypt_iv(decode(encrypted_data, 'base64'), decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'utf8');
END;
$$ LANGUAGE plpgsql;
-- Функция для шифрования JSON
CREATE OR REPLACE FUNCTION encrypt_json(data jsonb, key text)
RETURNS text AS $$
BEGIN
IF data IS NULL THEN
RETURN NULL;
END IF;
RETURN encode(encrypt_iv(data::text::bytea, decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'base64');
END;
$$ LANGUAGE plpgsql;
-- Функция для расшифровки JSON
CREATE OR REPLACE FUNCTION decrypt_json(encrypted_data text, key text)
RETURNS jsonb AS $$
BEGIN
IF encrypted_data IS NULL THEN
RETURN NULL;
END IF;
RETURN convert_from(decrypt_iv(decode(encrypted_data, 'base64'), decode(key, 'base64'), decode('000102030405060708090A0B0C0D0E0F', 'hex'), 'aes-cbc'), 'utf8')::jsonb;
END;
$$ LANGUAGE plpgsql;
EOF
print_green "----------------------------------------" print_green "----------------------------------------"
print_green "Проект Digital_Legal_Entity(DLE) доступен по адресам:" print_green "Проект Digital_Legal_Entity(DLE) доступен по адресам:"
print_green "Frontend: http://localhost:5173" print_green "Frontend: http://localhost:5173"
@@ -168,6 +222,9 @@ start_project() {
print_green "Ollama API: http://localhost:11434" print_green "Ollama API: http://localhost:11434"
print_green "PostgreSQL: localhost:5432" print_green "PostgreSQL: localhost:5432"
print_green "----------------------------------------" print_green "----------------------------------------"
print_green "🔐 Ключ шифрования: ./ssl/keys/full_db_encryption.key"
print_green "📋 Все настройки хранятся в зашифрованной базе данных"
print_green "----------------------------------------"
print_green "ИИ-ассистент готов к работе!" print_green "ИИ-ассистент готов к работе!"
print_green "----------------------------------------" print_green "----------------------------------------"
else else
@@ -188,7 +245,7 @@ main() {
print_no_git_instructions print_no_git_instructions
check_docker check_docker
check_env_files create_encryption_key
pull_images pull_images
start_project start_project
} }