обновление
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/169ec88754f8ab831077ca9fbb049cf4.json"
|
||||
"buildInfo": "../../../../build-info/3e12480a731f7a845287f0f150241bb4.json"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_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
1100
backend/cache/solidity-files-cache.json
vendored
1100
backend/cache/solidity-files-cache.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
bytes operation; // операция для исполнения
|
||||
uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
|
||||
uint256[] targetChains; // целевые сети для исполнения
|
||||
uint256 timelock; // earliest execution timestamp (sec)
|
||||
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
|
||||
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 ProposalExecuted(uint256 proposalId, bytes operation);
|
||||
event ProposalCancelled(uint256 proposalId, string reason);
|
||||
event ProposalTimelockSet(uint256 proposalId, uint256 timelock);
|
||||
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
|
||||
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
|
||||
event ModuleAdded(bytes32 moduleId, address moduleAddress);
|
||||
@@ -192,29 +190,47 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
bytes memory _operation,
|
||||
uint256 _governanceChainId,
|
||||
uint256[] memory _targetChains,
|
||||
uint256 _timelockDelay
|
||||
uint256 /* _timelockDelay */
|
||||
) external returns (uint256) {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
require(_duration > 0, "Duration must be positive");
|
||||
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++;
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
|
||||
|
||||
proposal.id = proposalId;
|
||||
proposal.description = _description;
|
||||
proposal.forVotes = 0;
|
||||
proposal.againstVotes = 0;
|
||||
proposal.executed = false;
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = msg.sender;
|
||||
proposal.initiator = _initiator;
|
||||
proposal.operation = _operation;
|
||||
proposal.governanceChainId = _governanceChainId;
|
||||
proposal.timelock = block.timestamp + _timelockDelay;
|
||||
|
||||
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
|
||||
uint256 nowClock = clock();
|
||||
proposal.snapshotTimepoint = nowClock == 0 ? 0 : nowClock - 1;
|
||||
|
||||
// запись целевых сетей
|
||||
for (uint256 i = 0; i < _targetChains.length; i++) {
|
||||
require(supportedChains[_targetChains[i]], "Target chain not supported");
|
||||
@@ -222,10 +238,9 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
}
|
||||
|
||||
allProposalIds.push(proposalId);
|
||||
emit ProposalCreated(proposalId, msg.sender, _description);
|
||||
emit ProposalCreated(proposalId, _initiator, _description);
|
||||
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
|
||||
emit ProposalTargetsSet(proposalId, _targetChains);
|
||||
emit ProposalTimelockSet(proposalId, proposal.timelock);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
@@ -297,7 +312,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
"Voting not ended and quorum not reached"
|
||||
);
|
||||
require(passed && quorumReached, "Proposal not passed");
|
||||
require(block.timestamp >= proposal.timelock, "Timelock not expired");
|
||||
|
||||
proposal.executed = true;
|
||||
|
||||
@@ -341,7 +355,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
require(!proposal.executed, "Proposal already executed in this chain");
|
||||
require(currentChainId != proposal.governanceChainId, "Use executeProposal in governance chain");
|
||||
require(_isTargetChain(proposal, currentChainId), "Chain not in targets");
|
||||
require(block.timestamp >= proposal.timelock, "Timelock not expired");
|
||||
|
||||
require(signers.length == signatures.length, "Bad signatures");
|
||||
bytes32 opHash = keccak256(proposal.operation);
|
||||
@@ -620,24 +633,28 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
require(!activeModules[_moduleId], "Module already exists");
|
||||
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(
|
||||
bytes4(keccak256("_addModule(bytes32,address)")),
|
||||
_moduleId,
|
||||
_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(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(
|
||||
bytes4(keccak256("_removeModule(bytes32)")),
|
||||
_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,
|
||||
address initiator,
|
||||
uint256 governanceChainId,
|
||||
uint256 timelock,
|
||||
|
||||
uint256 snapshotTimepoint,
|
||||
uint256[] memory targets
|
||||
) {
|
||||
@@ -769,7 +790,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
p.deadline,
|
||||
p.initiator,
|
||||
p.governanceChainId,
|
||||
p.timelock,
|
||||
|
||||
p.snapshotTimepoint,
|
||||
p.targetChains
|
||||
);
|
||||
@@ -818,7 +839,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
if (p.executed) return 3;
|
||||
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
||||
bool votingOver = block.timestamp >= p.deadline;
|
||||
bool ready = passed && quorumReached && block.timestamp >= p.timelock;
|
||||
bool ready = passed && quorumReached;
|
||||
if (ready) return 5; // ReadyForExecution
|
||||
if (passed && (votingOver || quorumReached)) return 1; // Succeeded
|
||||
if (votingOver && !passed) return 2; // Defeated
|
||||
|
||||
@@ -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;
|
||||
11
backend/db/migrations/050_create_secrets_table.sql
Normal file
11
backend/db/migrations/050_create_secrets_table.sql
Normal 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
|
||||
);
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ethers = require('ethers'); // Added ethers for private key validation
|
||||
const create2 = require('../utils/create2');
|
||||
const verificationStore = require('../services/verificationStore');
|
||||
const etherscanV2 = require('../services/etherscanV2VerificationService');
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @desc Получить параметры по умолчанию для создания DLE v2
|
||||
@@ -329,4 +372,145 @@ router.post('/predict-addresses', auth.requireAuth, auth.requireAdmin, async (re
|
||||
logger.error('predict-addresses error', e);
|
||||
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 });
|
||||
}
|
||||
});
|
||||
@@ -2,25 +2,159 @@
|
||||
const hre = require('hardhat');
|
||||
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 provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
const wallet = new ethers.Wallet(pk, provider);
|
||||
const net = await provider.getNetwork();
|
||||
|
||||
// Ensure factory
|
||||
let faddr = factoryAddress;
|
||||
const code = faddr ? await provider.getCode(faddr) : '0x';
|
||||
if (!faddr || code === '0x') {
|
||||
const Factory = await hre.ethers.getContractFactory('FactoryDeployer', wallet);
|
||||
const factory = await Factory.deploy();
|
||||
await factory.waitForDeployment();
|
||||
faddr = await factory.getAddress();
|
||||
// DEBUG: базовая информация по сети
|
||||
try {
|
||||
const calcInitHash = ethers.keccak256(dleInit);
|
||||
const saltLen = ethers.getBytes(salt).length;
|
||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`);
|
||||
console.log(`[MULTI_DBG] wallet=${wallet.address} targetFactoryNonce=${targetFactoryNonce}`);
|
||||
console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`);
|
||||
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 addr = rc.logs?.[0]?.args?.addr || (await Factory.computeAddress(salt, initCodeHash));
|
||||
return { factory: faddr, address: addr };
|
||||
let addr = rc.logs?.[0]?.args?.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() {
|
||||
@@ -31,7 +165,10 @@ async function main() {
|
||||
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());
|
||||
|
||||
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
|
||||
const paramsPath = path.join(__dirname, './current-params.json');
|
||||
@@ -53,12 +190,33 @@ async function main() {
|
||||
};
|
||||
const deployTx = await DLE.getDeployTransaction(dleConfig, params.currentChainId);
|
||||
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 = [];
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
const rpcUrl = networks[i];
|
||||
const factory = factories[i] || process.env.FACTORY_ADDRESS || null;
|
||||
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, factory, dleInit);
|
||||
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonce, dleInit);
|
||||
results.push({ rpcUrl, ...r });
|
||||
}
|
||||
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(results));
|
||||
|
||||
@@ -16,6 +16,8 @@ const fs = require('fs');
|
||||
const { ethers } = require('ethers');
|
||||
const logger = require('../utils/logger');
|
||||
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
||||
const etherscanV2 = require('./etherscanV2VerificationService');
|
||||
const verificationStore = require('./verificationStore');
|
||||
|
||||
/**
|
||||
* Сервис для управления DLE v2 (Digital Legal Entity)
|
||||
@@ -28,6 +30,8 @@ class DLEV2Service {
|
||||
* @returns {Promise<Object>} - Результат создания DLE
|
||||
*/
|
||||
async createDLE(dleParams) {
|
||||
let paramsFile = null;
|
||||
let tempParamsFile = null;
|
||||
try {
|
||||
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
|
||||
|
||||
@@ -38,10 +42,10 @@ class DLEV2Service {
|
||||
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);
|
||||
if (!fs.existsSync(deployDir)) {
|
||||
fs.mkdirSync(deployDir, { recursive: true });
|
||||
@@ -64,8 +68,9 @@ class DLEV2Service {
|
||||
{
|
||||
const { ethers } = require('ethers');
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrls[0]);
|
||||
const walletAddress = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey, provider).address : null;
|
||||
if (walletAddress) {
|
||||
if (dleParams.privateKey) {
|
||||
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 minBalance = ethers.parseEther("0.00001");
|
||||
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(',');
|
||||
|
||||
// Мультисетевой деплой одним вызовом
|
||||
// Генерируем одноразовый 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, {
|
||||
rpcUrls: rpcUrls.join(','),
|
||||
privateKey: dleParams.privateKey,
|
||||
salt: process.env.CREATE2_SALT,
|
||||
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
|
||||
salt: create2Salt,
|
||||
initCodeHash,
|
||||
factories: factoryAddresses
|
||||
});
|
||||
|
||||
// Очищаем временные файлы
|
||||
this.cleanupTempFiles(paramsFile, tempParamsFile);
|
||||
// Сохраняем информацию о созданном DLE для отображения на странице управления
|
||||
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;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при создании DLE v2:', 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 из формы
|
||||
@@ -165,11 +288,27 @@ class DLEV2Service {
|
||||
|
||||
// Преобразуем суммы из строк или чисел в BigNumber, если нужно
|
||||
if (deployParams.initialAmounts && Array.isArray(deployParams.initialAmounts)) {
|
||||
deployParams.initialAmounts = deployParams.initialAmounts.map(amount => {
|
||||
if (typeof amount === 'string' && !amount.startsWith('0x')) {
|
||||
return ethers.parseEther(amount).toString();
|
||||
deployParams.initialAmounts = deployParams.initialAmounts.map(rawAmount => {
|
||||
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
|
||||
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);
|
||||
if (!m) throw new Error('Результат не найден');
|
||||
const arr = JSON.parse(m[1]);
|
||||
if (!Array.isArray(arr) || arr.length === 0) throw new Error('Пустой результат деплоя');
|
||||
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('Адреса отличаются между сетями');
|
||||
resolve({ success: true, data: { dleAddress: addr, networks: arr } });
|
||||
} 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
|
||||
* @returns {Array<Object>} - Список DLE v2
|
||||
@@ -402,6 +569,191 @@ class DLEV2Service {
|
||||
const initCode = deployTx.data;
|
||||
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();
|
||||
89
backend/services/etherscanV2VerificationService.js
Normal file
89
backend/services/etherscanV2VerificationService.js
Normal 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();
|
||||
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
|
||||
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() {
|
||||
const providers = await encryptedDb.getData('rpc_providers', {}, null, 'id');
|
||||
return providers;
|
||||
@@ -24,7 +32,7 @@ async function saveAllRpcProviders(rpcConfigs) {
|
||||
// Сохраняем новые провайдеры
|
||||
for (const cfg of rpcConfigs) {
|
||||
await encryptedDb.saveData('rpc_providers', {
|
||||
network_id: cfg.networkId,
|
||||
network_id: normalizeNetworkId(cfg.networkId),
|
||||
rpc_url: cfg.rpcUrl,
|
||||
chain_id: cfg.chainId || null
|
||||
});
|
||||
@@ -41,12 +49,12 @@ async function upsertRpcProvider(cfg) {
|
||||
rpc_url: cfg.rpcUrl,
|
||||
chain_id: cfg.chainId || null
|
||||
}, {
|
||||
network_id: cfg.networkId
|
||||
network_id: normalizeNetworkId(cfg.networkId)
|
||||
});
|
||||
} else {
|
||||
// Создаем новый провайдер
|
||||
await encryptedDb.saveData('rpc_providers', {
|
||||
network_id: cfg.networkId,
|
||||
network_id: normalizeNetworkId(cfg.networkId),
|
||||
rpc_url: cfg.rpcUrl,
|
||||
chain_id: cfg.chainId || null
|
||||
});
|
||||
@@ -58,8 +66,14 @@ async function deleteRpcProvider(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) {
|
||||
|
||||
56
backend/services/secretStore.js
Normal file
56
backend/services/secretStore.js
Normal 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 };
|
||||
|
||||
|
||||
47
backend/services/verificationStore.js
Normal file
47
backend/services/verificationStore.js
Normal 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 };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user