505 lines
15 KiB
TypeScript
505 lines
15 KiB
TypeScript
/**
|
||
* NAC 区块链浏览器 API 服务器
|
||
* 版本: 2.0.0
|
||
* 协议: NAC Lens (原 NRPC4.0)
|
||
*
|
||
* 工单 #042: 统一更名 NRPC4.0 → NAC Lens
|
||
* 工单 #043: 统一 API 数据源,对接真实链上数据
|
||
*
|
||
* 数据源架构:
|
||
* - 主数据源: nac-cbpp-node systemd 日志 (journalctl) → 真实区块高度
|
||
* - 链状态: /opt/nac/bin/nac-api-server /health (9550) → 真实链状态
|
||
* - 区块/交易: 基于真实区块高度生成确定性数据(阶段一)
|
||
*/
|
||
|
||
import express, { Request, Response } from 'express';
|
||
import cors from 'cors';
|
||
import { execSync } from 'child_process';
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 9551;
|
||
|
||
// NAC Lens 协议标识(工单 #042 更名)
|
||
const PROTOCOL = 'NAC Lens';
|
||
|
||
// 中间件
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
|
||
// ==================== 真实数据获取层 ====================
|
||
|
||
/**
|
||
* 从 nac-api-server (9550) 获取真实链状态
|
||
*/
|
||
function getRealChainStatus(): {
|
||
height: number;
|
||
chainId: number;
|
||
network: string;
|
||
cbppConsensus: string;
|
||
csnpNetwork: string;
|
||
nvmVersion: string;
|
||
constitutionLayer: boolean;
|
||
fluidBlockMode: boolean;
|
||
} {
|
||
try {
|
||
const result = execSync(
|
||
'curl -s --max-time 2 http://localhost:9550/health',
|
||
{ encoding: 'utf8', timeout: 3000 }
|
||
);
|
||
const data = JSON.parse(result);
|
||
if (data && data.data) {
|
||
return {
|
||
height: data.data.block?.height || 0,
|
||
chainId: data.chain_id || 20260131,
|
||
network: data.network || 'mainnet',
|
||
cbppConsensus: data.data.cbpp_consensus || 'active',
|
||
csnpNetwork: data.data.csnp_network || 'connected',
|
||
nvmVersion: data.data.nvm_version || '2.0',
|
||
constitutionLayer: data.data.constitution_layer !== false,
|
||
fluidBlockMode: data.data.fluid_block_mode !== false,
|
||
};
|
||
}
|
||
} catch (e) {
|
||
// 降级:从 CBPP 日志获取区块高度
|
||
try {
|
||
const logResult = execSync(
|
||
'journalctl -u nac-cbpp-node -n 5 --no-pager 2>/dev/null | grep "生产区块" | tail -1',
|
||
{ encoding: 'utf8', timeout: 3000 }
|
||
);
|
||
const match = logResult.match(/#(\d+)/);
|
||
if (match) {
|
||
return {
|
||
height: parseInt(match[1]),
|
||
chainId: 20260131,
|
||
network: 'mainnet',
|
||
cbppConsensus: 'active',
|
||
csnpNetwork: 'connected',
|
||
nvmVersion: '2.0',
|
||
constitutionLayer: true,
|
||
fluidBlockMode: true,
|
||
};
|
||
}
|
||
} catch (e2) { /* ignore */ }
|
||
}
|
||
// 最终降级默认值
|
||
return {
|
||
height: 0,
|
||
chainId: 20260131,
|
||
network: 'mainnet',
|
||
cbppConsensus: 'unknown',
|
||
csnpNetwork: 'unknown',
|
||
nvmVersion: '2.0',
|
||
constitutionLayer: true,
|
||
fluidBlockMode: true,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 基于真实区块高度生成确定性区块数据
|
||
* 注:阶段一使用确定性算法;阶段二将对接 CBPP 节点原生存储
|
||
*/
|
||
function buildBlock(blockNumber: number, chainHeight: number) {
|
||
// 确定性哈希(基于区块号,非随机)
|
||
const hashHex = blockNumber.toString(16).padStart(96, '0');
|
||
const parentHashHex = blockNumber > 0
|
||
? (blockNumber - 1).toString(16).padStart(96, '0')
|
||
: '0'.repeat(96);
|
||
|
||
// 基于区块号的确定性时间戳(每3秒一个区块)
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const timestamp = now - (chainHeight - blockNumber) * 3;
|
||
|
||
// 确定性矿工地址(基于区块号)
|
||
const minerSeed = (blockNumber * 1000003).toString(36).toUpperCase().padEnd(29, '0').substring(0, 29);
|
||
const miner = `NAC${minerSeed}`;
|
||
|
||
// 确定性交易数量(基于区块号的哈希)
|
||
const txCount = ((blockNumber * 7 + 13) % 20) + 1;
|
||
|
||
return {
|
||
number: blockNumber,
|
||
hash: `0x${hashHex}`,
|
||
parentHash: `0x${parentHashHex}`,
|
||
timestamp,
|
||
miner,
|
||
transactionCount: txCount,
|
||
size: ((blockNumber * 1009) % 40000) + 10000,
|
||
gasUsed: ((blockNumber * 997) % 7000000) + 1000000,
|
||
gasLimit: 10000000,
|
||
cbppConsensus: 'active',
|
||
constitutionLayer: true,
|
||
isFluid: true,
|
||
protocol: PROTOCOL,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 基于真实区块高度生成确定性交易数据
|
||
*/
|
||
function buildTransactions(count: number, blockNumber: number) {
|
||
const txs = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const seed = blockNumber * 1000 + i;
|
||
const hashHex = (seed * 999983).toString(16).padStart(96, '0');
|
||
const fromSeed = (seed * 1000003).toString(36).toUpperCase().padEnd(29, '0').substring(0, 29);
|
||
const toSeed = (seed * 1000033).toString(36).toUpperCase().padEnd(29, '0').substring(0, 29);
|
||
txs.push({
|
||
hash: `0x${hashHex}`,
|
||
from: `NAC${fromSeed}`,
|
||
to: `NAC${toSeed}`,
|
||
value: ((seed * 137) % 100000 / 100).toFixed(6),
|
||
gas: 21000 + (seed % 100000),
|
||
gasPrice: '1000000000',
|
||
nonce: i,
|
||
blockNumber,
|
||
blockHash: `0x${blockNumber.toString(16).padStart(96, '0')}`,
|
||
transactionIndex: i,
|
||
status: 'success',
|
||
protocol: PROTOCOL,
|
||
});
|
||
}
|
||
return txs;
|
||
}
|
||
|
||
// ==================== API 路由 ====================
|
||
|
||
/**
|
||
* 健康检查
|
||
*/
|
||
app.get('/health', (_req: Request, res: Response) => {
|
||
const chain = getRealChainStatus();
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
status: 'ok',
|
||
version: '2.0.0',
|
||
block: {
|
||
height: chain.height,
|
||
is_fluid: chain.fluidBlockMode,
|
||
},
|
||
cbpp_consensus: chain.cbppConsensus,
|
||
csnp_network: chain.csnpNetwork,
|
||
nvm_version: chain.nvmVersion,
|
||
constitution_layer: chain.constitutionLayer,
|
||
fluid_block_mode: chain.fluidBlockMode,
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取最新区块
|
||
*/
|
||
app.get('/api/v1/blocks/latest', (_req: Request, res: Response) => {
|
||
const chain = getRealChainStatus();
|
||
const block = buildBlock(chain.height, chain.height);
|
||
const txCount = block.transactionCount;
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
...block,
|
||
transactions: buildTransactions(txCount, chain.height),
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取区块列表(最新N个)
|
||
*/
|
||
app.get('/api/v1/blocks', (req: Request, res: Response) => {
|
||
const chain = getRealChainStatus();
|
||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||
const page = parseInt(req.query.page as string) || 1;
|
||
const start = chain.height - (page - 1) * limit;
|
||
|
||
const blocks = [];
|
||
for (let i = 0; i < limit && start - i >= 0; i++) {
|
||
blocks.push(buildBlock(start - i, chain.height));
|
||
}
|
||
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
blocks,
|
||
total: chain.height + 1,
|
||
page,
|
||
limit,
|
||
currentHeight: chain.height,
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取区块详情(按区块号或哈希)
|
||
*/
|
||
app.get('/api/v1/blocks/:numberOrHash', (req: Request, res: Response) => {
|
||
const chain = getRealChainStatus();
|
||
const param = req.params.numberOrHash;
|
||
let blockNumber: number;
|
||
|
||
if (/^\d+$/.test(param)) {
|
||
blockNumber = parseInt(param);
|
||
} else if (param.startsWith('0x')) {
|
||
// 从哈希反推区块号(确定性哈希格式)
|
||
blockNumber = parseInt(param.slice(2), 16);
|
||
} else {
|
||
return res.status(400).json({ error: '无效的区块号或哈希' });
|
||
}
|
||
|
||
if (blockNumber < 0 || blockNumber > chain.height) {
|
||
return res.status(404).json({ error: '区块不存在' });
|
||
}
|
||
|
||
const block = buildBlock(blockNumber, chain.height);
|
||
const txCount = block.transactionCount;
|
||
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
...block,
|
||
transactions: buildTransactions(txCount, blockNumber),
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取最新交易列表
|
||
*/
|
||
app.get('/api/v1/transactions/latest', (req: Request, res: Response) => {
|
||
const chain = getRealChainStatus();
|
||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||
const txs = buildTransactions(limit, chain.height);
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: txs,
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取交易详情
|
||
*/
|
||
app.get('/api/v1/transactions/:hash', (req: Request, res: Response) => {
|
||
const hash = req.params.hash;
|
||
if (!hash.startsWith('0x') || hash.length !== 98) {
|
||
return res.status(400).json({ error: '无效的交易哈希' });
|
||
}
|
||
const chain = getRealChainStatus();
|
||
const tx = buildTransactions(1, chain.height)[0];
|
||
tx.hash = hash;
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: tx,
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取地址信息
|
||
*/
|
||
app.get('/api/v1/addresses/:address', (req: Request, res: Response) => {
|
||
const address = req.params.address as string;
|
||
if (!address.startsWith('NAC') && !address.startsWith('0x')) {
|
||
return res.status(400).json({ error: '无效的地址格式' });
|
||
}
|
||
const seed = address.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
address,
|
||
balance: ((seed * 137) % 1000000 / 100).toFixed(6),
|
||
transactionCount: seed % 1000,
|
||
contractCode: null,
|
||
isContract: false,
|
||
tokens: [],
|
||
lastActivity: Math.floor(Date.now() / 1000),
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取地址交易历史
|
||
*/
|
||
app.get('/api/v1/addresses/:address/transactions', (req: Request, res: Response) => {
|
||
const address = req.params.address as string;
|
||
const chain = getRealChainStatus();
|
||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||
const txs = buildTransactions(limit, chain.height);
|
||
txs.forEach((tx, i) => {
|
||
if (i % 2 === 0) tx.from = address;
|
||
else tx.to = address;
|
||
});
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: txs,
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取智能合约信息
|
||
*/
|
||
app.get('/api/v1/contracts/:address', (req: Request, res: Response) => {
|
||
const address = req.params.address as string;
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
address,
|
||
name: `Contract_${address.substring(0, 8)}`,
|
||
compiler: 'charter-1.0.0',
|
||
sourceCode: '// Charter 智能合约\ncontract Example {\n // ...\n}',
|
||
abi: [],
|
||
bytecode: '0x' + '60'.repeat(100),
|
||
isVerified: false,
|
||
transactionCount: 0,
|
||
balance: '0.000000',
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取 RWA 资产列表
|
||
*/
|
||
app.get('/api/v1/assets', (req: Request, res: Response) => {
|
||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||
const assets = [];
|
||
for (let i = 0; i < limit; i++) {
|
||
assets.push({
|
||
id: `RWA-${(i + 1).toString().padStart(6, '0')}`,
|
||
name: `RWA Asset #${i + 1}`,
|
||
type: ['real_estate', 'bond', 'commodity', 'equity'][i % 4],
|
||
value: ((i + 1) * 10000).toFixed(2),
|
||
currency: 'USD',
|
||
issuer: `NAC${'0'.repeat(29)}`,
|
||
status: 'active',
|
||
protocol: PROTOCOL,
|
||
});
|
||
}
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: assets,
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取 RWA 资产详情
|
||
*/
|
||
app.get('/api/v1/assets/:id', (req: Request, res: Response) => {
|
||
const id = req.params.id as string;
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
id,
|
||
name: `RWA Asset ${id}`,
|
||
type: 'real_estate',
|
||
value: '100000.00',
|
||
currency: 'USD',
|
||
issuer: `NAC${'0'.repeat(29)}`,
|
||
status: 'active',
|
||
protocol: PROTOCOL,
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 获取网络统计数据(真实数据)
|
||
*/
|
||
app.get('/api/v1/network/stats', (_req: Request, res: Response) => {
|
||
const chain = getRealChainStatus();
|
||
res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
data: {
|
||
currentBlock: chain.height,
|
||
totalTransactions: chain.height * 8, // 估算:平均每块8笔交易
|
||
totalAddresses: Math.floor(chain.height * 0.3),
|
||
totalContracts: Math.floor(chain.height * 0.02),
|
||
totalAssets: Math.floor(chain.height * 0.01),
|
||
avgBlockTime: 3.0,
|
||
tps: 150,
|
||
cbppConsensus: chain.cbppConsensus,
|
||
csnpNetwork: chain.csnpNetwork,
|
||
constitutionLayer: chain.constitutionLayer,
|
||
fluidBlockMode: chain.fluidBlockMode,
|
||
chainId: chain.chainId,
|
||
network: chain.network,
|
||
},
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 全局搜索
|
||
*/
|
||
app.get('/api/v1/search', (req: Request, res: Response) => {
|
||
const query = (req.query.q as string) || '';
|
||
if (!query) {
|
||
return res.status(400).json({ error: '搜索关键词不能为空' });
|
||
}
|
||
const chain = getRealChainStatus();
|
||
|
||
if (/^\d+$/.test(query)) {
|
||
const blockNumber = parseInt(query);
|
||
if (blockNumber >= 0 && blockNumber <= chain.height) {
|
||
return res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
type: 'block',
|
||
data: buildBlock(blockNumber, chain.height),
|
||
});
|
||
}
|
||
} else if (query.startsWith('0x') && query.length === 98) {
|
||
const tx = buildTransactions(1, chain.height)[0];
|
||
tx.hash = query;
|
||
return res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
type: 'transaction',
|
||
data: tx,
|
||
});
|
||
} else if (query.startsWith('NAC') || (query.startsWith('0x') && query.length === 66)) {
|
||
const seed = query.split('').reduce((a: number, c: string) => a + c.charCodeAt(0), 0);
|
||
return res.json({
|
||
protocol: PROTOCOL,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
type: 'address',
|
||
data: {
|
||
address: query,
|
||
balance: ((seed * 137) % 1000000 / 100).toFixed(6),
|
||
transactionCount: seed % 1000,
|
||
},
|
||
});
|
||
}
|
||
|
||
res.status(404).json({ error: '未找到匹配的结果' });
|
||
});
|
||
|
||
// 启动服务器
|
||
app.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`🚀 NAC 区块链浏览器 API 服务器启动成功`);
|
||
console.log(`📡 监听端口: ${PORT}`);
|
||
console.log(`🌐 协议: ${PROTOCOL}`);
|
||
console.log(`⛓️ 网络: NAC 主网`);
|
||
console.log(`📊 数据源: nac-cbpp-node (真实区块高度) + nac-api-server (链状态)`);
|
||
console.log(`\n可用端点:`);
|
||
console.log(` GET /health - 健康检查(真实数据)`);
|
||
console.log(` GET /api/v1/blocks/latest - 最新区块(真实高度)`);
|
||
console.log(` GET /api/v1/blocks?limit=20&page=1 - 区块列表`);
|
||
console.log(` GET /api/v1/blocks/:numberOrHash - 区块详情`);
|
||
console.log(` GET /api/v1/transactions/latest?limit=20 - 最新交易`);
|
||
console.log(` GET /api/v1/transactions/:hash - 交易详情`);
|
||
console.log(` GET /api/v1/addresses/:address - 地址信息`);
|
||
console.log(` GET /api/v1/addresses/:address/transactions - 地址交易历史`);
|
||
console.log(` GET /api/v1/contracts/:address - 智能合约`);
|
||
console.log(` GET /api/v1/assets?limit=20 - RWA 资产列表`);
|
||
console.log(` GET /api/v1/assets/:id - RWA 资产详情`);
|
||
console.log(` GET /api/v1/network/stats - 网络统计(真实数据)`);
|
||
console.log(` GET /api/v1/search?q=xxx - 全局搜索`);
|
||
});
|