feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
This commit is contained in:
@@ -1,96 +1,116 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const baseUrl = (process.env.BASE_URL || 'https://explorer.d-bis.org').replace(/\/$/, '');
|
||||
const baseUrl = (process.env.BASE_URL || 'https://explorer.d-bis.org').replace(/\/$/, '')
|
||||
const addressUnderTest = process.env.SMOKE_ADDRESS || '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506'
|
||||
|
||||
const checks = [
|
||||
{ path: '/', homeVisible: true, expectTexts: ['Gas & Network', 'Latest Blocks', 'Latest Transactions'] },
|
||||
{ path: '/blocks', activeView: 'blocksView', expectTexts: ['All Blocks'] },
|
||||
{ path: '/transactions', activeView: 'transactionsView', expectTexts: ['All Transactions'] },
|
||||
{ path: '/addresses', activeView: 'addressesView', expectTexts: ['All Addresses'] },
|
||||
{ path: '/tokens', activeView: 'tokensView', expectTexts: ['Tokens'] },
|
||||
{ path: '/pools', activeView: 'poolsView', expectTexts: ['Pools', 'Canonical PMM routes'] },
|
||||
{ path: '/routes', activeView: 'routesView', expectTexts: ['Routes', 'Live Route Decision Tree'] },
|
||||
{ path: '/watchlist', activeView: 'watchlistView', expectTexts: ['Watchlist'] },
|
||||
{ path: '/bridge', activeView: 'bridgeView', expectTexts: ['Bridge Monitoring'] },
|
||||
{ path: '/weth', activeView: 'wethView', expectTexts: ['WETH', 'Wrap ETH to WETH9'] },
|
||||
{ path: '/liquidity', activeView: 'liquidityView', expectTexts: ['Liquidity Access', 'Public Explorer Access Points'] },
|
||||
{ path: '/more', activeView: 'moreView', expectTexts: ['More', 'Tools & Services'] },
|
||||
{ path: '/analytics', activeView: 'analyticsView', expectTexts: ['Analytics Dashboard', 'Live Network Analytics'] },
|
||||
{ path: '/operator', activeView: 'operatorView', expectTexts: ['Operator Panel', 'Operator Access Hub'] },
|
||||
{ path: '/block/1', activeView: 'blockDetailView', expectTexts: ['Block Details'] },
|
||||
{
|
||||
path: '/tx/0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
activeView: 'transactionDetailView',
|
||||
expectTexts: ['Transaction Details', 'Transaction not found'],
|
||||
},
|
||||
{
|
||||
path: '/address/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22',
|
||||
activeView: 'addressDetailView',
|
||||
expectTexts: ['Address Details', 'Address'],
|
||||
},
|
||||
];
|
||||
{ path: '/', expectTexts: ['SolaceScanScout', 'Recent Blocks', 'Open wallet tools'] },
|
||||
{ path: '/blocks', expectTexts: ['Blocks'] },
|
||||
{ path: '/transactions', expectTexts: ['Transactions'] },
|
||||
{ path: '/addresses', expectTexts: ['Addresses', 'Open An Address'] },
|
||||
{ path: '/watchlist', expectTexts: ['Watchlist', 'Saved Addresses'] },
|
||||
{ path: '/pools', expectTexts: ['Pools', 'Pool operation shortcuts'] },
|
||||
{ path: '/liquidity', expectTexts: ['Chain 138 Liquidity Access', 'Explorer Access Points'] },
|
||||
{ path: '/wallet', expectTexts: ['Wallet & MetaMask', 'Install Open Snap'] },
|
||||
{ path: '/tokens', expectTexts: ['Tokens', 'Find A Token'] },
|
||||
{ path: '/search', expectTexts: ['Search'], placeholder: 'Search by address, transaction hash, block number...' },
|
||||
{ path: `/addresses/${addressUnderTest}`, expectTexts: [], anyOfTexts: ['Back to addresses', 'Address not found'] },
|
||||
]
|
||||
|
||||
function hasExpectedText(text, snippets) {
|
||||
return snippets.some((snippet) => text.includes(snippet));
|
||||
async function bodyText(page) {
|
||||
return (await page.textContent('body')) || ''
|
||||
}
|
||||
|
||||
async function hasShell(page) {
|
||||
const homeLink = await page.getByRole('link', { name: /Go to explorer home/i }).isVisible().catch(() => false)
|
||||
const supportText = await page.getByText(/Support:/i).isVisible().catch(() => false)
|
||||
return homeLink && supportText
|
||||
}
|
||||
|
||||
async function waitForBodyText(page, snippets, timeoutMs = 15000) {
|
||||
if (!snippets || snippets.length === 0) {
|
||||
return bodyText(page)
|
||||
}
|
||||
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let lastText = ''
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
lastText = await bodyText(page)
|
||||
if (snippets.every((snippet) => lastText.includes(snippet))) {
|
||||
return lastText
|
||||
}
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
return lastText
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
let failures = 0;
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage()
|
||||
let failures = 0
|
||||
|
||||
for (const check of checks) {
|
||||
const url = `${baseUrl}${check.path}`;
|
||||
const url = `${baseUrl}${check.path}`
|
||||
|
||||
try {
|
||||
const response = await page.goto(url, { waitUntil: 'networkidle' });
|
||||
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
if (!response || !response.ok()) {
|
||||
console.error(`FAIL ${check.path}: HTTP ${response ? response.status() : 'no-response'}`);
|
||||
failures += 1;
|
||||
continue;
|
||||
console.error(`FAIL ${check.path}: HTTP ${response ? response.status() : 'no-response'}`)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const bodyText = await page.textContent('body');
|
||||
if (check.homeVisible) {
|
||||
const homeVisible = await page.$eval('#homeView', (el) => getComputedStyle(el).display !== 'none').catch(() => false);
|
||||
if (!homeVisible) {
|
||||
console.error(`FAIL ${check.path}: home view not visible`);
|
||||
failures += 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
const activeView = await page.$eval('.detail-view.active', (el) => el.id).catch(() => null);
|
||||
if (activeView !== check.activeView) {
|
||||
console.error(`FAIL ${check.path}: expected active view ${check.activeView}, got ${activeView}`);
|
||||
failures += 1;
|
||||
continue;
|
||||
const text = await waitForBodyText(page, check.expectTexts)
|
||||
const missing = check.expectTexts.filter((snippet) => !text.includes(snippet))
|
||||
if (missing.length > 0) {
|
||||
console.error(`FAIL ${check.path}: missing text ${missing.join(' | ')}`)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (check.anyOfTexts && !check.anyOfTexts.some((snippet) => text.includes(snippet))) {
|
||||
console.error(`FAIL ${check.path}: expected one of ${check.anyOfTexts.join(' | ')}`)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (check.placeholder) {
|
||||
const placeholderVisible = await page.getByPlaceholder(check.placeholder).isVisible().catch(() => false)
|
||||
if (!placeholderVisible) {
|
||||
console.error(`FAIL ${check.path}: missing placeholder ${check.placeholder}`)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasExpectedText(bodyText || '', check.expectTexts)) {
|
||||
console.error(`FAIL ${check.path}: expected one of ${check.expectTexts.join(' | ')}`);
|
||||
failures += 1;
|
||||
continue;
|
||||
if (!(await hasShell(page))) {
|
||||
console.error(`FAIL ${check.path}: shared explorer shell not visible`)
|
||||
failures += 1
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(`OK ${check.path}`);
|
||||
console.log(`OK ${check.path}`)
|
||||
} catch (error) {
|
||||
console.error(`FAIL ${check.path}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
failures += 1;
|
||||
console.error(`FAIL ${check.path}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
failures += 1
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
await browser.close()
|
||||
|
||||
if (failures > 0) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`All ${checks.length} route checks passed for ${baseUrl}`);
|
||||
console.log(`All ${checks.length} route checks passed for ${baseUrl}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error))
|
||||
process.exitCode = 1
|
||||
})
|
||||
|
||||
47
frontend/scripts/start-standalone.mjs
Normal file
47
frontend/scripts/start-standalone.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { cp, mkdir } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
const projectRoot = process.cwd()
|
||||
const standaloneRoot = path.join(projectRoot, '.next', 'standalone')
|
||||
const standaloneNextRoot = path.join(standaloneRoot, '.next')
|
||||
const standaloneServer = path.join(standaloneRoot, 'server.js')
|
||||
|
||||
async function copyIfPresent(sourcePath, destinationPath) {
|
||||
if (!existsSync(sourcePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(destinationPath), { recursive: true })
|
||||
await cp(sourcePath, destinationPath, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(standaloneServer)) {
|
||||
console.error('Standalone server build is missing. Run `npm run build` first.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await copyIfPresent(path.join(projectRoot, '.next', 'static'), path.join(standaloneNextRoot, 'static'))
|
||||
await copyIfPresent(path.join(projectRoot, 'public'), path.join(standaloneRoot, 'public'))
|
||||
|
||||
const child = spawn(process.execPath, [standaloneServer], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
})
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal)
|
||||
return
|
||||
}
|
||||
process.exit(code ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user