Files
proxmox/forge-verification-proxy/server.js
defiQUG bea1903ac9
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
Sync all local changes: docs, config, scripts, submodule refs, verification evidence
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 15:46:06 -08:00

307 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Forge Verification Proxy — bridges Forge to Blockscout
*
* Strategy 1: Forward to Etherscan-compatible API with module/action in query
* Strategy 2 (fallback): Forward to Blockscout v2 API
*
* Forge sends: POST with JSON { contractaddress, sourceCode, codeformat, ... }
* Blockscout Etherscan API expects: ?module=contract&action=verifysourcecode (in URL)
*
* Usage: BLOCKSCOUT_URL=http://192.168.11.140:4000 node server.js
* Forge: --verifier-url "http://localhost:3080/"
*/
import http from 'node:http';
const PORT = parseInt(process.env.PORT || '3080', 10);
const BLOCKSCOUT_URL = (process.env.BLOCKSCOUT_URL || 'http://192.168.11.140:4000').replace(/\/$/, '');
/** Parse body as JSON or application/x-www-form-urlencoded (Forge/Etherscan style). */
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
if (!body || !body.trim()) {
resolve({});
return;
}
const contentType = (req.headers['content-type'] || '').toLowerCase();
if (contentType.includes('application/x-www-form-urlencoded')) {
try {
const params = new URLSearchParams(body);
const payload = {};
for (const [k, v] of params) {
if (v !== undefined && v !== '') payload[k] = v;
}
resolve(payload);
} catch (e) {
reject(e);
}
return;
}
try {
resolve(JSON.parse(body));
} catch (e) {
reject(e);
}
});
req.on('error', reject);
});
}
function send(res, status, data) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
/**
* Forward to Blockscout Etherscan API with module/action in query.
* Same JSON body, but URL includes required params.
*/
async function forwardEtherscanFormat(payload) {
const query = new URLSearchParams({ module: 'contract', action: 'verifysourcecode' });
const path = `/api/?${query}`;
const body = JSON.stringify(payload);
const url = new URL(path, BLOCKSCOUT_URL);
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
Host: url.hostname + (url.port ? ':' + url.port : ''),
},
},
(res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {}, raw: data });
} catch {
resolve({ status: res.statusCode, data: null, raw: data });
}
});
}
);
req.on('error', reject);
req.write(body);
req.end();
});
}
/**
* Forward to Blockscout v2 flattened-code API (for Standard JSON, we pass as source_code).
*/
async function forwardV2Flattened(payload) {
const addr = payload.contractaddress || payload.contractAddress;
const sourceCode = payload.sourceCode ?? payload.source_code;
const codeformat = (payload.codeformat || '').toLowerCase();
const isStandardJson =
codeformat === 'solidity-standard-json-input' ||
(typeof sourceCode === 'string' && sourceCode.trimStart().startsWith('{') && sourceCode.includes('"sources"'));
const path = isStandardJson
? `/api/v2/smart-contracts/${addr}/verification/via/standard-input`
: `/api/v2/smart-contracts/${addr}/verification/via/flattened-code`;
const v2Body = {
compiler_version: payload.compilerversion || payload.compilerVersion || 'v0.8.20+commit.a1b79de6',
contract_name: payload.contractname || payload.contractName || 'Contract',
license_type: payload.licensetype || payload.licenseType || 'mit',
is_optimization_enabled: [true, '1', 1, 'true'].includes(payload.optimizationUsed ?? payload.optimization_used),
optimization_runs: parseInt(payload.runs ?? payload.optimization_runs ?? '200', 10) || 200,
evm_version: payload.evmversion || payload.evm_version || 'london',
autodetect_constructor_args: payload.autodetectConstructorArguments !== false,
source_code: typeof sourceCode === 'string' ? sourceCode : JSON.stringify(sourceCode),
};
if (payload.constructorArguments) v2Body.constructor_args = payload.constructorArguments;
const body = JSON.stringify(v2Body);
const url = new URL(path, BLOCKSCOUT_URL);
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
},
(res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {}, raw: data });
} catch {
resolve({ status: res.statusCode, data: null, raw: data });
}
});
}
);
req.on('error', reject);
req.write(body);
req.end();
});
}
function toEtherscanResponse(result) {
const { status, data, raw } = result;
if (status >= 200 && status < 300 && data?.status === '1') {
return { status: '1', message: data.message || 'OK', result: data.result ?? 'Verification submitted' };
}
if (status >= 200 && status < 300) {
return { status: '1', message: 'OK', result: data?.result ?? 'Verification submitted' };
}
// Blockscout may return HTML (502/500) or invalid JSON when DB/migrations fail
let msg = data?.message || data?.error;
if (!msg && raw) {
if (raw.trimStart().startsWith('<')) {
msg = 'Blockscout returned HTML (likely DB down or migrations needed). Run scripts/fix-blockscout-ssl-and-migrations.sh';
} else if (raw.length > 200) {
msg = raw.slice(0, 200) + '...';
} else {
msg = raw;
}
}
return {
status: '0',
message: msg || 'Verification failed',
result: null,
};
}
/** Forward GET/other requests to Blockscout (getabi, checkverifystatus, etc.) */
function proxyToBlockscout(req, res) {
let targetPath = (req.url || '/').startsWith('/api') ? req.url : '/api' + (req.url === '/' ? '' : req.url);
// Blockscout redirects /api to /api/ — use /api/ to avoid 301
if (targetPath.startsWith('/api?') || targetPath === '/api') {
targetPath = '/api/' + (targetPath.slice(4) || '');
}
const url = new URL(targetPath, BLOCKSCOUT_URL);
const proxyReq = http.request(
{
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: req.method,
headers: { host: url.host },
},
(proxyRes) => {
const headers = { ...proxyRes.headers };
delete headers['transfer-encoding'];
res.writeHead(proxyRes.statusCode || 200, headers);
proxyRes.pipe(res);
}
);
proxyReq.on('error', (e) => {
console.error('[forge-verification-proxy]', e.message);
send(res, 502, { status: '0', message: 'Blockscout unreachable', result: null });
});
if (req.method === 'POST' || req.method === 'PUT') {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
const path = (req.url || '/').split('?')[0];
if (req.method === 'GET') {
await proxyToBlockscout(req, res);
return;
}
if (req.method !== 'POST') {
send(res, 405, { status: '0', message: 'Method not allowed', result: null });
return;
}
let payload;
try {
payload = await parseBody(req);
} catch (e) {
send(res, 400, { status: '0', message: 'Invalid request body (JSON or form)', result: null });
return;
}
if (!payload.contractaddress && !payload.contractAddress) {
send(res, 400, {
status: '0',
message: 'Params contractaddress and sourceCode are required',
result: null,
});
return;
}
const codeformat = (payload.codeformat || '').toLowerCase();
const sourceCode = payload.sourceCode ?? payload.source_code;
const isStandardJson =
codeformat === 'solidity-standard-json-input' ||
(typeof sourceCode === 'string' && sourceCode.trimStart().startsWith('{') && sourceCode.includes('"sources"'));
// Etherscan API expects Standard JSON in sourceCode; flattened Solidity causes "Invalid JSON".
// Try v2 API first for flattened code; use Etherscan only for Standard JSON.
const tryV2First = !isStandardJson;
try {
let result;
let out;
if (tryV2First) {
result = await forwardV2Flattened(payload);
out = toEtherscanResponse(result);
if (out.status !== '1') {
console.error('[forge-verification-proxy] v2 API failed:', out.message, '- trying Etherscan format...');
result = await forwardEtherscanFormat(payload);
const etherOut = toEtherscanResponse(result);
send(res, 200, etherOut.status === '1' ? etherOut : out);
return;
}
} else {
result = await forwardEtherscanFormat(payload);
out = toEtherscanResponse(result);
if (out.status !== '1') {
console.error('[forge-verification-proxy] Etherscan API failed:', out.message, '- trying v2...');
result = await forwardV2Flattened(payload);
const v2Out = toEtherscanResponse(result);
send(res, 200, v2Out);
return;
}
}
send(res, 200, out);
} catch (e) {
console.error('[forge-verification-proxy]', e.message);
send(res, 500, {
status: '0',
message: e.message || 'Proxy error',
result: null,
});
}
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`[forge-verification-proxy] Listening on port ${PORT}`);
console.log(`[forge-verification-proxy] Blockscout: ${BLOCKSCOUT_URL}`);
console.log(`[forge-verification-proxy] Forge: --verifier-url "http://<host>:${PORT}/"`);
});