Skip to content

Commit c824a8e

Browse files
authored
Merge pull request #1286 from lilycom02/main
feat: Implement blockchain indexer API routes
2 parents ec18d92 + b687ac7 commit c824a8e

File tree

9 files changed

+593
-0
lines changed

9 files changed

+593
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Ethereum Node URL
2+
ETH_NODE_URL=https://mainnet.infura.io/v3/YOUR-PROJECT-ID
3+
4+
# MongoDB Connection String
5+
MONGODB_URI=mongodb://localhost:27017/blockchain-indexer
6+
7+
# Server Port
8+
PORT=3000

basic/83-blockchain-indexer/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# 区块链索引器
2+
3+
## 简介
4+
本项目实现了一个简单的区块链索引器,用于抓取、解析和存储区块链数据。通过这个项目,你可以学习到:
5+
6+
1. 如何从区块链节点获取区块数据
7+
2. 如何解析区块和交易信息
8+
3. 如何设计和实现数据存储结构
9+
4. 如何构建查询API接口
10+
11+
## 功能特点
12+
13+
- 实时同步区块数据
14+
- 解析交易信息
15+
- 数据持久化存储
16+
- 提供查询API接口
17+
18+
## 技术栈
19+
20+
- Node.js
21+
- Web3.js
22+
- Express.js
23+
- MongoDB
24+
25+
## 项目结构
26+
27+
```
28+
├── src/
29+
│ ├── indexer/ # 索引器核心逻辑
30+
│ ├── models/ # 数据模型
31+
│ ├── api/ # API接口
32+
│ └── utils/ # 工具函数
33+
├── config/ # 配置文件
34+
└── tests/ # 测试文件
35+
```
36+
37+
## 快速开始
38+
39+
1. 安装依赖
40+
```bash
41+
npm install
42+
```
43+
44+
2. 配置环境变量
45+
```bash
46+
cp .env.example .env
47+
# 编辑.env文件,设置必要的配置项
48+
```
49+
50+
3. 启动项目
51+
```bash
52+
npm start
53+
```
54+
55+
## API文档
56+
57+
### 获取区块信息
58+
```
59+
GET /api/block/:blockNumber
60+
```
61+
62+
### 获取交易信息
63+
```
64+
GET /api/transaction/:txHash
65+
```
66+
67+
### 获取地址信息
68+
```
69+
GET /api/address/:address
70+
```
71+
72+
## 开发计划
73+
74+
- [ ] 实现基础区块同步功能
75+
- [ ] 实现交易解析功能
76+
- [ ] 实现数据存储功能
77+
- [ ] 实现API接口
78+
- [ ] 添加测试用例
79+
- [ ] 优化性能
80+
- [ ] 添加监控功能
81+
82+
## 注意事项
83+
84+
1. 确保有可用的以太坊节点
85+
2. 注意数据同步的性能优化
86+
3. 建议使用索引优化查询性能
87+
88+
## 参考资料
89+
90+
- [Web3.js文档](https://web3js.readthedocs.io/)
91+
- [以太坊JSON-RPC API](https://eth.wiki/json-rpc/API)
92+
- [MongoDB文档](https://docs.mongodb.com/)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "blockchain-indexer",
3+
"version": "1.0.0",
4+
"description": "A simple blockchain indexer for learning purposes",
5+
"main": "src/index.js",
6+
"scripts": {
7+
"start": "node src/index.js",
8+
"dev": "nodemon src/index.js",
9+
"test": "jest"
10+
},
11+
"dependencies": {
12+
"web3": "^1.9.0",
13+
"express": "^4.18.2",
14+
"mongoose": "^7.0.3",
15+
"dotenv": "^16.0.3",
16+
"winston": "^3.8.2"
17+
},
18+
"devDependencies": {
19+
"jest": "^29.5.0",
20+
"nodemon": "^2.0.22"
21+
}
22+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const Block = require('../models/Block');
4+
5+
// 获取区块信息
6+
router.get('/block/:blockNumber', async (req, res) => {
7+
try {
8+
const blockNumber = parseInt(req.params.blockNumber);
9+
const blockInfo = await Block.findOne({ number: blockNumber });
10+
11+
if (!blockInfo) {
12+
return res.status(404).json({
13+
success: false,
14+
error: '区块未找到'
15+
});
16+
}
17+
18+
res.json({
19+
success: true,
20+
data: blockInfo
21+
});
22+
} catch (error) {
23+
res.status(500).json({
24+
success: false,
25+
error: error.message
26+
});
27+
}
28+
});
29+
30+
// 获取交易信息
31+
router.get('/transaction/:txHash', async (req, res) => {
32+
try {
33+
const txHash = req.params.txHash;
34+
const block = await Block.findOne({ 'transactions.hash': txHash });
35+
36+
if (!block) {
37+
return res.status(404).json({
38+
success: false,
39+
error: '交易未找到'
40+
});
41+
}
42+
43+
const txInfo = block.transactions.find(tx => tx.hash === txHash);
44+
res.json({
45+
success: true,
46+
data: txInfo
47+
});
48+
} catch (error) {
49+
res.status(500).json({
50+
success: false,
51+
error: error.message
52+
});
53+
}
54+
});
55+
56+
// 获取地址的交易历史
57+
router.get('/address/:address/transactions', async (req, res) => {
58+
try {
59+
const address = req.params.address.toLowerCase();
60+
const page = parseInt(req.query.page) || 1;
61+
const limit = parseInt(req.query.limit) || 10;
62+
const skip = (page - 1) * limit;
63+
64+
const query = {
65+
$or: [
66+
{ 'transactions.from': address },
67+
{ 'transactions.to': address }
68+
]
69+
};
70+
71+
const [blocks, total] = await Promise.all([
72+
Block.find(query)
73+
.sort({ number: -1 })
74+
.skip(skip)
75+
.limit(limit),
76+
Block.countDocuments(query)
77+
]);
78+
79+
const transactions = blocks.reduce((acc, block) => {
80+
return acc.concat(
81+
block.transactions.filter(tx =>
82+
tx.from.toLowerCase() === address ||
83+
(tx.to && tx.to.toLowerCase() === address)
84+
)
85+
);
86+
}, []);
87+
88+
res.json({
89+
success: true,
90+
data: {
91+
transactions,
92+
pagination: {
93+
page,
94+
limit,
95+
total
96+
}
97+
}
98+
});
99+
} catch (error) {
100+
res.status(500).json({
101+
success: false,
102+
error: error.message
103+
});
104+
}
105+
});
106+
107+
// 获取最新的区块列表
108+
router.get('/blocks/latest', async (req, res) => {
109+
try {
110+
const limit = parseInt(req.query.limit) || 10;
111+
const blocks = await Block.find()
112+
.sort({ number: -1 })
113+
.limit(limit)
114+
.select('-transactions');
115+
116+
res.json({
117+
success: true,
118+
data: blocks
119+
});
120+
} catch (error) {
121+
res.status(500).json({
122+
success: false,
123+
error: error.message
124+
});
125+
}
126+
});
127+
128+
// 获取最新的交易列表
129+
router.get('/transactions/latest', async (req, res) => {
130+
try {
131+
const limit = parseInt(req.query.limit) || 10;
132+
const blocks = await Block.find()
133+
.sort({ number: -1 })
134+
.limit(Math.ceil(limit / 2))
135+
.select('transactions');
136+
137+
const transactions = blocks.reduce((acc, block) => {
138+
return acc.concat(block.transactions);
139+
}, []).slice(0, limit);
140+
141+
res.json({
142+
success: true,
143+
data: transactions
144+
});
145+
} catch (error) {
146+
res.status(500).json({
147+
success: false,
148+
error: error.message
149+
});
150+
}
151+
});
152+
153+
// 搜索功能(支持区块号、交易哈希、地址)
154+
router.get('/search/:query', async (req, res) => {
155+
try {
156+
const query = req.params.query;
157+
let result = {
158+
type: '',
159+
data: null
160+
};
161+
162+
// 尝试作为区块号搜索
163+
if (/^\d+$/.test(query)) {
164+
const block = await Block.findOne({ number: parseInt(query) });
165+
if (block) {
166+
result = { type: 'block', data: block };
167+
}
168+
}
169+
170+
// 尝试作为交易哈希搜索
171+
if (!result.data && /^0x[a-fA-F0-9]{64}$/.test(query)) {
172+
const block = await Block.findOne({ 'transactions.hash': query });
173+
if (block) {
174+
const transaction = block.transactions.find(tx => tx.hash === query);
175+
if (transaction) {
176+
result = { type: 'transaction', data: transaction };
177+
}
178+
}
179+
}
180+
181+
// 尝试作为地址搜索
182+
if (!result.data && /^0x[a-fA-F0-9]{40}$/.test(query)) {
183+
const address = query.toLowerCase();
184+
const blocks = await Block.find({
185+
$or: [
186+
{ 'transactions.from': address },
187+
{ 'transactions.to': address }
188+
]
189+
}).limit(10);
190+
191+
if (blocks.length > 0) {
192+
const transactions = blocks.reduce((acc, block) => {
193+
return acc.concat(
194+
block.transactions.filter(tx =>
195+
tx.from.toLowerCase() === address ||
196+
(tx.to && tx.to.toLowerCase() === address)
197+
)
198+
);
199+
}, []);
200+
result = { type: 'address', data: transactions };
201+
}
202+
}
203+
204+
res.json({
205+
success: true,
206+
data: result
207+
});
208+
} catch (error) {
209+
res.status(500).json({
210+
success: false,
211+
error: error.message
212+
});
213+
}
214+
});
215+
216+
217+
module.exports = router;

0 commit comments

Comments
 (0)