diff --git a/doc/api.md b/doc/api.md
index f6db880..05ad34d 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -14,8 +14,8 @@
* [3.1 统计delegate,undelegate,claim总量](#3.1)
1.1 获取Validator集合
-* `GET /api/valiators`
-* 参数
+* `GET /api/valiators`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|-----------|--------|----|----------|
@@ -24,8 +24,8 @@
| page | number | N | 页码,默认1 |
| page_size | number | N | 页大小,默认10 |
-* Request: `http://localhost/api/validators?online=true&page=1&page_size=5`
-* Response: 返回结果按power(质押总量)降序排列
+* Request: `http://localhost/api/validators?online=true&page=1&page_size=5`
+* Response: 返回结果按power(质押总量)降序排列
```json
{
"total": 31,
@@ -146,15 +146,15 @@
```
1.2 获取validator最近20笔质押变化
-* `GET /api/diff/latest`
-* 参数
+* `GET /api/diff/latest`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|-----------|--------|----|-------------|
| validator | string | Y | validator地址 |
-* Request: `http://localhost/api/diff/latest?validator=0xc8d2d4ff0b882243f82c1fb20574c81e4c866e72`
-* Response:
+* Request: `http://localhost/api/diff/latest?validator=0xc8d2d4ff0b882243f82c1fb20574c81e4c866e72`
+* Response:
* 按高度降序排列
* 返回值中`amount`,非零正数表示delegate的数量,非零负数表示undelegate的数量
* 如果`amount`是0,则用`op`区分,`op`为零表示delegate,非零表示undelegate
@@ -247,8 +247,8 @@
]
```
1.3 获取validator的delegate记录
-* `GET /api/records/delegate`
-* 参数
+* `GET /api/records/delegate`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|-----------|--------|----|--------------------------|
@@ -256,8 +256,8 @@
| page | number | N | 页码,默认1 |
| page_size | number | N | 页大小,默认10 |
-* Request: `http://localhost/api/records/delegate?validator=0xc8d2d4ff0b882243f82c1fb20574c81e4c866e72&page=1&page_size=5`
-* Response:
+* Request: `http://localhost/api/records/delegate?validator=0xc8d2d4ff0b882243f82c1fb20574c81e4c866e72&page=1&page_size=5`
+* Response:
* 按`timestamp`降序排列
```json
{
@@ -304,8 +304,8 @@
```
1.4 获取validator的undelegate记录
-* `GET /api/records/undelegate`
-* 参数
+* `GET /api/records/undelegate`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|-----------|--------|----|---------------------------|
@@ -313,8 +313,8 @@
| page | number | N | 页码,默认1 |
| page_size | number | N | 页大小,默认10 |
-* Request: `http://localhost/api/records/undelegate?validator=0x6e20c920f1bdb817f0e19cd05dae01c6affa5228&page=1&page_size=10`
-* Response:
+* Request: `http://localhost/api/records/undelegate?validator=0x6e20c920f1bdb817f0e19cd05dae01c6affa5228&page=1&page_size=10`
+* Response:
* 按`timestamp`降序排列
```json
{
@@ -342,16 +342,16 @@
2.1 获取bound数量
-* `GET /api/bound`
-* 参数
+* `GET /api/bound`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|-----------|--------|----|-------------|
| validator | string | Y | validator地址 |
| delegator | string | Y | delegator地址 |
-* Request: `http://localhost/api/bound?validator=0x09ef1db6b67d1cbf7eba6bd9b204611848993df7&delegator=0x2d15d52cc138ffb322b732239cd3630735abac88`
-* Response:
+* Request: `http://localhost/api/bound?validator=0x09ef1db6b67d1cbf7eba6bd9b204611848993df7&delegator=0x2d15d52cc138ffb322b732239cd3630735abac88`
+* Response:
```json
{
"bound_amount": "110000001800000063120",
@@ -360,17 +360,16 @@
```
-
2.2 获取reward数量
-* `GET /api/reward`
-* 参数
+* `GET /api/reward`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|---------|--------|----|-------------|
| address | string | Y | delegator地址 |
-* Request: `http://localhost/api/reward?address=0x2d15d52cc138ffb322b732239cd3630735abac88`
-* Response:
+* Request: `http://localhost/api/reward?address=0x2d15d52cc138ffb322b732239cd3630735abac88`
+* Response:
```json
{
"reward": "16742332457649244907"
@@ -378,16 +377,16 @@
```
2.3 获取debt数量
-* `GET /api/debt`
-* 参数
+* `GET /api/debt`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|-----------|--------|----|-------------|
| validator | string | Y | validator地址 |
| delegator | string | Y | delegator地址 |
-* Request: `http://localhost/api/debt?validator=0xd518c4f95a3f39ed853a2614566897c4ad5a008f&delegator=0x2d15d52cc138ffb322b732239cd3630735abac88`
-* Response:
+* Request: `http://localhost/api/debt?validator=0xd518c4f95a3f39ed853a2614566897c4ad5a008f&delegator=0x2d15d52cc138ffb322b732239cd3630735abac88`
+* Response:
```json
{
"debt": "96558069283467635"
@@ -395,15 +394,15 @@
```
3.1 获取debt数量
-* `GET /api/sum`
-* 参数
+* `GET /api/sum`
+* 参数
| 参数 | 类型 | 必传 | 说明 |
|---------|--------|----|----|
| address | string | Y | 地址 |
-* Request: `http://localhost/api/sum?address=0xeb2b96369e83e1466bb56f2bf9d97cbda130e741`
-* Response:
+* Request: `http://localhost/api/sum?address=0xeb2b96369e83e1466bb56f2bf9d97cbda130e741`
+* Response:
```json
{
"delegate": "32254951206000000000000",
diff --git a/updater/Cargo.toml b/updater/Cargo.toml
new file mode 100644
index 0000000..d1dbcb0
--- /dev/null
+++ b/updater/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "updater"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+base64 = "0.22.0"
+clap = { version = "4.5.1", features = ["derive"] }
+crossbeam = "0.8.4"
+env_logger = "0.11.2"
+ethers = { version = "2.0.13", features = ["abigen","legacy"] }
+log = "0.4.20"
+num_cpus = "1.16.0"
+reqwest = { version = "0.12.0", features = ["json"] }
+rustc-hex = "2.1.0"
+serde = "1.0.197"
+serde_json = "1.0.114"
+sha2 = "0.10.8"
+sqlx = { version = "0.7.3", features = ["bigdecimal", "runtime-tokio", "postgres", "chrono", "json"]}
+tokio = { version = "1.36.0", features = ["full"]}
+toml = "0.8.12"
+url = "2.5.0"
diff --git a/updater/src/main.rs b/updater/src/main.rs
new file mode 100644
index 0000000..ae95894
--- /dev/null
+++ b/updater/src/main.rs
@@ -0,0 +1,91 @@
+mod db;
+mod error;
+mod updater;
+
+use crate::db::Storage;
+use crate::error::Result;
+use crate::updater::Updater;
+use clap::Parser;
+use ethers::contract::abigen;
+use ethers::prelude::{Http, Provider};
+use ethers::types::Address;
+use log::info;
+use serde::{Deserialize, Serialize};
+use sqlx::pool::PoolOptions;
+use sqlx::{Pool, Postgres};
+use std::fs::File;
+use std::io::Read;
+use std::sync::Arc;
+use std::time::Duration;
+
+const DEFAULT_RPC_RETRIES: usize = 3;
+const DEFAULT_INTERVAL: u64 = 15; // 15s
+
+abigen!(RewardContract, "../abi/Reward.json");
+abigen!(StakingContract, "../abi/Staking.json");
+
+#[derive(Serialize, Deserialize)]
+struct UpdaterConfig {
+ pub evm_rpc: String,
+ pub staking: String,
+ pub reward: String,
+ pub db_url: String,
+}
+
+impl UpdaterConfig {
+ pub fn new(file_path: &str) -> Result {
+ let mut f = File::open(file_path)?;
+ let mut s = String::new();
+ f.read_to_string(&mut s)?;
+ let c: UpdaterConfig = toml::from_str(&s)?;
+ Ok(c)
+ }
+}
+
+#[derive(Parser, Debug)]
+struct Args {
+ /// Node RPC
+ #[arg(long)]
+ pub node: String,
+ /// Block height to start scanning
+ #[arg(long)]
+ pub start: Option,
+ /// Interval of scanning in seconds
+ #[arg(long)]
+ pub interval: Option,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ env_logger::init();
+
+ let config = UpdaterConfig::new("./config.toml")?;
+ info!("EVM RPC: {}", config.evm_rpc);
+ info!("Staking contract: {}", config.staking);
+ info!("Reward contract: {}", config.reward);
+
+ let pool: Pool = PoolOptions::new()
+ .connect(&config.db_url)
+ .await
+ .expect("can't connect to database");
+ info!("Connecting db...ok");
+
+ let storage = Storage::new(pool);
+ let args = Args::parse();
+ let interval = if let Some(interval) = args.interval {
+ Duration::from_secs(interval)
+ } else {
+ Duration::from_secs(DEFAULT_INTERVAL)
+ };
+
+ let provider = Provider::::try_from(config.evm_rpc)?;
+ let staking_addr: Address = config.staking.parse()?;
+ let staking = StakingContract::new(staking_addr, Arc::new(provider.clone()));
+ let reward_addr: Address = config.reward.parse()?;
+ let reward = RewardContract::new(reward_addr, Arc::new(provider.clone()));
+ info!("Updating interval: {}s", interval.as_secs());
+ let updater = Updater::new(DEFAULT_RPC_RETRIES, provider, staking, reward, storage);
+ let _ = updater.run().await?;
+
+ Ok(())
+}