Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement blockchain indexer API routes #1286

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions basic/83-blockchain-indexer/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Ethereum Node URL
ETH_NODE_URL=https://mainnet.infura.io/v3/YOUR-PROJECT-ID

# MongoDB Connection String
MONGODB_URI=mongodb://localhost:27017/blockchain-indexer

# Server Port
PORT=3000
92 changes: 92 additions & 0 deletions basic/83-blockchain-indexer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 区块链索引器

## 简介
本项目实现了一个简单的区块链索引器,用于抓取、解析和存储区块链数据。通过这个项目,你可以学习到:

1. 如何从区块链节点获取区块数据
2. 如何解析区块和交易信息
3. 如何设计和实现数据存储结构
4. 如何构建查询API接口

## 功能特点

- 实时同步区块数据
- 解析交易信息
- 数据持久化存储
- 提供查询API接口

## 技术栈

- Node.js
- Web3.js
- Express.js
- MongoDB

## 项目结构

```
├── src/
│ ├── indexer/ # 索引器核心逻辑
│ ├── models/ # 数据模型
│ ├── api/ # API接口
│ └── utils/ # 工具函数
├── config/ # 配置文件
└── tests/ # 测试文件
```

## 快速开始

1. 安装依赖
```bash
npm install
```

2. 配置环境变量
```bash
cp .env.example .env
# 编辑.env文件,设置必要的配置项
```

3. 启动项目
```bash
npm start
```

## API文档

### 获取区块信息
```
GET /api/block/:blockNumber
```

### 获取交易信息
```
GET /api/transaction/:txHash
```

### 获取地址信息
```
GET /api/address/:address
```

## 开发计划

- [ ] 实现基础区块同步功能
- [ ] 实现交易解析功能
- [ ] 实现数据存储功能
- [ ] 实现API接口
- [ ] 添加测试用例
- [ ] 优化性能
- [ ] 添加监控功能

## 注意事项

1. 确保有可用的以太坊节点
2. 注意数据同步的性能优化
3. 建议使用索引优化查询性能

## 参考资料

- [Web3.js文档](https://web3js.readthedocs.io/)
- [以太坊JSON-RPC API](https://eth.wiki/json-rpc/API)
- [MongoDB文档](https://docs.mongodb.com/)
22 changes: 22 additions & 0 deletions basic/83-blockchain-indexer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "blockchain-indexer",
"version": "1.0.0",
"description": "A simple blockchain indexer for learning purposes",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest"
},
"dependencies": {
"web3": "^1.9.0",
"express": "^4.18.2",
"mongoose": "^7.0.3",
"dotenv": "^16.0.3",
"winston": "^3.8.2"
},
"devDependencies": {
"jest": "^29.5.0",
"nodemon": "^2.0.22"
}
}
217 changes: 217 additions & 0 deletions basic/83-blockchain-indexer/src/api/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
const express = require('express');
const router = express.Router();
const Block = require('../models/Block');

// 获取区块信息
router.get('/block/:blockNumber', async (req, res) => {
try {
const blockNumber = parseInt(req.params.blockNumber);
const blockInfo = await Block.findOne({ number: blockNumber });

if (!blockInfo) {
return res.status(404).json({
success: false,
error: '区块未找到'
});
}

res.json({
success: true,
data: blockInfo
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});

// 获取交易信息
router.get('/transaction/:txHash', async (req, res) => {
try {
const txHash = req.params.txHash;
const block = await Block.findOne({ 'transactions.hash': txHash });

if (!block) {
return res.status(404).json({
success: false,
error: '交易未找到'
});
}

const txInfo = block.transactions.find(tx => tx.hash === txHash);
res.json({
success: true,
data: txInfo
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});

// 获取地址的交易历史
router.get('/address/:address/transactions', async (req, res) => {
try {
const address = req.params.address.toLowerCase();
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;

const query = {
$or: [
{ 'transactions.from': address },
{ 'transactions.to': address }
]
};

const [blocks, total] = await Promise.all([
Block.find(query)
.sort({ number: -1 })
.skip(skip)
.limit(limit),
Block.countDocuments(query)
]);

const transactions = blocks.reduce((acc, block) => {
return acc.concat(
block.transactions.filter(tx =>
tx.from.toLowerCase() === address ||
(tx.to && tx.to.toLowerCase() === address)
)
);
}, []);

res.json({
success: true,
data: {
transactions,
pagination: {
page,
limit,
total
}
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});

// 获取最新的区块列表
router.get('/blocks/latest', async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
const blocks = await Block.find()
.sort({ number: -1 })
.limit(limit)
.select('-transactions');

res.json({
success: true,
data: blocks
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});

// 获取最新的交易列表
router.get('/transactions/latest', async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
const blocks = await Block.find()
.sort({ number: -1 })
.limit(Math.ceil(limit / 2))
.select('transactions');

const transactions = blocks.reduce((acc, block) => {
return acc.concat(block.transactions);
}, []).slice(0, limit);

res.json({
success: true,
data: transactions
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});

// 搜索功能(支持区块号、交易哈希、地址)
router.get('/search/:query', async (req, res) => {
try {
const query = req.params.query;
let result = {
type: '',
data: null
};

// 尝试作为区块号搜索
if (/^\d+$/.test(query)) {
const block = await Block.findOne({ number: parseInt(query) });
if (block) {
result = { type: 'block', data: block };
}
}

// 尝试作为交易哈希搜索
if (!result.data && /^0x[a-fA-F0-9]{64}$/.test(query)) {
const block = await Block.findOne({ 'transactions.hash': query });
if (block) {
const transaction = block.transactions.find(tx => tx.hash === query);
if (transaction) {
result = { type: 'transaction', data: transaction };
}
}
}

// 尝试作为地址搜索
if (!result.data && /^0x[a-fA-F0-9]{40}$/.test(query)) {
const address = query.toLowerCase();
const blocks = await Block.find({
$or: [
{ 'transactions.from': address },
{ 'transactions.to': address }
]
}).limit(10);

if (blocks.length > 0) {
const transactions = blocks.reduce((acc, block) => {
return acc.concat(
block.transactions.filter(tx =>
tx.from.toLowerCase() === address ||
(tx.to && tx.to.toLowerCase() === address)
)
);
}, []);
result = { type: 'address', data: transactions };
}
}

res.json({
success: true,
data: result
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});


module.exports = router;
Loading