docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
58
forge-verification-proxy/README.md
Normal file
58
forge-verification-proxy/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Forge Verification Proxy
|
||||
|
||||
**Purpose:** Bridges Forge's Etherscan-style `verify-contract` to Blockscout (Chain 138).
|
||||
|
||||
**Problem:** Forge sends JSON body only; Blockscout's Etherscan API expects `module` and `action` in the query string. Direct calls fail with "Params 'module' and 'action' are required parameters".
|
||||
|
||||
**Solution:** This proxy accepts Forge's POST, adds `?module=contract&action=verifysourcecode`, forwards to Blockscout, and falls back to Blockscout v2 API if needed.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
**Preferred: orchestrated script (starts proxy if needed):**
|
||||
```bash
|
||||
source smom-dbis-138/.env 2>/dev/null
|
||||
./scripts/verify/run-contract-verification-with-proxy.sh
|
||||
```
|
||||
|
||||
**Manual (proxy + verify):**
|
||||
```bash
|
||||
# 1. Start the proxy (from project root)
|
||||
BLOCKSCOUT_URL=http://192.168.11.140:4000 node forge-verification-proxy/server.js
|
||||
|
||||
# 2. Verify via proxy (script defaults to http://127.0.0.1:3080/)
|
||||
./scripts/verify-contracts-blockscout.sh
|
||||
|
||||
# Or from another host:
|
||||
BLOCKSCOUT_URL=http://192.168.11.140:4000 node forge-verification-proxy/server.js
|
||||
# Then: FORGE_VERIFIER_URL="http://192.168.11.140:3080/" ./scripts/verify-contracts-blockscout.sh
|
||||
```
|
||||
|
||||
**Direct Forge:**
|
||||
|
||||
```bash
|
||||
forge verify-contract <ADDR> <PATH> \
|
||||
--chain-id 138 \
|
||||
--verifier blockscout \
|
||||
--verifier-url "http://<proxy-host>:3080/" \
|
||||
--rpc-url "http://192.168.11.211:8545"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | 3080 | Proxy listen port |
|
||||
| `BLOCKSCOUT_URL` | http://192.168.11.140:4000 | Blockscout API base URL (IP:port) |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [scripts/verify/run-contract-verification-with-proxy.sh](../scripts/verify/run-contract-verification-with-proxy.sh) — Orchestrated script (starts proxy if needed)
|
||||
- [scripts/verify-contracts-blockscout.sh](../scripts/verify-contracts-blockscout.sh) — Verification script (called by orchestrated script)
|
||||
- [docs/03-deployment/BLOCKSCOUT_FORGE_VERIFICATION_EVALUATION.md](../docs/03-deployment/BLOCKSCOUT_FORGE_VERIFICATION_EVALUATION.md) — Evaluation and design
|
||||
- [docs/03-deployment/BLOCKSCOUT_FIX_RUNBOOK.md](../docs/03-deployment/BLOCKSCOUT_FIX_RUNBOOK.md) — Blockscout troubleshooting
|
||||
11
forge-verification-proxy/package.json
Normal file
11
forge-verification-proxy/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "forge-verification-proxy",
|
||||
"version": "1.0.0",
|
||||
"description": "Proxy that adapts Forge verification format to Blockscout v2 API",
|
||||
"type": "module",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
}
|
||||
}
|
||||
287
forge-verification-proxy/server.js
Normal file
287
forge-verification-proxy/server.js
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/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(/\/$/, '');
|
||||
|
||||
function parseJson(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? 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 parseJson(req);
|
||||
} catch (e) {
|
||||
send(res, 400, { status: '0', message: 'Invalid JSON', 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}/"`);
|
||||
});
|
||||
Reference in New Issue
Block a user