обновление

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

View File

@@ -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();

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');
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) {

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 };