├── lib ├── src │ ├── lib.rs │ ├── db.rs │ └── neo.rs ├── Cargo.toml └── README.md ├── indexer ├── src │ ├── db │ │ ├── mod.rs │ │ ├── model.rs │ │ └── database.rs │ ├── spawn │ │ ├── mod.rs │ │ ├── sync.rs │ │ └── indexer.rs │ ├── rpc │ │ ├── mod.rs │ │ ├── method.rs │ │ ├── client.rs │ │ └── models.rs │ ├── utils │ │ ├── mod.rs │ │ ├── logger.rs │ │ ├── node.rs │ │ ├── conversion.rs │ │ └── conversion_test.rs │ ├── config.rs │ └── main.rs ├── Cargo.toml ├── README.md └── config │ └── protocol.mainnet.yml ├── api ├── src │ ├── transaction │ │ ├── mod.rs │ │ ├── controller.rs │ │ └── internals.rs │ ├── shared │ │ ├── mod.rs │ │ ├── models.rs │ │ ├── checker.rs │ │ └── events.rs │ ├── stat │ │ ├── mod.rs │ │ ├── models.rs │ │ ├── controller.rs │ │ └── internals.rs │ ├── block │ │ ├── mod.rs │ │ ├── models.rs │ │ ├── controller.rs │ │ └── internals.rs │ ├── error │ │ └── mod.rs │ └── main.rs ├── Cargo.toml ├── README.md └── Cargo.lock ├── gui ├── src │ ├── constants │ │ └── index.js │ ├── entry-client.jsx │ ├── routes │ │ ├── result.jsx │ │ ├── visualizer.jsx │ │ ├── [...404].jsx │ │ └── index.jsx │ ├── components │ │ ├── Search.jsx │ │ ├── About.jsx │ │ ├── Results.jsx │ │ ├── Visualizer.jsx │ │ ├── Query.jsx │ │ └── Stats.jsx │ ├── entry-server.jsx │ ├── helpers │ │ ├── checker.js │ │ ├── fetcher.js │ │ ├── formatter.js │ │ └── grapher.js │ └── root.jsx ├── public │ └── favicon.ico ├── vite.config.js ├── jsconfig.json ├── package.json └── README.md ├── Cargo.toml ├── .gitignore ├── todo.md ├── README.md └── LICENSE /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | pub mod neo; 3 | -------------------------------------------------------------------------------- /indexer/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; 2 | pub mod model; 3 | -------------------------------------------------------------------------------- /indexer/src/spawn/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod indexer; 2 | pub mod sync; 3 | -------------------------------------------------------------------------------- /api/src/transaction/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod controller; 2 | mod internals; 3 | -------------------------------------------------------------------------------- /api/src/shared/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod checker; 2 | pub mod events; 3 | pub mod models; 4 | -------------------------------------------------------------------------------- /api/src/stat/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod controller; 2 | pub mod internals; 3 | mod models; 4 | -------------------------------------------------------------------------------- /gui/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const API_PATH = "http://127.0.0.1:8080/v1" 2 | -------------------------------------------------------------------------------- /indexer/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod method; 3 | pub mod models; 4 | -------------------------------------------------------------------------------- /api/src/block/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod controller; 2 | pub mod internals; 3 | pub mod models; 4 | -------------------------------------------------------------------------------- /gui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgedlt/shrike/HEAD/gui/public/favicon.ico -------------------------------------------------------------------------------- /indexer/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod conversion; 2 | pub mod logger; 3 | pub mod node; 4 | mod conversion_test; 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "api", 6 | "indexer", 7 | "lib" 8 | ] -------------------------------------------------------------------------------- /gui/src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | import { mount, StartClient } from "solid-start/entry-client" 2 | mount(() => , document) 3 | -------------------------------------------------------------------------------- /gui/vite.config.js: -------------------------------------------------------------------------------- 1 | import solid from "solid-start/vite"; 2 | import { defineConfig } from "vite"; 3 | export default defineConfig({ 4 | plugins: [solid()], 5 | }); 6 | -------------------------------------------------------------------------------- /gui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "jsxImportSource": "solid-js", 5 | "paths": { 6 | "~/*": [ 7 | "./src/*" 8 | ] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /gui/src/routes/result.jsx: -------------------------------------------------------------------------------- 1 | import Results from "~/components/Results" 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /gui/src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | export default function Search() { 2 | return ( 3 |
4 | 5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /gui/src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | createHandler, 3 | renderAsync, 4 | StartServer 5 | } from "solid-start/entry-server" 6 | export default createHandler( 7 | renderAsync((event) => ) 8 | ) 9 | -------------------------------------------------------------------------------- /gui/src/routes/visualizer.jsx: -------------------------------------------------------------------------------- 1 | import Visualizer from "~/components/Visualizer" 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /api/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug)] 4 | pub struct Error { 5 | pub error: String, 6 | } 7 | 8 | #[allow(dead_code, clippy::enum_variant_names)] 9 | pub enum Errors { 10 | SqlError, 11 | RequestError, 12 | RateLimitError, 13 | NotImplementedError, 14 | } 15 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | base64 = { version = "0.20.0" } 10 | hex = { version = "0.4.3" } 11 | sha2 = "0.10.6" 12 | directories-next = "2.0.0" 13 | once_cell = "1.17.1" 14 | lazy_static = "1.5" 15 | 16 | -------------------------------------------------------------------------------- /gui/src/routes/[...404].jsx: -------------------------------------------------------------------------------- 1 | import { A, Title } from "solid-start" 2 | import { HttpStatusCode } from "solid-start/server" 3 | export default function NotFound() { 4 | return ( 5 |
6 | Not Found 7 | 8 |

Page Not Found

9 | Home 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /gui/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import Query from "~/components/Query" 2 | import Stats from "~/components/Stats" 3 | import About from "~/components/About" 4 | export default function Home() { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /api/src/block/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | #[derive(Serialize, Deserialize, Clone)] 4 | pub struct Block { 5 | pub index: u64, 6 | pub hash: String, 7 | pub size: u32, 8 | pub version: u8, 9 | pub merkle_root: String, 10 | pub time: u64, 11 | pub nonce: String, 12 | pub speaker: u8, 13 | pub next_consensus: String, 14 | pub reward: f64, 15 | pub reward_receiver: String, 16 | pub witnesses: Value, 17 | } 18 | -------------------------------------------------------------------------------- /api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "api" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix-web = "4.2.1" 10 | serde_json = "1.0.91" 11 | serde = { version = "1.0.152", features = ["derive"] } 12 | rusqlite = { version = "0.28.0", features = ["bundled", "serde_json"] } 13 | r2d2 = "0.8.10" 14 | r2d2_sqlite = "0.21.0" 15 | actix-cors = "0.6.4" 16 | tokio = { version = "1.24.2", features = ["rt", "macros"] } 17 | futures = "0.3.25" 18 | once_cell = "1.17.0" 19 | lib = { path = "../lib" } 20 | -------------------------------------------------------------------------------- /gui/src/helpers/checker.js: -------------------------------------------------------------------------------- 1 | export class Checker { 2 | static isNeoAddress(s) { 3 | return (s.length === 34 && s[0] === "N" && /^[A-HJ-NP-Za-km-z1-9]*$/.test(s)) ? true : false 4 | } 5 | 6 | static isNeoScriptHash(s) { 7 | return (s.length === 42 && s.slice(0, 2) === "0x" === /^[A-Fa-f0-9]*$/.test(s.slice(2, 42))) ? true : false 8 | } 9 | 10 | static isNeoTxidHash(s) { 11 | return (s.length === 66 && s.slice(0, 2) === "0x" && /^[A-Fa-f0-9]*$/.test(s.slice(2, 66))) ? true : false 12 | } 13 | 14 | static isReasonableNumber(s) { 15 | return (s.length < 9 && /^[0-9]*$/.test(s)) ? true : false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | 4 | # api 5 | /api/target 6 | 7 | # indexer 8 | /indexer/target 9 | /indexer/chains 10 | /indexer/log/neogo.log 11 | /indexer/.env 12 | /indexer/neogo 13 | /indexer/neogo.exe 14 | /indexer/shrike.db3 15 | /indexer/shrike.db3-journal 16 | /indexer/shrike.db3-shm 17 | /indexer/shrike.db3-wal 18 | /indexer/todo.md 19 | /indexer/debug.log 20 | /indexer/readme.md.backup 21 | 22 | # gui 23 | /gui/dist 24 | /gui/.solid 25 | /gui/.output 26 | /gui/.vercel 27 | /gui/.netlify 28 | /gui/netlify 29 | /gui/node_modules 30 | /gui/.idea 31 | /gui/.project 32 | /gui/.classpath 33 | /gui/*.launch 34 | /gui/.settings/ 35 | /gui/.DS_Store 36 | /gui/Thumbs.db 37 | -------------------------------------------------------------------------------- /lib/src/db.rs: -------------------------------------------------------------------------------- 1 | use directories_next::ProjectDirs; 2 | use once_cell::sync::Lazy; 3 | 4 | use std::{fs, path::PathBuf}; 5 | 6 | pub static DB_PATH: Lazy = Lazy::new(|| { 7 | let project_dirs = 8 | ProjectDirs::from("", "", "Shrike").expect("Failed to get project directories"); 9 | let mut path = project_dirs.data_local_dir().to_path_buf(); 10 | path.push("shrike.db3"); 11 | 12 | // Check if the parent directory exists and create it if necessary 13 | let parent = path.parent().expect("Failed to get db parent directory"); 14 | if !parent.exists() { 15 | fs::create_dir_all(parent).expect("Failed to create db parent directory"); 16 | } 17 | 18 | path 19 | }); 20 | -------------------------------------------------------------------------------- /indexer/src/utils/logger.rs: -------------------------------------------------------------------------------- 1 | use log::LevelFilter; 2 | 3 | use std::io::{self, Write}; 4 | 5 | use crate::config::AppConfig; 6 | 7 | pub fn init() { 8 | let config = AppConfig::new(); 9 | let level = match config.log_level.as_str() { 10 | "debug" => LevelFilter::Debug, 11 | "info" => LevelFilter::Info, 12 | "warn" => LevelFilter::Warn, 13 | "error" => LevelFilter::Error, 14 | _ => LevelFilter::Off, 15 | }; 16 | 17 | env_logger::builder() 18 | .filter_level(level) 19 | .format_timestamp(None) 20 | .init(); 21 | } 22 | 23 | pub fn inline_print(message: &str) { 24 | print!("{message}"); 25 | io::stdout().flush().unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /gui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gui", 3 | "scripts": { 4 | "dev": "solid-start dev", 5 | "build": "solid-start build", 6 | "start": "solid-start start" 7 | }, 8 | "type": "module", 9 | "devDependencies": { 10 | "esbuild": "^0.14.54", 11 | "postcss": "^8.4.18", 12 | "solid-start-node": "^0.2.0", 13 | "vite": "^3.1.8" 14 | }, 15 | "dependencies": { 16 | "@solidjs/meta": "^0.28.0", 17 | "@solidjs/router": "^0.6.0", 18 | "cytoscape": "^3.23.0", 19 | "cytoscape-dagre": "^2.5.0", 20 | "cytoscape-popper": "^2.0.0", 21 | "solid-js": "^1.6.2", 22 | "solid-start": "^0.2.0", 23 | "undici": "^5.11.0" 24 | }, 25 | "engines": { 26 | "node": ">=16.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ## To Do List 2 | 3 | ### Backend: 4 | 5 | * Add more useful methods 6 | - e.g. 1: Method to get all transfers by address 7 | - e.g. 2: Method to get all transactions by block index/hash 8 | * Make a separate table (view?) for transfers/contracts/balances and associated queries 9 | * Add DB download utility for easy data sharing 10 | * Contract validation/blacklisting (prevent stat manipulation via fake events) 11 | * Add graceful shutdown for SIGTERM 12 | 13 | ### Frontend: 14 | 15 | * JSON to CSV converter for transfers by address 16 | * Add more methods and stats aimed at regular users 17 | * Base58/64 decoding? 18 | * Prettify JSON values 19 | * Replace DB download link with a better download utility 20 | * Wallet support? 21 | -------------------------------------------------------------------------------- /indexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "indexer" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dotenv = { version ="0.15.0" } 10 | rusqlite = { version = "0.28.0", features = ["bundled"] } 11 | serde = { version = "1.0.151", features = ["derive"] } 12 | serde_json = { version = "1.0.89" } 13 | reqwest = { version = "0.11.13", features = ["json", "rustls-tls"], default-features = false } 14 | futures = "0.3.25" 15 | tokio = { version = "1.23.0", features = ["full"] } 16 | lib = { path = "../lib" } 17 | text_io = "0.1.12" 18 | thiserror = "1.0.40" 19 | anyhow = "1.0.70" 20 | env_logger = "0.10.0" 21 | log = "0.4.17" 22 | regex = "1.5.4" -------------------------------------------------------------------------------- /indexer/src/config.rs: -------------------------------------------------------------------------------- 1 | use lib::db::DB_PATH; 2 | 3 | #[derive(Debug)] 4 | pub struct AppConfig { 5 | pub test_db: bool, 6 | pub db_path: String, 7 | pub node_path: String, 8 | pub node_version: String, 9 | pub log_level: String, 10 | pub batch_size: u64, 11 | pub keep_alive: bool, 12 | pub keep_alive_interval: u64, 13 | pub height_limit: u64, 14 | } 15 | 16 | impl AppConfig { 17 | pub fn new() -> Self { 18 | Self { 19 | test_db: false, 20 | db_path: DB_PATH 21 | .to_str() 22 | .expect("Failed to convert path") 23 | .to_string(), 24 | node_path: String::from("http://localhost:10332"), 25 | node_version: String::from("v0.107.2"), 26 | log_level: String::from("info"), 27 | batch_size: 25, 28 | keep_alive: true, 29 | keep_alive_interval: 5, 30 | height_limit: 0, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gui/src/components/About.jsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return ( 3 |
4 |
5 | 6 | Additional resources 7 | 8 |

9 | Shrike is an open source tool. You can make suggestions, contribute code, or learn how to run your own local instance of one or more of its components by visiting the GitHub repo. 10 |

11 |

12 | The GUI (and API behind it) are built to serve regular users. Those looking to run more advanced queries should acquire a full copy of the database, either by running the Indexer or downloading the latest copy here (chain height: 2853950). 13 |

14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # Lib 2 | 3 | The Lib is a shared library used across multiple Shrike components. It provides common functions and models to ensure consistency and reduce code duplication. 4 | 5 | ## Features 6 | 7 | - Neo data conversion methods. 8 | - Database path handling. 9 | - Will be expanded with other functions and models as needed. 10 | 11 | ## Getting Started 12 | 13 | ### Prerequisites 14 | 15 | - Latest stable Rust version. We recommend using [Rustup](https://rustup.rs/). 16 | 17 | ### Usage 18 | 19 | 1. Clone or download the Lib folder. 20 | 2. Import the library into your Rust project by adding the following to your `Cargo.toml`: 21 | 22 | ```toml 23 | [dependencies] 24 | shrike-lib = { path = "path/to/shrike-lib" } 25 | ``` 26 | 27 | 3. Use the functions and models provided by the library in your code. 28 | 29 | ## Contributing 30 | 31 | Contributions to the Lib are welcomed. If you have suggestions for additional functions or improvements to the existing ones, feel free to open an issue or submit a pull request. 32 | -------------------------------------------------------------------------------- /gui/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import { A, useLocation } from "solid-start" 2 | 3 | export default function Results() { 4 | 5 | const result = useLocation().state 6 | 7 | return ( 8 |
9 |

Results

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {(result) => 19 | 20 | 21 | 22 | 23 | 24 | } 25 | 26 |
FieldValue
{result[0]}{result[1]}
27 | Back 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shrike 2 | 3 | Shrike is a suite of tools designed for analyzing Neo blockchain data. The infrastructure comprises four main components: 4 | 5 | - **Indexer** - Synchronizes a NeoGo node, retrieves blockchain data, and processes it into a relational database. 6 | - **API** - Provides a REST API for serving useful queries on indexed data. 7 | - **GUI** - A user-friendly web interface for interacting with the data provided by Shrike. 8 | - **Lib** - A shared library containing methods and models used across multiple Shrike components. 9 | 10 | Each component has its own README with instructions for use. Contributions in the form of pull requests and suggestions for improvements or additional features are welcomed. 11 | 12 | ## Getting Started 13 | 14 | To get started with Shrike, visit the README for the component you want to use: 15 | 16 | - [Indexer readme](./indexer/README.md) 17 | - [API readme](./api/README.md) 18 | - [GUI readme](./gui/README.md) 19 | - [Lib readme](./lib/README.md) 20 | 21 | ## Acknowledgements 22 | 23 | Thanks to the [NeoGo](https://github.com/nspcc-dev/neo-go) team for their excellent software and documentation. Also thanks to @liaojinghui for their work on [neo-rs](https://github.com/Liaojinghui/neo-rs/). 24 | -------------------------------------------------------------------------------- /api/src/stat/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct BlockCount { 5 | pub total_blocks: u64, 6 | } 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct TransactionCount { 10 | pub total_transactions: u64, 11 | } 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct TotalSystemFee { 15 | pub total_sysfee: f64, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub struct TransferCount { 20 | pub total_transfers: u64, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | pub struct SenderCount { 25 | pub total_senders: u64, 26 | } 27 | 28 | #[derive(Serialize, Deserialize)] 29 | pub struct ContractCount { 30 | pub total_contracts: u64, 31 | } 32 | 33 | #[derive(Serialize, Deserialize)] 34 | pub struct ShrikeStats { 35 | pub total_blocks: u64, 36 | pub total_transactions: u64, 37 | pub total_sysfee: f64, 38 | pub total_transfers: u64, 39 | pub total_senders: u64, 40 | pub total_contracts: u64, 41 | } 42 | 43 | #[derive(Serialize, Deserialize)] 44 | pub struct NetworkStatistics { 45 | pub total_transactions: u64, 46 | pub total_addresses: u64, 47 | pub total_contracts: u64, 48 | pub current_week_transactions: u64, 49 | pub current_week_addresses: u64, 50 | pub current_week_contracts: u64, 51 | } 52 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | The API is an Actix Web-based service that executes various queries against the indexed data and serves the responses. It is designed to provide a simple and efficient way to interact with the Shrike database. 4 | 5 | ## Features 6 | 7 | - Executes queries against the Shrike database. 8 | - Serves responses in a simple and efficient manner. 9 | - Supports basic queries with potential for expansion. 10 | 11 | ## Getting Started 12 | 13 | ### Prerequisites 14 | 15 | - Latest stable Rust version. We recommend using [Rustup](https://rustup.rs/). 16 | - A copy of the Shrike database, which can be obtained by running the Indexer. 17 | 18 | ### Quickstart 19 | 20 | 1. Clone or download the API folder. 21 | 2. Update the `DB_PATH` constant in `main.rs` with the path to your Shrike database. 22 | 3. Run `cargo run --release` to serve the API. 23 | 4. Make your requests! The default path for the API when run locally is: `http://0.0.0.0:8080/v1/module/method/parameter`. 24 | 25 | A hosted version of the API will be available in the future. 26 | 27 | ## API Reference 28 | 29 | The API currently supports basic queries. More detailed documentation on the available endpoints and their usage will be provided in the future. 30 | 31 | ## Contributing 32 | 33 | Contributions to the API are welcomed. If you have suggestions for additional queries or improvements to the existing ones, feel free to open an issue or submit a pull request. -------------------------------------------------------------------------------- /indexer/src/rpc/method.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use super::models::{AppLogResult, BlockResult, NeoParam}; 4 | 5 | pub trait RpcMethod { 6 | type ReturnType: for<'de> Deserialize<'de>; 7 | 8 | fn method_name(&self) -> &'static str; 9 | fn params(&self) -> Vec; 10 | } 11 | 12 | pub struct GetBlockCount; 13 | 14 | impl RpcMethod for GetBlockCount { 15 | type ReturnType = u64; 16 | 17 | fn method_name(&self) -> &'static str { 18 | "getblockcount" 19 | } 20 | 21 | fn params(&self) -> Vec { 22 | vec![] 23 | } 24 | } 25 | 26 | pub struct GetBlock { 27 | pub block_height: u64, 28 | pub verbosity: u8, 29 | } 30 | 31 | impl RpcMethod for GetBlock { 32 | type ReturnType = BlockResult; 33 | 34 | fn method_name(&self) -> &'static str { 35 | "getblock" 36 | } 37 | 38 | fn params(&self) -> Vec { 39 | vec![ 40 | NeoParam::Integer(self.block_height), 41 | NeoParam::Integer(u64::from(self.verbosity)), 42 | ] 43 | } 44 | } 45 | 46 | pub struct GetApplicationLog { 47 | pub hash: String, 48 | } 49 | 50 | impl RpcMethod for GetApplicationLog { 51 | type ReturnType = AppLogResult; 52 | 53 | fn method_name(&self) -> &'static str { 54 | "getapplicationlog" 55 | } 56 | 57 | fn params(&self) -> Vec { 58 | vec![NeoParam::String(self.hash.clone())] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /api/src/block/controller.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, HttpResponse, Responder}; 2 | 3 | use crate::error::Error; 4 | use crate::shared::models::TransactionList; 5 | use crate::ConnectionPool; 6 | 7 | use super::internals; 8 | 9 | #[get("/v1/block/{id}")] 10 | async fn get_block(pool: web::Data, path: web::Path) -> impl Responder { 11 | let conn = &pool.connection.get().unwrap(); 12 | let id = path.into_inner(); 13 | 14 | let result = internals::get_block_internal(conn, id); 15 | 16 | match result { 17 | Ok(block) => HttpResponse::Ok().json(block), 18 | Err(_) => HttpResponse::Ok().json(Error { 19 | error: "Block does not exist.".to_string(), 20 | }), 21 | } 22 | } 23 | 24 | #[get("/v1/block/{id}/transactions")] 25 | async fn get_block_transactions( 26 | pool: web::Data, 27 | path: web::Path, 28 | ) -> impl Responder { 29 | let conn = &pool.connection.get().unwrap(); 30 | let id = path.into_inner(); 31 | 32 | let transactions = internals::get_block_transactions_internal(conn, id).unwrap(); 33 | 34 | match transactions.is_empty() { 35 | false => HttpResponse::Ok().json(TransactionList { transactions }), 36 | true => HttpResponse::Ok().json(Error { 37 | error: "No transactions for that block.".to_string(), 38 | }), 39 | } 40 | } 41 | 42 | pub fn config(cfg: &mut web::ServiceConfig) { 43 | cfg.service(get_block).service(get_block_transactions); 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, edgedlt 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /api/src/stat/controller.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, HttpResponse, Responder}; 2 | 3 | use super::internals::CURRENT_NETWORK_STATISTICS; 4 | use super::internals::CURRENT_STATS; 5 | use super::models::NetworkStatistics; 6 | use super::models::ShrikeStats; 7 | 8 | // Now this path is always fast and always up to date 9 | #[get("/v1/stat/stats")] 10 | async fn get_stats() -> impl Responder { 11 | let lock = CURRENT_STATS.read().unwrap(); 12 | 13 | HttpResponse::Ok().json(ShrikeStats { 14 | total_blocks: lock.total_blocks, 15 | total_transactions: lock.total_transactions, 16 | total_sysfee: lock.total_sysfee, 17 | total_transfers: lock.total_transfers, 18 | total_senders: lock.total_senders, 19 | total_contracts: lock.total_contracts, 20 | }) 21 | } 22 | 23 | #[get("/v1/stat/network-statistics")] 24 | async fn get_network_statistics() -> impl Responder { 25 | let lock: std::sync::RwLockReadGuard<'_, NetworkStatistics> = 26 | CURRENT_NETWORK_STATISTICS.read().unwrap(); 27 | 28 | HttpResponse::Ok().json(NetworkStatistics { 29 | total_transactions: lock.total_transactions, 30 | total_addresses: lock.total_addresses, 31 | total_contracts: lock.total_contracts, 32 | current_week_transactions: lock.current_week_transactions, 33 | current_week_addresses: lock.current_week_addresses, 34 | current_week_contracts: lock.current_week_contracts, 35 | }) 36 | } 37 | 38 | pub fn config(cfg: &mut web::ServiceConfig) { 39 | cfg.service(get_stats).service(get_network_statistics); 40 | } 41 | -------------------------------------------------------------------------------- /api/src/shared/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | pub const GAS_PRECISION: f64 = 100000000.0; 5 | pub const FUSDT_PRECISION: f64 = 1000000.0; 6 | 7 | pub type Hash160 = String; 8 | pub type Address = String; 9 | 10 | #[derive(Serialize, Deserialize, Clone)] 11 | pub struct Transaction { 12 | pub index: u64, 13 | pub hash: String, 14 | pub block_index: u64, 15 | pub vm_state: String, 16 | pub size: u32, 17 | pub version: u8, 18 | pub nonce: u64, 19 | pub sender: String, 20 | pub sysfee: String, 21 | pub netfee: String, 22 | pub valid_until: u64, 23 | pub signers: Value, 24 | pub script: String, 25 | pub witnesses: Value, 26 | pub stack_result: Value, 27 | pub notifications: Value, 28 | } 29 | 30 | #[derive(Serialize, Deserialize, Clone)] 31 | pub struct TransactionList { 32 | pub transactions: Vec, 33 | } 34 | 35 | #[derive(Serialize, Deserialize, Clone)] 36 | pub struct Transfer { 37 | pub contract: Hash160, 38 | pub from: Address, 39 | pub to: Address, 40 | pub amount: f64, 41 | } 42 | 43 | #[derive(Serialize, Deserialize, Clone)] 44 | pub struct TxData { 45 | pub txid: String, 46 | pub time: u64, // unix timestamp, extra call to set it until I modify the db to store block time for transactions 47 | pub sysfee: f64, 48 | pub netfee: f64, 49 | pub nep17_transfers: Vec, 50 | pub nep11_transfers: Vec, 51 | } 52 | 53 | #[derive(Serialize, Deserialize, Clone)] 54 | pub struct TxDataList { 55 | pub address: String, 56 | pub as_sender: Vec, 57 | pub as_participant: Vec, 58 | } 59 | 60 | #[derive(Serialize, Deserialize, Clone)] 61 | pub struct Event { 62 | pub contract: Hash160, 63 | pub eventname: String, 64 | pub state: serde_json::Value, 65 | } 66 | -------------------------------------------------------------------------------- /api/src/main.rs: -------------------------------------------------------------------------------- 1 | mod block; 2 | mod error; 3 | mod shared; 4 | mod stat; 5 | mod transaction; 6 | 7 | use actix_cors::Cors; 8 | use actix_web::{http::header, web, App, HttpServer}; 9 | use lib::db::DB_PATH; 10 | use r2d2::Pool; 11 | use r2d2_sqlite::SqliteConnectionManager; 12 | use rusqlite::OpenFlags; 13 | use tokio::{task, time}; 14 | 15 | use std::time::Duration; 16 | 17 | const REFRESH_INTERVAL: u64 = 3; // how often we check for a new block and refresh stats in seconds 18 | 19 | pub struct ConnectionPool { 20 | connection: Pool, 21 | } 22 | 23 | #[actix_web::main] 24 | async fn main() -> std::io::Result<()> { 25 | let db_path = DB_PATH 26 | .to_str() 27 | .expect("Failed to convert database path to str"); 28 | let manager = 29 | SqliteConnectionManager::file(db_path).with_flags(OpenFlags::SQLITE_OPEN_READ_ONLY); 30 | let pool = Pool::new(manager).unwrap(); 31 | 32 | let connection_pool = web::Data::new(ConnectionPool { connection: pool }); 33 | 34 | let internal_connection = connection_pool.clone(); 35 | 36 | task::spawn(async move { 37 | let mut interval = time::interval(Duration::from_secs(REFRESH_INTERVAL)); 38 | loop { 39 | let c = internal_connection.clone(); 40 | interval.tick().await; 41 | stat::internals::set_stats_internal(c).await; 42 | } 43 | }); 44 | 45 | println!("Opening to requests on http://0.0.0.0:8080."); 46 | 47 | HttpServer::new(move || { 48 | let cors = Cors::default() 49 | .allow_any_origin() 50 | .allowed_methods(vec!["GET"]) 51 | .allowed_header(header::CONTENT_TYPE) 52 | .max_age(3600); 53 | App::new() 54 | .wrap(cors) 55 | .app_data(connection_pool.clone()) 56 | .configure(block::controller::config) 57 | .configure(transaction::controller::config) 58 | .configure(stat::controller::config) 59 | }) 60 | .bind(("0.0.0.0", 8080))? 61 | .run() 62 | .await 63 | } 64 | -------------------------------------------------------------------------------- /gui/README.md: -------------------------------------------------------------------------------- 1 | # GUI 2 | 3 | The GUI is a user-friendly web interface built with SolidJS (SolidStart) and PicoCSS. It provides a simple way for users to interact with the data provided by Shrike. 4 | 5 | ## Features 6 | 7 | - User-friendly interface for interacting with Shrike data. 8 | - Built with SolidJS and PicoCSS for a lightweight and efficient user experience. 9 | - Suitable for both regular and power users. 10 | 11 | ## Getting Started 12 | 13 | ### Prerequisites 14 | 15 | - Node.js and npm installed on your machine. You can download them from [here](https://nodejs.org/en/download/). 16 | - A running instance of the Shrike API. 17 | 18 | ### Quickstart 19 | 20 | 1. Clone or download the GUI folder. 21 | 2. Update the path in `/constants/index.js` to point to your running Shrike API instance. 22 | 3. Install the dependencies by running `npm install`. 23 | 4. Serve the GUI locally using `npm run dev`. You can then open it in your browser at `http://127.0.0.1:5173/`. 24 | 25 | ## Contributing 26 | 27 | Contributions to the GUI are welcomed. If you have suggestions for improvements or additional features, feel free to open an issue or submit a pull request. 28 | 29 | ----- 30 | 31 | ## SolidStart 32 | 33 | Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); 34 | 35 | ### Creating a project 36 | 37 | ```bash 38 | # create a new project in the current directory 39 | npm init solid@latest 40 | 41 | # create a new project in my-app 42 | npm init solid@latest my-app 43 | ``` 44 | 45 | ### Developing 46 | 47 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 48 | 49 | ```bash 50 | npm run dev 51 | 52 | # or start the server and open the app in a new browser tab 53 | npm run dev -- --open 54 | ``` 55 | 56 | ### Building 57 | 58 | Solid apps are built with _adapters_, which optimise your project for deployment to different environments. 59 | 60 | By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different adapter, add it to the `devDependencies` in `package.json` and specify in your `vite.config.js`. 61 | -------------------------------------------------------------------------------- /indexer/src/spawn/sync.rs: -------------------------------------------------------------------------------- 1 | use tokio::fs::File; 2 | use tokio::io::{AsyncBufReadExt, BufReader, Lines}; 3 | use tokio::process::Command; 4 | use tokio::sync::oneshot; 5 | use tokio::task::JoinHandle; 6 | 7 | use log::warn; 8 | use crate::utils::{logger, node}; 9 | use regex::Regex; 10 | use std::path::Path; 11 | 12 | pub async fn run_node( 13 | max_height: u64, 14 | ) -> Result< 15 | ( 16 | Lines>, 17 | JoinHandle<()>, 18 | oneshot::Sender<()>, 19 | ), 20 | anyhow::Error, 21 | > { 22 | let re = Regex::new(r#""headerHeight": (\d+),"#).unwrap(); 23 | let log_path = Path::new("./log/neogo.log"); 24 | 25 | // Start the node process 26 | let mut cmd = Command::new(node::NEOGO_PATH); 27 | let mut node = cmd 28 | .args(["node", "-m"]) 29 | .spawn() 30 | .expect("Failed to run node"); 31 | 32 | // Wait for log file to be created 33 | while !log_path.exists() { 34 | tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; 35 | } 36 | 37 | // Open and read the log file 38 | let file = File::open(log_path).await?; 39 | let mut reader = BufReader::new(file).lines(); 40 | 41 | while let Some(line) = reader.next_line().await.unwrap_or_default() { 42 | if line.contains("headerHeight") { 43 | if let Some(caps) = re.captures(&line) { 44 | let height = caps.get(1).unwrap().as_str().parse::().unwrap(); 45 | logger::inline_print(&format!("\rCurrent height: {height}")); 46 | 47 | if max_height != 0 && height >= max_height { 48 | warn!("Exceeded target height."); 49 | break; 50 | } 51 | } 52 | } else { 53 | // println!("{}", line); // for debugging 54 | } 55 | 56 | if line.contains("synchronized") { 57 | println!(); 58 | break; 59 | } 60 | } 61 | 62 | let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); 63 | 64 | let handle = { 65 | tokio::spawn(async move { 66 | let _ = shutdown_rx.await; 67 | warn!("Shutdown signal received."); 68 | let _ = node.kill().await; 69 | warn!("Node killed."); 70 | }) 71 | }; 72 | 73 | Ok((reader, handle, shutdown_tx)) 74 | } 75 | -------------------------------------------------------------------------------- /api/src/shared/checker.rs: -------------------------------------------------------------------------------- 1 | use lib::neo::ALPHABET; 2 | 3 | pub fn is_neo_address(string: &str) -> bool { 4 | string.chars().count() == 34 5 | && string.starts_with('N') 6 | && string.chars().all(|c| ALPHABET.contains(&(c as u8))) 7 | } 8 | 9 | #[allow(dead_code)] 10 | pub fn is_neo_script_hash(string: &str) -> bool { 11 | string.chars().count() == 42 12 | && string.starts_with("0x") 13 | && string 14 | .chars() 15 | .skip(2) 16 | .take(40) 17 | .all(|c| c.is_ascii_hexdigit()) 18 | } 19 | 20 | pub fn is_neo_txid_hash(string: &str) -> bool { 21 | string.chars().count() == 66 22 | && string.starts_with("0x") 23 | && string 24 | .chars() 25 | .skip(2) 26 | .take(64) 27 | .all(|c| c.is_ascii_hexdigit()) 28 | } 29 | 30 | #[test] 31 | fn test_is_neo_address() { 32 | assert!(is_neo_address("NSTSntFPK36QXsjEK6oAhnPzSyfgfVA2GQ")); 33 | assert!(!is_neo_address("NSTSntFPK36QXsjEK6oAhnPzSyfgfVA2GQ1")); 34 | assert!(!is_neo_address("NSTSntFPK36QXsjEK6oAhnPzSyfgfVA2G")); 35 | assert!(!is_neo_address("NSTSntFPK36QXsjEK6OAhnPzSyfgfVA2GQ")); 36 | } 37 | 38 | #[test] 39 | fn test_is_neo_script_hash() { 40 | assert!(is_neo_script_hash( 41 | "0x6250481ec87ae2052f90ec7cb46d757b8db1c447" 42 | )); 43 | assert!(!is_neo_script_hash( 44 | "0x6250481ec/7ae2052f90ec7cb46#757b8db1c447" 45 | )); 46 | assert!(!is_neo_script_hash("NSTSntFPK36QXsjEK6oAhnPzSyfgfVA2GQ")); 47 | assert!(!is_neo_script_hash( 48 | "0000000000000000000000000000000000000000" 49 | )); 50 | assert!(!is_neo_script_hash( 51 | "0x3184522d88fe2a5e0ac35324d119e6e82b3df7d7c95c927c247155467d49e8ba" 52 | )); 53 | } 54 | 55 | #[test] 56 | fn test_is_neo_txid_hash() { 57 | assert!(is_neo_txid_hash( 58 | "0x3184522d88fe2a5e0ac35324d119e6e82b3df7d7c95c927c247155467d49e8ba" 59 | )); 60 | assert!(!is_neo_txid_hash( 61 | "0x31*4522d88fe2a5e0ac35324d119e6e82b3df7d7c95@927c247155467d49e8ba" 62 | )); 63 | assert!(!is_neo_txid_hash("NSTSntFPK36QXsjEK6oAhnPzSyfgfVA2GQ")); 64 | assert!(!is_neo_txid_hash( 65 | "0x00000000000000000000000000000000000000000" 66 | )); 67 | assert!(!is_neo_txid_hash( 68 | "0x6250481ec87ae2052f90ec7cb46d757b8db1c447" 69 | )); 70 | } 71 | -------------------------------------------------------------------------------- /api/src/transaction/controller.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, HttpResponse, Responder}; 2 | 3 | use crate::error::Error; 4 | use crate::shared::checker; 5 | use crate::ConnectionPool; 6 | 7 | use super::internals; 8 | 9 | #[get("/v1/transaction/{hash}")] 10 | async fn get_transaction( 11 | pool: web::Data, 12 | path: web::Path, 13 | ) -> impl Responder { 14 | let conn = &pool.connection.get().unwrap(); 15 | let hash = path.into_inner(); 16 | 17 | if !checker::is_neo_txid_hash(&hash) { 18 | return HttpResponse::Ok().json(Error { 19 | error: "Invalid transaction hash.".to_string(), 20 | }); 21 | } 22 | 23 | let transaction = internals::get_transaction_internal(conn, hash); 24 | 25 | match transaction { 26 | Ok(tx) => HttpResponse::Ok().json(tx), 27 | Err(err) => HttpResponse::Ok().json(err), 28 | } 29 | } 30 | 31 | #[get("/v1/transaction/sender/{address}")] 32 | async fn get_sender_transactions( 33 | pool: web::Data, 34 | path: web::Path, 35 | ) -> impl Responder { 36 | let conn = &pool.connection.get().unwrap(); 37 | let address = path.into_inner(); 38 | 39 | if !checker::is_neo_address(&address) { 40 | return HttpResponse::Ok().json(Error { 41 | error: "Invalid address.".to_string(), 42 | }); 43 | } 44 | 45 | let transactions = internals::get_sender_transactions_internal(conn, address); 46 | 47 | match transactions { 48 | Ok(txs) => HttpResponse::Ok().json(txs), 49 | Err(err) => HttpResponse::Ok().json(err), 50 | } 51 | } 52 | 53 | #[get("/v1/transaction/transfers/{address}")] 54 | async fn get_address_transfers( 55 | pool: web::Data, 56 | path: web::Path, 57 | ) -> impl Responder { 58 | let conn = &pool.connection.get().unwrap(); 59 | let address = path.into_inner(); 60 | 61 | if !checker::is_neo_address(&address) { 62 | return HttpResponse::Ok().json(Error { 63 | error: "Invalid address.".to_string(), 64 | }); 65 | } 66 | 67 | let transfer_list = internals::get_address_transfers_internal(conn, address); 68 | 69 | match transfer_list { 70 | Ok(txs) => HttpResponse::Ok().json(txs), 71 | Err(err) => HttpResponse::Ok().json(err), 72 | } 73 | } 74 | 75 | pub fn config(cfg: &mut web::ServiceConfig) { 76 | cfg.service(get_transaction) 77 | .service(get_sender_transactions) 78 | .service(get_address_transfers); 79 | } 80 | -------------------------------------------------------------------------------- /gui/src/helpers/fetcher.js: -------------------------------------------------------------------------------- 1 | import { Formatter } from "./formatter"; 2 | import { Checker } from "./checker"; 3 | import { API_PATH } from "../constants/index.js" 4 | 5 | export class Fetcher { 6 | static async block() { 7 | let value = document.getElementById("getblock").value 8 | 9 | if (!Checker.isNeoTxidHash(value) && !Checker.isReasonableNumber(value)) { 10 | document.getElementById("getblock").ariaInvalid = "true" 11 | await new Promise(r => setTimeout(r, 2000)); 12 | document.getElementById("getblock").ariaInvalid = "" 13 | return false 14 | } 15 | 16 | let res = await fetch(`${API_PATH}/block/${value}`) 17 | let block = await res.json() 18 | return Formatter.flattenObject(block) 19 | } 20 | 21 | static async transaction() { 22 | let value = document.getElementById("gettransaction").value 23 | 24 | if (!Checker.isNeoTxidHash(value)) { 25 | document.getElementById("gettransaction").ariaInvalid = "true" 26 | await new Promise(r => setTimeout(r, 2000)); 27 | document.getElementById("gettransaction").ariaInvalid = "" 28 | return false 29 | } 30 | 31 | let res = await fetch(`${API_PATH}/transaction/${value}`) 32 | let tx = await res.json() 33 | return Formatter.flattenObject(tx) 34 | } 35 | 36 | static async blockTransactions() { 37 | let value = document.getElementById("getblocktransactions").value 38 | 39 | if (!Checker.isNeoTxidHash(value)) { 40 | document.getElementById("getblocktransactions").ariaInvalid = "true" 41 | await new Promise(r => setTimeout(r, 2000)); 42 | document.getElementById("getblocktransactions").ariaInvalid = "" 43 | return false 44 | } 45 | 46 | let res = await fetch(`${API_PATH}/block/${value}/transactions`) 47 | let txList = await res.json() 48 | return Formatter.flattenObject(txList) 49 | } 50 | 51 | static async addressTransfers() { 52 | let value = document.getElementById("getaddresstransfers").value 53 | 54 | if (!Checker.isNeoAddress(value)) { 55 | document.getElementById("getaddresstransfers").ariaInvalid = "true" 56 | await new Promise(r => setTimeout(r, 2000)); 57 | document.getElementById("getaddresstransfers").ariaInvalid = "" 58 | return false 59 | } 60 | 61 | let res = await fetch(`${API_PATH}/transaction/transfers/${value}`) 62 | return await res.json() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /indexer/src/db/model.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct Transaction { 3 | pub hash: String, 4 | pub block_index: u64, 5 | pub vm_state: String, 6 | pub size: u32, 7 | pub version: u8, 8 | pub nonce: u64, 9 | pub sender: String, 10 | pub sysfee: String, 11 | pub netfee: String, 12 | pub valid_until: u64, 13 | pub signers: String, 14 | pub script: String, 15 | pub witnesses: String, 16 | pub stack_result: String, 17 | pub notifications: String, 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct Block { 22 | pub hash: String, 23 | pub size: u32, 24 | pub version: u8, 25 | pub merkle_root: String, 26 | pub time: u64, 27 | pub nonce: String, 28 | pub speaker: u8, 29 | pub next_consensus: String, 30 | pub reward: f64, 31 | pub reward_receiver: String, 32 | pub witnesses: String, 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct Address { 37 | pub block_index: u64, 38 | pub address: String, 39 | pub balances: String, 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct Contract { 44 | pub block_index: u64, 45 | pub hash: String, 46 | pub contract_type: String, 47 | } 48 | 49 | impl Block { 50 | pub fn genesis_block() -> Block { 51 | Block { 52 | hash: String::from("0x1f4d1defa46faa5e7b9b8d3f79a06bec777d7c26c4aa5f6f5899a291daa87c15"), 53 | size: 114, 54 | version: 0, 55 | merkle_root: String::from("0x0000000000000000000000000000000000000000000000000000000000000000"), 56 | time: 1_468_595_301_000, 57 | nonce: String::from("000000007C2BAC1D"), 58 | speaker: 0, 59 | next_consensus: String::from("NSiVJYZej4XsxG5CUpdwn7VRQk8iiiDMPM"), 60 | reward: 0.5, 61 | reward_receiver: String::from("NZeAarn3UMCqNsTymTMF2Pn6X7Yw3GhqDv"), 62 | witnesses: r#"[{"invocation":"DEAq7W/jUhpMon1t9muqXKfBvNyGwLfFFM1vAxrMKvUl6MqK+LL/lJAJP9uAk/cberIWWhSsdcxUtltkBLemg/VuDECQZGuvP93JlZga2ml8cnbe5cNiGgO0EMrbGYyzvgr8calP5SwMNPSYms10gIHxlsuXDU++EQpZu/vKxfHoxdC5DEDgsA3POVZdfN+i5+ekvtsaIvif42n0GC+dZi3Rp37ETmt4NtkoK2I2UXi+WIjm5yXLJsPhAvEV6cJSrvqBdsQBDEDTS6NU+kB+tgeBe9lWv+6y0L2qcUBIaUxiTCaNWZtLPghQICBvjDz1/9ttJRXG3I5N9CFDjjLKCpdIY842HW4/DEC+wlWjkCzVqzKslvpCKZbEPUGIf87CFAD88xqzl26m/TpTUcT0+D5oI2bVzAk0mcdBTPnyjcNbv17BFmr63+09","verification":"FQwhAkhv0VcCxEkKJnAxEqXMHQkj/Wl6M0Br1aHADgATsJpwDCECTHt/tsMQ/M8bozsIJRnYKWTqk4aNZ2Zi1KWa1UjfDn0MIQKq7DhHD2qtAELG6HfP2Ah9Jnaw9Rb93TYoAbm9OTY5ngwhA7IJ/U9TpxcOpERODLCmu2pTwr0BaSaYnPhfmw+6F6cMDCEDuNnVdx2PUTqghpucyNUJhkA7eMbaNokGOMPUalrc4EoMIQLKDidpe5wkj28W4IX9AGHib0TahbWO6DXBEMql7DulVAwhAt9I9g6PPgHEj/QLm38TENeosqGTGIvv4cLj33QOiVCTF0Ge0Nw6"}]"#.to_string() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gui/src/helpers/formatter.js: -------------------------------------------------------------------------------- 1 | export class Formatter { 2 | 3 | static formatJson(json) { 4 | return JSON.stringify(json, null, 0) 5 | } 6 | 7 | static flattenObject(obj) { 8 | let arr = Object.entries(obj) 9 | 10 | return arr.map(el => { 11 | if ( 12 | el[0] === "witnesses" || 13 | el[0] === "signers" || 14 | el[0] === "stack_result" || 15 | el[0] === "notifications" || 16 | el[0] === "transactions" 17 | ) { 18 | return [el[0], Formatter.formatJson(el[1])] 19 | } else { 20 | return [el[0], el[1]] 21 | } 22 | }) 23 | } 24 | 25 | static getAddresses(transferList) { 26 | let addresses = [] 27 | for (const transfer of transferList) { 28 | for (const nep17 of transfer.nep17_transfers) { 29 | 30 | if (!addresses.includes(nep17.from.slice(0,5))) { 31 | addresses.push(nep17.from.slice(0,5)) 32 | } 33 | 34 | if (!addresses.includes(nep17.to.slice(0,5))) { 35 | addresses.push(nep17.to.slice(0,5)) 36 | } 37 | } 38 | } 39 | return addresses 40 | } 41 | 42 | static getTransfers(transferList) { 43 | let transfers = [] 44 | for (const transfer of transferList) { 45 | for (const nep17 of transfer.nep17_transfers) { 46 | transfers.push({to: nep17.to.slice(0, 5), from: nep17.from.slice(0, 5), amount: nep17.amount, asset: nep17.contract}) 47 | } 48 | } 49 | return transfers 50 | } 51 | 52 | static parseIfHsl(cssString) { 53 | if (cssString.includes("hsl")) { 54 | return Formatter.hslToHex(cssString) 55 | } else { 56 | return cssString 57 | } 58 | } 59 | 60 | static parseHsl(rawHsl) { 61 | let hslArr = rawHsl.split("(")[1].split(")")[0].split(",") 62 | let h = Number(hslArr[0].split("deg")[0].trim()) 63 | let s = Number(hslArr[1].split("%")[0].trim()) 64 | let l = Number(hslArr[2].split("%")[0].trim()) 65 | return [h, s, l] 66 | } 67 | 68 | static hslToHex(rawHsl) { 69 | let hsl = Formatter.parseHsl(rawHsl) 70 | let h = hsl[0] 71 | let s = hsl[1] 72 | let l = hsl[2] 73 | 74 | l /= 100 75 | const a = s * Math.min(l, 1 - l) / 100 76 | const f = n => { 77 | const k = (n + h / 30) % 12 78 | const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1) 79 | return Math.round(255 * color).toString(16).padStart(2, '0') 80 | } 81 | return `#${f(0)}${f(8)}${f(4)}` 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /indexer/src/rpc/client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use reqwest::Client as ReqwestClient; 3 | 4 | use crate::config::AppConfig; 5 | 6 | use super::method::{GetApplicationLog, GetBlock, GetBlockCount, RpcMethod}; 7 | use super::models::{ 8 | BlockAppLogResult, BlockResult, RpcRequest, RpcResponse, TransactionAppLogResult, 9 | TransactionResult, 10 | }; 11 | 12 | pub struct Client { 13 | client: ReqwestClient, 14 | base_url: String, 15 | } 16 | 17 | impl Client { 18 | pub fn new(config: &AppConfig) -> Self { 19 | Self { 20 | client: ReqwestClient::new(), 21 | base_url: config.node_path.clone(), 22 | } 23 | } 24 | 25 | pub async fn send_request( 26 | &self, 27 | method: T, 28 | ) -> Result { 29 | let request_body = RpcRequest { 30 | jsonrpc: "2.0".to_string(), 31 | id: 1, 32 | method: method.method_name().to_string(), 33 | params: method.params(), 34 | }; 35 | let response: RpcResponse = self 36 | .client 37 | .post(&self.base_url) 38 | .json(&request_body) 39 | .send() 40 | .await? 41 | .json() 42 | .await?; 43 | 44 | Ok(response.result) 45 | } 46 | 47 | pub async fn get_current_height(&self) -> Result { 48 | let response = self.send_request(GetBlockCount).await?; 49 | Ok(response) 50 | } 51 | 52 | pub async fn get_block(&self, height: u64) -> Result { 53 | let response = self 54 | .send_request(GetBlock { 55 | block_height: height, 56 | verbosity: 1, 57 | }) 58 | .await?; 59 | Ok(response) 60 | } 61 | 62 | pub async fn get_application_log( 63 | &self, 64 | hash: &str, 65 | ) -> Result { 66 | let app_log = self 67 | .send_request(GetApplicationLog { 68 | hash: hash.to_string(), 69 | }) 70 | .await?; 71 | Ok(app_log) 72 | } 73 | 74 | pub async fn fetch_full_block(&self, height: u64) -> Result<(BlockResult, BlockAppLogResult)> { 75 | let block = self.get_block(height).await?; 76 | let block_app_log: BlockAppLogResult = self.get_application_log(&block.hash).await?; 77 | 78 | Ok((block, block_app_log)) 79 | } 80 | 81 | pub async fn fetch_full_transaction( 82 | &self, 83 | tx: TransactionResult, 84 | ) -> Result<(TransactionResult, TransactionAppLogResult)> { 85 | let tx_app_log: TransactionAppLogResult = self.get_application_log(&tx.hash).await?; 86 | 87 | Ok((tx, tx_app_log)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /api/src/shared/events.rs: -------------------------------------------------------------------------------- 1 | use crate::shared::models::{Transaction, Transfer, TxData}; 2 | 3 | use lib::neo; 4 | 5 | use super::models::{FUSDT_PRECISION, GAS_PRECISION}; 6 | 7 | // now supports inbound and outbound (dictated by sender field and from/to, depending on requirements) 8 | // still needs work to support all contract decimals properly 9 | // also it may return tons of pointless transfer data for airdrops that include the address 10 | // not sure what to do about that right now, as we might not want to fully discount transfers 11 | // that do not have the specified address as from/to/sender (e.g. internal transfers on DEX swaps) 12 | pub fn get_transfer_events(tx: Transaction) -> TxData { 13 | let mut transfers = Vec::new(); 14 | let notifications = tx.notifications.as_array().unwrap(); 15 | 16 | for notification in notifications { 17 | let state = notification["state"].clone(); 18 | 19 | if notification["eventname"] == "Transfer" 20 | && state["type"] == "Array" 21 | && state["value"][0]["type"] == "ByteString" 22 | || state["value"][0]["type"] == "Any" && state["value"][1]["type"] == "ByteString" 23 | || state["value"][1]["type"] == "Any" && state["value"][2]["type"] == "Integer" 24 | { 25 | let contract = notification["contract"].as_str().unwrap().to_string(); 26 | 27 | let from = if state["value"][0]["value"].is_string() { 28 | neo::base64_to_address(state["value"][0]["value"].as_str().unwrap()) 29 | } else { 30 | "null".to_string() 31 | }; 32 | 33 | let to = if state["value"][1]["value"].is_string() { 34 | neo::base64_to_address(state["value"][1]["value"].as_str().unwrap()) 35 | } else { 36 | "null".to_string() 37 | }; 38 | 39 | let qty = state["value"][2]["value"].as_str().unwrap(); 40 | 41 | let amount = match qty.parse::() { 42 | Ok(v) => { 43 | if contract == "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5" { 44 | v 45 | } else if contract == "0xcd48b160c1bbc9d74997b803b9a7ad50a4bef020" { 46 | v / FUSDT_PRECISION 47 | } else { 48 | v / GAS_PRECISION 49 | } 50 | } 51 | Err(_) => continue, 52 | }; 53 | 54 | let transfer = Transfer { 55 | contract, 56 | from, 57 | to, 58 | amount, // this will break on non-8 decimal contracts, will need contract table 59 | }; 60 | 61 | transfers.push(transfer); 62 | } 63 | } 64 | 65 | TxData { 66 | txid: tx.hash, 67 | time: 0, 68 | sysfee: tx.sysfee.parse::().unwrap() / GAS_PRECISION, 69 | netfee: tx.netfee.parse::().unwrap() / GAS_PRECISION, 70 | nep17_transfers: transfers, 71 | nep11_transfers: Vec::new(), 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gui/src/components/Visualizer.jsx: -------------------------------------------------------------------------------- 1 | import { onMount, createSignal } from "solid-js" 2 | import { A, useLocation } from "solid-start" 3 | import { Grapher } from "../helpers/grapher.js" 4 | import { Formatter } from "../helpers/formatter.js" 5 | 6 | const loadGraph = (nodes, edges) => { 7 | Grapher.draw(nodes, edges) 8 | } 9 | 10 | export default function Visualizer() { 11 | 12 | const result = useLocation().state 13 | const [address, setAddress] = createSignal(result.address) 14 | const [activeGraph, setActiveGraph] = createSignal("Sender") 15 | const [senderData, setSenderData] = createSignal([]) 16 | const [participantData, setParticipantData] = createSignal([]) 17 | 18 | if (typeof result === "undefined" || result === null) { 19 | return ( 20 |
21 |

No address loaded for visualization.

22 | Back 23 |
24 | ) 25 | } else { 26 | 27 | const senderNodes = Grapher.getNodesFromAddresses(Formatter.getAddresses(result.as_sender)) 28 | const senderEdges = Grapher.getEdgesFromTransfers(Formatter.getTransfers(result.as_sender)) 29 | setSenderData([senderNodes, senderEdges]) 30 | 31 | const participantNodes = Grapher.getNodesFromAddresses(Formatter.getAddresses(result.as_participant)) 32 | const participantEdges = Grapher.getEdgesFromTransfers(Formatter.getTransfers(result.as_participant)) 33 | setParticipantData([participantNodes, participantEdges]) 34 | 35 | onMount(() => { 36 | loadGraph(senderData()[0], senderData()[1]) 37 | }) 38 | } 39 | 40 | return ( 41 |
42 |
43 |
44 |

Visualizer

45 |
46 |
{activeGraph()}
47 |
{address()}
48 |
49 |
50 |
51 |
52 |
53 | 70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /gui/src/root.jsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { Suspense, createSignal } from "solid-js" 3 | import { 4 | A, 5 | Body, 6 | ErrorBoundary, 7 | FileRoutes, 8 | Head, 9 | Html, 10 | Meta, 11 | Routes, 12 | Scripts, 13 | Title 14 | } from "solid-start" 15 | 16 | import About from "./components/About" 17 | 18 | import "./pico.min.css" 19 | export default function Root() { 20 | 21 | const moon = 22 | const sun = 23 | 24 | const [theme, setTheme] = createSignal("dark") 25 | const [themeIcon, setThemeIcon] = createSignal(sun) 26 | 27 | const toggleDarkMode = () => { 28 | toggleTheme() 29 | toggleThemeIcon() 30 | } 31 | 32 | const toggleTheme = () => { theme() === "dark" ? setTheme("light") : setTheme("dark") } 33 | const toggleThemeIcon = () => { themeIcon() === moon ? setThemeIcon(sun) : setThemeIcon(moon) } 34 | 35 | return ( 36 | 37 | 38 | Shrike 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 |

Shrike

50 |

A data analysis tool for Neo

51 |
52 | 58 |
59 | 60 | 61 | 62 |
63 |
64 |
65 | 66 | 67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /gui/src/components/Query.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "solid-start" 2 | import { Fetcher } from "../helpers/fetcher.js" 3 | 4 | export default function Query() { 5 | 6 | const navigate = useNavigate() 7 | 8 | return ( 9 |
10 |

Query

11 |
{ 12 | e.preventDefault(); 13 | let result = await Fetcher.block(); 14 | if (result) { 15 | navigate("/result", { state: result }) 16 | } 17 | }}> 18 | 29 |
30 | 31 |
{ 32 | e.preventDefault(); 33 | let result = await Fetcher.transaction(); 34 | if (result) { 35 | navigate("/result", { state: result }) 36 | } 37 | }}> 38 | 49 |
50 | 51 |
{ 52 | e.preventDefault(); 53 | let result = await Fetcher.blockTransactions(); 54 | if (result) { 55 | navigate("/result", { state: result }) 56 | } 57 | }}> 58 | 69 |
70 | 71 |
{ 72 | e.preventDefault(); 73 | let result = await Fetcher.addressTransfers(); 74 | if (result) { 75 | navigate("/visualizer", { state: result }) 76 | } 77 | }}> 78 | 89 |
90 |
91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /indexer/src/rpc/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize, Serializer}; 2 | 3 | #[derive(Deserialize, Debug)] 4 | pub enum NeoParam { 5 | String(String), 6 | Integer(u64), 7 | } 8 | 9 | impl Serialize for NeoParam { 10 | fn serialize(&self, serializer: S) -> Result 11 | where 12 | S: Serializer, 13 | { 14 | match *self { 15 | NeoParam::String(ref value) => serializer.serialize_str(value), 16 | NeoParam::Integer(value) => serializer.serialize_u64(value), 17 | } 18 | } 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Debug)] 22 | pub struct RpcRequest { 23 | pub jsonrpc: String, 24 | pub method: String, 25 | pub params: Vec, 26 | pub id: u32, 27 | } 28 | 29 | #[derive(Deserialize, Debug)] 30 | pub struct RpcResponse { 31 | pub jsonrpc: String, 32 | pub id: u32, 33 | pub result: T, 34 | } 35 | 36 | #[derive(Deserialize, Debug, Clone)] 37 | pub struct BlockResult { 38 | pub hash: String, 39 | pub size: u32, 40 | pub version: u8, 41 | pub merkleroot: String, 42 | pub time: u64, 43 | pub nonce: String, 44 | pub index: u64, 45 | pub primary: u8, 46 | pub nextconsensus: String, 47 | pub witnesses: Vec, 48 | pub tx: Vec, 49 | } 50 | 51 | #[derive(Deserialize, Debug, Clone)] 52 | pub struct TransactionResult { 53 | pub hash: String, 54 | pub blockhash: Option, 55 | pub size: u32, 56 | pub version: u8, 57 | pub nonce: u64, 58 | pub sender: String, 59 | pub sysfee: String, 60 | pub netfee: String, 61 | pub validuntilblock: u64, 62 | pub signers: Vec, 63 | pub script: String, 64 | pub witnesses: Vec, 65 | } 66 | 67 | #[derive(Deserialize, Debug, Clone)] 68 | pub enum AppLogResult { 69 | BlockAppLogResult(BlockAppLogResult), 70 | TransactionAppLogResult(TransactionAppLogResult), 71 | } 72 | 73 | #[derive(Deserialize, Debug, Clone)] 74 | pub struct BlockAppLogResult { 75 | pub blockhash: String, 76 | pub executions: Vec, 77 | } 78 | 79 | #[derive(Deserialize, Debug, Clone)] 80 | pub struct TransactionAppLogResult { 81 | pub txid: String, 82 | pub executions: Vec, 83 | } 84 | 85 | #[derive(Serialize, Deserialize, Debug, Clone)] 86 | pub struct Execution { 87 | pub trigger: String, 88 | pub vmstate: String, 89 | pub exception: Option, 90 | pub gasconsumed: String, 91 | pub stack: Vec, 92 | pub notifications: Vec, 93 | } 94 | 95 | #[derive(Serialize, Deserialize, Debug, Clone)] 96 | pub struct Notification { 97 | pub contract: String, 98 | pub eventname: String, 99 | pub state: State, 100 | } 101 | 102 | #[derive(Serialize, Deserialize, Debug, Clone)] 103 | pub struct State { 104 | #[serde(rename = "type")] 105 | pub _type: String, 106 | pub value: Vec, 107 | } 108 | 109 | #[derive(Serialize, Deserialize, Debug, Clone)] 110 | pub struct StateValue { 111 | #[serde(rename = "type")] 112 | pub _type: String, 113 | pub value: Option, 114 | } 115 | 116 | #[derive(Serialize, Deserialize, Debug, Clone)] 117 | pub struct Witness { 118 | pub invocation: String, 119 | pub verification: String, 120 | } 121 | 122 | #[derive(Serialize, Deserialize, Debug, Clone)] 123 | pub struct Signer { 124 | pub account: String, 125 | pub scopes: String, 126 | pub allowedcontracts: Option>, 127 | } 128 | -------------------------------------------------------------------------------- /indexer/README.md: -------------------------------------------------------------------------------- 1 | # Indexer 2 | 3 | The Indexer is a key component of Shrike. It uses a local node to process Neo N3 blocks and transactions into a more easily queried format. 4 | 5 | 6 | ``` 7 | PS > cargo run --release 8 | Finished release [optimized] target(s) in 1.85s 9 | Running `..\shrike\target\release\indexer.exe` 10 | [INFO indexer::db::database] Using database at ..\AppData\Local\Shrike\data\shrike.db3. 11 | [INFO indexer] Welcome to Shrike! 12 | [INFO indexer] Checking for NeoGo.. 13 | [INFO indexer::db::database] WAL mode already active. 14 | [INFO indexer] Last stored block index: 4615157 15 | [INFO indexer] Starting node sync.. 16 | 17 | _ ____________ __________ 18 | / | / / ____/ __ \ / ____/ __ \ 19 | / |/ / __/ / / / /_____/ / __/ / / / 20 | / /| / /___/ /_/ /_____/ /_/ / /_/ / 21 | /_/ |_/_____/\____/ \____/\____/ 22 | 23 | /NEO-GO:0.105.0/ 24 | 25 | Current height: 4677439 26 | [INFO indexer] Sync completed in 95482 ms. 27 | [INFO indexer::spawn::indexer] Chain height is 4677745. 28 | [INFO indexer::spawn::indexer] Started indexing. 29 | [INFO indexer::spawn::indexer] Start height is 4615158. 62587 blocks to process. 30 | [INFO indexer::spawn::indexer] Updating tables: 31 | Indexed 62587 block(s). 32 | [INFO indexer::spawn::indexer] Indexing completed in 41335 ms. 33 | [INFO indexer::spawn::indexer] New stored height is 4677744. 34 | [WARN indexer::spawn::sync] Shutdown signal received. 35 | [WARN indexer::spawn::sync] Node killed. 36 | ``` 37 | 38 | ## Features 39 | 40 | - Synchronizes a NeoGo instance. 41 | - Fetches block, transaction, and application log data. 42 | - Processes and stores chain data in SQLite tables. 43 | 44 | ## Getting Started 45 | 46 | ### Prerequisites 47 | 48 | - Latest stable Rust version. We recommend using [Rustup](https://rustup.rs/). 49 | - SQLite-compatible database browser or query editor for data analysis. We recommend [DB Browser](https://sqlitebrowser.org/) for simplicity or [DBeaver](https://dbeaver.io/) for advanced use cases. 50 | 51 | ### Quickstart 52 | 53 | 1. Visit the [releases](#) page and download the appropriate executable for your platform (e.g., `indexer.exe` for `Windows`). 54 | 2. Download the [NeoGo config file](https://github.com/EdgeDLT/shrike/blob/main/indexer/config/protocol.mainnet.yml) and place it in a `config` directory located next to the executable. 55 | 3. Run the executable and follow the prompt to download NeoGo if you haven't already. 56 | 4. Wait for sync & index completion. 57 | 58 | ### Build Instructions 59 | 60 | 1. Clone or download the Indexer folder. 61 | 2. Open the root directory in a terminal and run `cargo run --release` to build and execute the Indexer. 62 | 3. Follow the prompt to download NeoGo if you haven't already. 63 | 4. Allow some time for the process to complete. 64 | 65 | ## Database 66 | 67 | The Shrike database consists of two tables: `blocks` and `transactions`. They are modeled to closely resemble their typical NeoRPC forms, with some adjustments for SQL and incorporating relevant parts of their respective `application logs`. 68 | 69 | ### Database Location 70 | 71 | - On Windows: `C:\\Users\\AppData\Local\Shrike\data\shrike.db3` 72 | - On Linux: `/home//.local/share/Shrike/shrike.db3` 73 | - On MacOS: `/Users//Library/Application Support/Shrike/shrike.db3` 74 | 75 | ### Storage Requirements 76 | 77 | Using the Indexer requires a significant amount of storage space, slightly more than syncing a node alone. As of block height 4408282, As of now, the chain folder is 39.1GB, and the Shrike DB is 12.2GB. Estimate the required headroom to account for future blockchain growth based on your use case. 78 | 79 | ## Contributing 80 | 81 | As the project is in early development, schema changes may occur from time to time when there is strong justification for it. Additional tables (such as `contracts` or `balances`) are also possible, feel free to submit a PR if you want to expedite such a process. -------------------------------------------------------------------------------- /indexer/config/protocol.mainnet.yml: -------------------------------------------------------------------------------- 1 | ProtocolConfiguration: 2 | Magic: 860833102 3 | MaxTraceableBlocks: 2102400 4 | InitialGASSupply: 52000000 5 | TimePerBlock: 15s 6 | MemPoolSize: 50000 7 | StandbyCommittee: 8 | - 03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c 9 | - 02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093 10 | - 03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a 11 | - 02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554 12 | - 024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d 13 | - 02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e 14 | - 02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70 15 | - 023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe 16 | - 03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379 17 | - 03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050 18 | - 03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0 19 | - 02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62 20 | - 03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0 21 | - 0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654 22 | - 020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639 23 | - 0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30 24 | - 03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde 25 | - 02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad 26 | - 0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d 27 | - 03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc 28 | - 02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a 29 | ValidatorsCount: 7 30 | SeedList: 31 | - n3.edgeofneo.com:10332 32 | - neo3.edgeofneo.com:10332 33 | - neo1-nodes.ghostmarket.io:443 34 | - mainnet3.neo.coz.io:443 35 | - seed4.neo.org:10333 36 | - seed5.neo.org:10333 37 | - n3seed1.ngd.network:10332 38 | - rpc10.n3.nspcc.ru:10331 39 | VerifyTransactions: false 40 | P2PSigExtensions: false 41 | Hardforks: 42 | Aspidochelone: 1730000 43 | Basilisk: 4120000 44 | Cockatrice: 5450000 45 | 46 | ApplicationConfiguration: 47 | SkipBlockVerification: false 48 | # LogPath could be set up in case you need stdout logs to some proper file. 49 | LogPath: "./log/neogo.log" 50 | DBConfiguration: 51 | Type: "leveldb" #other options: 'inmemory','boltdb' 52 | # DB type options. Uncomment those you need in case you want to switch DB type. 53 | LevelDBOptions: 54 | DataDirectoryPath: "./chains/mainnet" 55 | # BoltDBOptions: 56 | # FilePath: "./chains/mainnet.bolt" 57 | P2P: 58 | Addresses: 59 | - ":10333" # in form of "[host]:[port][:announcedPort]" 60 | DialTimeout: 3s 61 | ProtoTickInterval: 2s 62 | PingInterval: 30s 63 | PingTimeout: 90s 64 | MaxPeers: 100 65 | AttemptConnPeers: 20 66 | MinPeers: 10 67 | Relay: true 68 | Consensus: 69 | Enabled: false 70 | UnlockWallet: 71 | Path: "/cn_wallet.json" 72 | Password: "pass" 73 | Oracle: 74 | Enabled: false 75 | AllowedContentTypes: 76 | - application/json 77 | P2PNotary: 78 | Enabled: false 79 | UnlockWallet: 80 | Path: "/notary_wallet.json" 81 | Password: "pass" 82 | 83 | RPC: 84 | Enabled: true 85 | Addresses: 86 | - ":10332" 87 | MaxGasInvoke: 15 88 | EnableCORSWorkaround: false 89 | TLSConfig: 90 | Enabled: false 91 | Addresses: 92 | - ":10331" 93 | CertFile: serv.crt 94 | KeyFile: serv.key 95 | Prometheus: 96 | Enabled: false 97 | Addresses: 98 | - ":2112" 99 | Pprof: 100 | Enabled: false 101 | Addresses: 102 | - ":2113" 103 | -------------------------------------------------------------------------------- /gui/src/helpers/grapher.js: -------------------------------------------------------------------------------- 1 | import cytoscape from 'cytoscape'; 2 | import { Formatter } from './formatter'; 3 | import cytoscapeDagre from 'cytoscape-dagre'; 4 | import cytoscapePopper from 'cytoscape-popper'; 5 | 6 | cytoscape.use(cytoscapeDagre); 7 | cytoscape.use(cytoscapePopper) 8 | 9 | export class Grapher { 10 | static getNodesFromAddresses(addresses) { 11 | return addresses.map(a => { 12 | return { data: { id: a } } 13 | }) 14 | } 15 | 16 | static getEdgesFromTransfers(transfers) { 17 | return transfers.map(t => { 18 | let i = t.from.slice(0,5) + t.to.slice(0,5) 19 | return { data: { id: i, weight: t.amount, source: t.from, target: t.to } } 20 | }) 21 | } 22 | 23 | static default(id){ 24 | 25 | return cytoscape({ 26 | container: document.getElementById(id), 27 | 28 | boxSelectionEnabled: false, 29 | autounselectify: true, 30 | 31 | style: cytoscape.stylesheet() 32 | .selector('node') 33 | .style({ 34 | 'content': 'data(id)', 35 | 'background-color': "#61bffc", 36 | 'color': "white" 37 | }) 38 | .selector('edge') 39 | .style({ 40 | 'curve-style': 'segments', 41 | 'target-arrow-shape': 'triangle', 42 | 'width': 2, 43 | 'line-color': '#ddd', 44 | 'line-opacity': 0.5, 45 | 'target-arrow-color': '#ddd' 46 | }) 47 | .selector('.highlighted') 48 | .style({ 49 | 'background-color': '#61bffc', 50 | 'line-color': '#61bffc', 51 | 'target-arrow-color': '#61bffc', 52 | 'transition-property': 'background-color, line-color, target-arrow-color', 53 | 'transition-duration': '0.5s' 54 | }), 55 | 56 | elements: { 57 | nodes: [ 58 | { data: { id: "Hello" } }, 59 | { data: { id: "World" } } 60 | ], 61 | edges: [ 62 | { data: { id: "HelloWorld", weight: 1, source: "Hello", target: "World" } } 63 | ] 64 | }, 65 | 66 | layout: { 67 | name: 'dagre', 68 | fit: true, 69 | padding: 20, 70 | nodeDimensionsIncludeLabels: true, 71 | } 72 | }) 73 | } 74 | 75 | static draw(nodes, edges) { 76 | 77 | let primary = Formatter.parseIfHsl(getComputedStyle(document.documentElement).getPropertyValue('--primary')) 78 | let secondary = Formatter.parseIfHsl(getComputedStyle(document.documentElement).getPropertyValue('--secondary')) 79 | 80 | return cytoscape({ 81 | container: document.getElementById("cyto"), 82 | 83 | boxSelectionEnabled: false, 84 | autounselectify: true, 85 | 86 | style: cytoscape.stylesheet() 87 | .selector('node') 88 | .style({ 89 | 'content': 'data(id)', 90 | 'background-color': primary, 91 | 'color': primary 92 | }) 93 | .selector('edge') 94 | .style({ 95 | 'curve-style': 'bezier', 96 | 'target-arrow-shape': 'triangle', 97 | 'width': 2, 98 | 'line-color': '#ddd', 99 | 'line-opacity': 0.5, 100 | 'target-arrow-color': secondary 101 | }) 102 | .selector('.highlighted') 103 | .style({ 104 | 'background-color': '#61bffc', 105 | 'line-color': '#61bffc', 106 | 'target-arrow-color': '#61bffc', 107 | 'transition-property': 'background-color, line-color, target-arrow-color', 108 | 'transition-duration': '0.5s' 109 | }), 110 | 111 | elements: { 112 | nodes: nodes, 113 | edges: edges 114 | }, 115 | 116 | layout: { 117 | name: 'dagre', 118 | fit: true, 119 | padding: 20, 120 | nodeDimensionsIncludeLabels: true, 121 | } 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /indexer/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use log::{error, info}; 3 | use tokio::time::{sleep, Duration}; 4 | 5 | use std::time::SystemTime; 6 | 7 | mod config; 8 | mod db; 9 | mod rpc; 10 | mod spawn; 11 | mod utils; 12 | 13 | use config::AppConfig; 14 | use db::database::Database as LocalDatabase; 15 | use db::model::Block; 16 | use rpc::client::Client as RpcClient; 17 | use spawn::indexer::Indexer; 18 | use utils::logger; 19 | 20 | use spawn::sync::run_node; 21 | use utils::node::check_neogo; 22 | 23 | #[tokio::main] 24 | async fn main() { 25 | logger::init(); 26 | 27 | if let Err(e) = run().await { 28 | // if let Err(e) = dev_run().await { 29 | error!("Application error: {:?}", e); 30 | std::process::exit(1); 31 | } 32 | } 33 | 34 | // shortcut for development runs 35 | #[allow(dead_code)] 36 | async fn dev_run() -> Result<()> { 37 | let config = AppConfig::new(); 38 | 39 | check_neogo(&config) 40 | .await 41 | .context("Failed to confirm NeoGo install")?; 42 | 43 | info!("Dev run complete"); 44 | Ok(()) 45 | } 46 | 47 | async fn run() -> Result<()> { 48 | let config = AppConfig::new(); 49 | 50 | let client = RpcClient::new(&config); 51 | let db = LocalDatabase::new(&config).context("Failed to initialize database")?; 52 | 53 | info!("Welcome to Shrike!"); 54 | info!("Checking for NeoGo.."); 55 | 56 | check_neogo(&config) 57 | .await 58 | .context("Failed to confirm NeoGo install")?; 59 | 60 | // make sure WAL journal mode is enabled 61 | db.set_to_wal().context("Failed to set to WAL")?; 62 | 63 | // fails if it already exists 64 | db.create_block_table() 65 | .context("Failed to create block table")?; 66 | db.create_transaction_table() 67 | .context("Failed to create transaction table")?; 68 | db.create_address_table() 69 | .context("Failed to create address table")?; 70 | db.create_contract_table() 71 | .context("Failed to create contract table")?; 72 | 73 | // create indexes if they don't exist 74 | db.create_index("idx_blocks_hash", "blocks", "hash") 75 | .context("Failed to create block index")?; 76 | db.create_index("idx_tx_hash", "transactions", "hash") 77 | .context("Failed to create txid index")?; 78 | db.create_index("idx_tx_senders", "transactions", "sender") 79 | .context("Failed to create txsender index")?; 80 | db.create_index("idx_transaction_block_index", "transactions", "block_index") 81 | .context("Failed to create transaction block index")?; 82 | db.create_index("idx_address_address", "addresses", "address") 83 | .context("Failed to create address index")?; 84 | db.create_index("idx_contract_hash", "contracts", "hash") 85 | .context("Failed to create contract index")?; 86 | 87 | // some setup 88 | let index_result = db 89 | .get_last_index("blocks") 90 | .context("Failed to get last stored block index"); 91 | 92 | if let Ok(value) = index_result { 93 | info!("Last stored block index: {}", value); 94 | value 95 | } else { 96 | info!("No rows in table yet. Adding first entry..."); 97 | db.insert_into_block_table(&Block::genesis_block()) 98 | .context("Failed to insert genesis block")?; 99 | 0 100 | }; 101 | 102 | // spawn the node and wait for the sync to complete 103 | info!("Starting node sync.."); 104 | let start = SystemTime::now(); 105 | let (_stderr_out, handle, shutdown_tx) = run_node(config.height_limit) 106 | .await 107 | .context("Failed to sync node")?; 108 | 109 | let sync_end = SystemTime::now(); 110 | let sync_duration = sync_end.duration_since(start)?; 111 | info!("Sync completed in {} ms.", sync_duration.as_millis()); 112 | sleep(Duration::from_secs(2)).await; 113 | 114 | // Launch indexer 115 | let indexer = Indexer::new(client, db, config); 116 | indexer.run().await?; 117 | 118 | // send the shutdown signal to the node and wait for it to exit 119 | let _ = shutdown_tx.send(()); 120 | handle.await.context("Failed to kill node")?; 121 | 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /indexer/src/utils/node.rs: -------------------------------------------------------------------------------- 1 | use log::{error, info, warn}; 2 | use text_io::read; 3 | use tokio::{fs::File, io::AsyncWriteExt}; 4 | 5 | use crate::config::AppConfig; 6 | use std::{env, path::Path, process::Command}; 7 | 8 | // move these to config in future 9 | #[cfg(target_os = "linux")] 10 | pub static NEOGO_PATH: &str = "./neogo"; 11 | #[cfg(target_os = "macos")] 12 | pub static NEOGO_PATH: &str = "./neogo"; 13 | #[cfg(target_os = "windows")] 14 | pub static NEOGO_PATH: &str = "./neogo.exe"; 15 | 16 | fn get_neogo_release_notes(config: &AppConfig) -> String { 17 | #[cfg(target_os = "linux")] 18 | { 19 | format!( 20 | "https://github.com/nspcc-dev/neo-go/releases/tag/{}", 21 | config.node_version 22 | ) 23 | } 24 | #[cfg(target_os = "macos")] 25 | { 26 | format!( 27 | "https://github.com/nspcc-dev/neo-go/releases/tag/{}", 28 | config.node_version 29 | ) 30 | } 31 | #[cfg(target_os = "windows")] 32 | { 33 | format!( 34 | "https://github.com/nspcc-dev/neo-go/releases/tag/{}", 35 | config.node_version 36 | ) 37 | } 38 | } 39 | 40 | fn get_neogo_dl(config: &AppConfig) -> String { 41 | #[cfg(target_os = "linux")] 42 | { 43 | format!( 44 | "https://github.com/nspcc-dev/neo-go/releases/download/{}/neo-go-linux-amd64", 45 | config.node_version 46 | ) 47 | } 48 | #[cfg(target_os = "macos")] 49 | { 50 | format!( 51 | "https://github.com/nspcc-dev/neo-go/releases/download/{}/neo-go-darwin-arm64", 52 | config.node_version 53 | ) 54 | } 55 | #[cfg(target_os = "windows")] 56 | { 57 | format!( 58 | "https://github.com/nspcc-dev/neo-go/releases/download/{}/neo-go-windows-amd64.exe", 59 | config.node_version 60 | ) 61 | } 62 | } 63 | 64 | pub async fn check_neogo(config: &AppConfig) -> Result<(), anyhow::Error> { 65 | let path = Path::new(NEOGO_PATH); 66 | if !path.exists() { 67 | warn!("NeoGo not found in directory. Install? (y/n)"); 68 | let answer: char = read!(); 69 | assert!((answer == 'y'), "User declined to install NeoGo."); 70 | 71 | let mut file = File::create(path).await?; 72 | let mut response = reqwest::get(&get_neogo_dl(config)).await.unwrap(); 73 | while let Some(chunk) = response.chunk().await.unwrap() { 74 | file.write_all(&chunk).await?; 75 | } 76 | 77 | if env::consts::OS != "windows" { 78 | info!("Updating permissions.."); 79 | Command::new("chmod") 80 | .arg("+x") 81 | .arg(NEOGO_PATH) 82 | .output() 83 | .expect("failed to update permissions"); 84 | } 85 | } else { 86 | info!("NeoGo already installed."); 87 | let installed_version = check_neogo_version()?; 88 | let expected_version = config.node_version.to_string(); 89 | 90 | if installed_version != expected_version { 91 | error!("Incorrect NeoGo version detected. Remove {} and re-run to install the correct version.", NEOGO_PATH); 92 | error!("Check the NeoGo version release notes at {} to see if chain state data is compatible.", get_neogo_release_notes(config)); 93 | return Err(anyhow::anyhow!( 94 | "NeoGo version mismatch. Expected {}, got {}.", 95 | expected_version, 96 | installed_version 97 | )); 98 | } 99 | } 100 | Ok(()) 101 | } 102 | 103 | pub fn check_neogo_version() -> Result { 104 | let command_output = Command::new(NEOGO_PATH) 105 | .arg("-v") 106 | .output() 107 | .expect("Failed to execute version check"); 108 | 109 | if !command_output.status.success() { 110 | return Err(anyhow::anyhow!("NeoGo version check failed.")); 111 | } 112 | 113 | let mut lines = command_output.stdout.split(|b| *b == b'\n'); 114 | let version_line = lines.find(|line| line.starts_with(b"Version:")); 115 | 116 | if let Some(version_line) = version_line { 117 | let version = String::from_utf8_lossy(version_line); 118 | let version = format!("v{}", version.trim().split(' ').nth(1).unwrap()); 119 | Ok(version) 120 | } else { 121 | Err(anyhow::anyhow!("NeoGo version check failed.")) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /api/src/transaction/internals.rs: -------------------------------------------------------------------------------- 1 | use lib::neo; 2 | use r2d2::PooledConnection; 3 | use r2d2_sqlite::SqliteConnectionManager; 4 | 5 | use crate::block::internals; 6 | use crate::error::Error; 7 | use crate::shared::events; 8 | use crate::shared::models::{Transaction, TransactionList, TxDataList}; 9 | 10 | pub fn get_transaction_internal( 11 | conn: &PooledConnection, 12 | hash: String, 13 | ) -> Result { 14 | let sql = "SELECT * FROM transactions WHERE hash = ?"; 15 | let mut stmt = conn.prepare(sql).unwrap(); 16 | 17 | let transaction = stmt.query_row([hash], |row| { 18 | Ok(Transaction { 19 | index: row.get(0)?, 20 | hash: row.get(1)?, 21 | block_index: row.get(2)?, 22 | vm_state: row.get(3)?, 23 | size: row.get(4)?, 24 | version: row.get(5)?, 25 | nonce: row.get(6)?, 26 | sender: row.get(7)?, 27 | sysfee: row.get(8)?, 28 | netfee: row.get(9)?, 29 | valid_until: row.get(10)?, 30 | signers: row.get(11)?, 31 | script: row.get(12)?, 32 | witnesses: row.get(13)?, 33 | stack_result: row.get(14)?, 34 | notifications: row.get(15)?, 35 | }) 36 | }); 37 | 38 | transaction.map_err(|_| Error { 39 | error: "Transaction does not exist.".to_string(), 40 | }) 41 | } 42 | 43 | pub fn get_sender_transactions_internal( 44 | conn: &PooledConnection, 45 | address: String, 46 | ) -> Result { 47 | let sql = "SELECT * FROM transactions WHERE sender = ?"; 48 | let mut stmt = conn.prepare(sql).unwrap(); 49 | 50 | let mut rows = stmt.query([address]).unwrap(); 51 | let mut transactions = Vec::new(); 52 | 53 | while let Some(row) = rows.next().unwrap() { 54 | transactions.push(Transaction { 55 | index: row.get(0).unwrap(), 56 | hash: row.get(1).unwrap(), 57 | block_index: row.get(2).unwrap(), 58 | vm_state: row.get(3).unwrap(), 59 | size: row.get(4).unwrap(), 60 | version: row.get(5).unwrap(), 61 | nonce: row.get(6).unwrap(), 62 | sender: row.get(7).unwrap(), 63 | sysfee: row.get(8).unwrap(), 64 | netfee: row.get(9).unwrap(), 65 | valid_until: row.get(10).unwrap(), 66 | signers: row.get(11).unwrap(), 67 | script: row.get(12).unwrap(), 68 | witnesses: row.get(13).unwrap(), 69 | stack_result: row.get(14).unwrap(), 70 | notifications: row.get(15).unwrap(), 71 | }) 72 | } 73 | 74 | match transactions.is_empty() { 75 | false => Ok(TransactionList { transactions }), 76 | true => Err(Error { 77 | error: "No transactions for that sender.".to_string(), 78 | }), 79 | } 80 | } 81 | 82 | pub fn get_address_transfers_internal( 83 | conn: &PooledConnection, 84 | address: String, 85 | ) -> Result { 86 | let base64 = neo::address_to_base64(&address); 87 | let sql = "SELECT * FROM transactions WHERE notifications LIKE ?"; 88 | let mut stmt = conn.prepare(sql).unwrap(); 89 | 90 | let mut rows = stmt.query([format!("%{}%", base64)]).unwrap(); 91 | let mut transactions = Vec::new(); 92 | 93 | while let Some(row) = rows.next().unwrap() { 94 | transactions.push(Transaction { 95 | index: row.get(0).unwrap(), 96 | hash: row.get(1).unwrap(), 97 | block_index: row.get(2).unwrap(), 98 | vm_state: row.get(3).unwrap(), 99 | size: row.get(4).unwrap(), 100 | version: row.get(5).unwrap(), 101 | nonce: row.get(6).unwrap(), 102 | sender: row.get(7).unwrap(), 103 | sysfee: row.get(8).unwrap(), 104 | netfee: row.get(9).unwrap(), 105 | valid_until: row.get(10).unwrap(), 106 | signers: row.get(11).unwrap(), 107 | script: row.get(12).unwrap(), 108 | witnesses: row.get(13).unwrap(), 109 | stack_result: row.get(14).unwrap(), 110 | notifications: row.get(15).unwrap(), 111 | }) 112 | } 113 | 114 | let mut tx_list = TxDataList { 115 | address: address.clone(), 116 | as_sender: Vec::new(), 117 | as_participant: Vec::new(), 118 | }; 119 | 120 | for transaction in transactions { 121 | let sender = transaction.clone().sender; 122 | let block_time = 123 | internals::get_block_time(conn, transaction.block_index.to_string()).unwrap(); 124 | let mut tx_data = events::get_transfer_events(transaction); 125 | tx_data.time = block_time; 126 | 127 | if sender == address { 128 | tx_list.as_sender.push(tx_data); 129 | } else { 130 | tx_list.as_participant.push(tx_data); 131 | } 132 | } 133 | 134 | if tx_list.as_sender.is_empty() && tx_list.as_participant.is_empty() { 135 | Err(Error { 136 | error: "No transfers for that sender.".to_string(), 137 | }) 138 | } else { 139 | Ok(tx_list) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /gui/src/components/Stats.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal, createMemo } from "solid-js" 2 | import { API_PATH } from "../constants/index.js" 3 | 4 | export default function Stats() { 5 | 6 | const [lastRefresh, setLastRefresh] = createSignal(new Date().toUTCString()) 7 | const [blocks, setBlocks] = createSignal(0) 8 | const [transactions, setTransactions] = createSignal(0) 9 | const [senders, setSenders] = createSignal(0) 10 | const [burned, setBurned] = createSignal(0) 11 | const [deployed, setDeployed] = createSignal(0) 12 | const [transfers, setTransfers] = createSignal(0) 13 | 14 | const fetchStats = async () => { 15 | let stat_request = fetch(`${API_PATH}/stat/stats`) 16 | return await (await stat_request).json() 17 | } 18 | 19 | const updateStats = (stats) => { 20 | setBlocks(stats.total_blocks) 21 | setTransactions(stats.total_transactions) 22 | setSenders(stats.total_senders) 23 | setBurned(stats.total_sysfee) 24 | setDeployed(stats.total_contracts) 25 | setTransfers(stats.total_transfers) 26 | } 27 | 28 | const updateLastRefresh = () => { 29 | setLastRefresh(new Date().toUTCString()) 30 | } 31 | 32 | const updateAll = async () => { 33 | let stats = await fetchStats() 34 | 35 | if (stats.total_blocks !== blocks()) { 36 | updateStats(stats) 37 | updateLastRefresh() 38 | } 39 | } 40 | 41 | createMemo(async () => { 42 | 43 | // initial fetch then try update every 5 seconds 44 | await updateAll() 45 | 46 | setInterval(async () => { 47 | await updateAll() 48 | }, 5000) 49 | }) 50 | 51 | return ( 52 |
53 |
54 |

Stats

55 |
56 |
{ lastRefresh() }
57 |
Last Updated
58 |
59 |
60 |
61 | 62 | 63 |

64 |
Current Height
65 |
66 | 67 |

{ blocks() }

68 |
Current Height
69 |
70 |
71 |
72 |
73 | 74 | 75 |

76 |
Total Transactions
77 |
78 | 79 |

{ transactions() }

80 |
Total Transactions
81 |
82 |
83 |
84 |
85 | 86 | 87 |

88 |
Unique Senders
89 |
90 | 91 |

{ senders() }

92 |
Unique Senders
93 |
94 |
95 |
96 |
97 | 98 | 99 |

100 |
GAS Burned
101 |
102 | 103 |

{ burned() }

104 |
GAS Burned
105 |
106 |
107 |
108 |
109 | 110 | 111 |

112 |
Total Contracts
113 |
114 | 115 |

{ deployed() }

116 |
Total Contracts
117 |
118 |
119 |
120 |
121 | 122 | 123 |

124 |
Total Transfers
125 |
126 | 127 |

{ transfers() }

128 |
Total Transfers
129 |
130 |
131 |
132 |
133 |
134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /indexer/src/utils/conversion.rs: -------------------------------------------------------------------------------- 1 | use lib::neo::{ 2 | base64_to_address, base64_to_hex, base64_to_script_hash, hex_decode, hex_to_base64, 3 | neo3_disassemble, 4 | }; 5 | use serde_json::to_string; 6 | 7 | use crate::db::model::{Address, Block, Contract, Transaction}; 8 | use crate::rpc::models::{ 9 | BlockAppLogResult, BlockResult, TransactionAppLogResult, TransactionResult, 10 | }; 11 | 12 | pub fn convert_block_result(r: BlockResult, a: &BlockAppLogResult) -> Block { 13 | let block_reward = &a.executions[1].notifications[0].state.value[2].value; 14 | let block_receiver = &a.executions[1].notifications[0].state.value[1].value; 15 | 16 | let reward_string = block_reward.clone().unwrap().as_str().unwrap().to_string(); 17 | let reward = reward_string.parse::().unwrap(); 18 | let reward_as_float = reward as f64 / 100_000_000_f64; 19 | 20 | let receiver = serde_json::to_string(block_receiver).unwrap(); 21 | let stripped = &receiver[1..29]; 22 | let address = base64_to_address(stripped); 23 | 24 | Block { 25 | hash: r.hash, 26 | size: r.size, 27 | version: r.version, 28 | merkle_root: r.merkleroot, 29 | time: r.time, 30 | nonce: r.nonce, 31 | speaker: r.primary, 32 | next_consensus: r.nextconsensus, 33 | reward: reward_as_float, 34 | reward_receiver: address, 35 | witnesses: to_string(&r.witnesses).unwrap(), 36 | } 37 | } 38 | 39 | pub fn convert_transaction_result( 40 | t: TransactionResult, 41 | a: &TransactionAppLogResult, 42 | block_height: u64, 43 | ) -> Transaction { 44 | let state = &a.executions[0].vmstate; 45 | let stack = &a.executions[0].stack; 46 | let notifs = &a.executions[0].notifications; 47 | 48 | Transaction { 49 | hash: t.hash, 50 | block_index: block_height, 51 | vm_state: state.to_string(), 52 | size: t.size, 53 | version: t.version, 54 | nonce: t.nonce, 55 | sender: t.sender, 56 | sysfee: t.sysfee, 57 | netfee: t.netfee, 58 | valid_until: t.validuntilblock, 59 | signers: to_string(&t.signers).unwrap(), 60 | script: base64_to_hex(&t.script), 61 | witnesses: to_string(&t.witnesses).unwrap(), 62 | stack_result: to_string(&stack).unwrap(), 63 | notifications: to_string(¬ifs).unwrap(), 64 | } 65 | } 66 | 67 | pub fn convert_contract_result( 68 | script: String, 69 | notifications: serde_json::Value, 70 | block_height: u64, 71 | ) -> Vec { 72 | let mut contracts = Vec::new(); 73 | 74 | for notification in notifications.as_array().unwrap() { 75 | if notification["eventname"] == "Deploy" 76 | && notification["contract"] == "0xfffdc93764dbaddd97c48f252a53ea4643faa3fd" 77 | { 78 | let full_disassembled_script = neo3_disassemble(&hex_to_base64(&script)); 79 | let disassembled_script: Vec<&str> = full_disassembled_script.split("\n").collect(); 80 | 81 | let mut contract_supported_standard: String = "[]".to_string(); 82 | 83 | if let Some(data) = disassembled_script 84 | .iter() 85 | .find(|&s| s.contains("PUSHDATA2")) 86 | { 87 | let parts: Vec<&str> = data.split_whitespace().collect(); 88 | let metadata_hex = parts.get(1).unwrap_or(&""); 89 | let metadata_hex_decoded = hex_decode(metadata_hex); 90 | let metadata = String::from_utf8(metadata_hex_decoded).unwrap(); 91 | 92 | if metadata.starts_with("{") { 93 | let metadata_json: serde_json::Value = serde_json::from_str(&metadata).unwrap(); 94 | 95 | contract_supported_standard = metadata_json["supportedstandards"].to_string(); 96 | } 97 | } 98 | 99 | let contract_hash_base64 = notification["state"]["value"][0]["value"].clone(); 100 | let contract_script_hash = 101 | base64_to_script_hash(contract_hash_base64.as_str().unwrap()); 102 | 103 | contracts.push(Contract { 104 | block_index: block_height, 105 | hash: contract_script_hash, 106 | contract_type: contract_supported_standard, 107 | }); 108 | } 109 | } 110 | 111 | return contracts; 112 | } 113 | 114 | pub fn convert_address_result(notifications: serde_json::Value, block_height: u64) -> Vec
{ 115 | let mut addresses = Vec::new(); 116 | 117 | for notification in notifications.as_array().unwrap() { 118 | if notification["eventname"] == "Transfer" { 119 | let state = notification["state"].clone(); 120 | 121 | if let (Some(sender_type), Some(recipient_type)) = ( 122 | state["value"][0]["type"].as_str(), 123 | state["value"][1]["type"].as_str(), 124 | ) { 125 | if sender_type == "ByteString" && recipient_type == "ByteString" { 126 | let sender_address = 127 | base64_to_address(state["value"][0]["value"].as_str().unwrap()); 128 | let recipient_address = 129 | base64_to_address(state["value"][1]["value"].as_str().unwrap()); 130 | 131 | addresses.push(Address { 132 | block_index: block_height, 133 | address: sender_address, 134 | balances: "{}".to_string(), 135 | }); 136 | 137 | addresses.push(Address { 138 | block_index: block_height, 139 | address: recipient_address, 140 | balances: "{}".to_string(), 141 | }); 142 | } 143 | } 144 | } 145 | } 146 | 147 | return addresses; 148 | } 149 | -------------------------------------------------------------------------------- /api/src/block/internals.rs: -------------------------------------------------------------------------------- 1 | use r2d2::PooledConnection; 2 | use r2d2_sqlite::SqliteConnectionManager; 3 | 4 | use super::models::Block; 5 | use crate::error::Error; 6 | use crate::shared::checker; 7 | use crate::shared::models::Transaction; 8 | 9 | pub fn get_block_internal( 10 | conn: &PooledConnection, 11 | path: String, 12 | ) -> Result { 13 | match path.trim().parse::() { 14 | Ok(id) => { 15 | let sql = "SELECT * FROM blocks WHERE id = ?"; 16 | let mut stmt = conn.prepare(sql).unwrap(); 17 | 18 | let result = stmt.query_row([id], |row| { 19 | Ok(Block { 20 | index: row.get(0)?, 21 | hash: row.get(1)?, 22 | size: row.get(2)?, 23 | version: row.get(3)?, 24 | merkle_root: row.get(4)?, 25 | time: row.get(5)?, 26 | nonce: row.get(6)?, 27 | speaker: row.get(7)?, 28 | next_consensus: row.get(8)?, 29 | reward: row.get(9)?, 30 | reward_receiver: row.get(10)?, 31 | witnesses: row.get(11)?, 32 | }) 33 | }); 34 | 35 | result.map_err(|_| Error { 36 | error: "Block does not exist.".to_string(), 37 | }) 38 | } 39 | Err(_) => { 40 | if !checker::is_neo_txid_hash(&path) { 41 | return Err(Error { 42 | error: "Invalid block hash.".to_string(), 43 | }); 44 | } 45 | 46 | let sql = "SELECT * FROM blocks WHERE hash = ?"; 47 | let mut stmt = conn.prepare(sql).unwrap(); 48 | 49 | let result = stmt.query_row([path], |row| { 50 | Ok(Block { 51 | index: row.get(0)?, 52 | hash: row.get(1)?, 53 | size: row.get(2)?, 54 | version: row.get(3)?, 55 | merkle_root: row.get(4)?, 56 | time: row.get(5)?, 57 | nonce: row.get(6)?, 58 | speaker: row.get(7)?, 59 | next_consensus: row.get(8)?, 60 | reward: row.get(9)?, 61 | reward_receiver: row.get(10)?, 62 | witnesses: row.get(11)?, 63 | }) 64 | }); 65 | 66 | result.map_err(|_| Error { 67 | error: "Block does not exist.".to_string(), 68 | }) 69 | } 70 | } 71 | } 72 | 73 | pub fn get_block_time( 74 | conn: &PooledConnection, 75 | path: String, 76 | ) -> Result { 77 | match path.trim().parse::() { 78 | Ok(id) => { 79 | let sql = "SELECT time FROM blocks WHERE id = ?"; 80 | let mut stmt = conn.prepare(sql).unwrap(); 81 | 82 | let result = stmt.query_row([id], |row| row.get(0)); 83 | 84 | result.map_err(|_| Error { 85 | error: "Block does not exist.".to_string(), 86 | }) 87 | } 88 | Err(_) => { 89 | if !checker::is_neo_txid_hash(&path) { 90 | return Err(Error { 91 | error: "Invalid block hash.".to_string(), 92 | }); 93 | } 94 | 95 | let sql = "SELECT time FROM blocks WHERE hash = ?"; 96 | let mut stmt = conn.prepare(sql).unwrap(); 97 | 98 | let result = stmt.query_row([path], |row| row.get(0)); 99 | 100 | result.map_err(|_| Error { 101 | error: "Block does not exist.".to_string(), 102 | }) 103 | } 104 | } 105 | } 106 | 107 | pub fn get_block_transactions_internal( 108 | conn: &PooledConnection, 109 | path: String, 110 | ) -> Result, Error> { 111 | match path.trim().parse::() { 112 | Ok(id) => { 113 | let sql = "SELECT * FROM transactions WHERE block_index = ?"; 114 | let mut stmt = conn.prepare(sql).unwrap(); 115 | 116 | let mut rows = stmt.query([id]).unwrap(); 117 | let mut transactions = Vec::new(); 118 | 119 | while let Some(row) = rows.next().unwrap() { 120 | transactions.push(Transaction { 121 | index: row.get(0).unwrap(), 122 | hash: row.get(1).unwrap(), 123 | block_index: row.get(2).unwrap(), 124 | vm_state: row.get(3).unwrap(), 125 | size: row.get(4).unwrap(), 126 | version: row.get(5).unwrap(), 127 | nonce: row.get(6).unwrap(), 128 | sender: row.get(7).unwrap(), 129 | sysfee: row.get(8).unwrap(), 130 | netfee: row.get(9).unwrap(), 131 | valid_until: row.get(10).unwrap(), 132 | signers: row.get(11).unwrap(), 133 | script: row.get(12).unwrap(), 134 | witnesses: row.get(13).unwrap(), 135 | stack_result: row.get(14).unwrap(), 136 | notifications: row.get(15).unwrap(), 137 | }) 138 | } 139 | Ok(transactions) 140 | } 141 | Err(_) => { 142 | if !checker::is_neo_txid_hash(&path) { 143 | return Err(Error { 144 | error: "Invalid block hash.".to_string(), 145 | }); 146 | } 147 | 148 | let sql = "SELECT * FROM transactions WHERE block_index = (SELECT id FROM blocks WHERE hash = ?)"; 149 | let mut stmt = conn.prepare(sql).unwrap(); 150 | 151 | let mut rows = stmt.query([path]).unwrap(); 152 | let mut transactions = Vec::new(); 153 | 154 | while let Some(row) = rows.next().unwrap() { 155 | transactions.push(Transaction { 156 | index: row.get(0).unwrap(), 157 | hash: row.get(1).unwrap(), 158 | block_index: row.get(2).unwrap(), 159 | vm_state: row.get(3).unwrap(), 160 | size: row.get(4).unwrap(), 161 | version: row.get(5).unwrap(), 162 | nonce: row.get(6).unwrap(), 163 | sender: row.get(7).unwrap(), 164 | sysfee: row.get(8).unwrap(), 165 | netfee: row.get(9).unwrap(), 166 | valid_until: row.get(10).unwrap(), 167 | signers: row.get(11).unwrap(), 168 | script: row.get(12).unwrap(), 169 | witnesses: row.get(13).unwrap(), 170 | stack_result: row.get(14).unwrap(), 171 | notifications: row.get(15).unwrap(), 172 | }) 173 | } 174 | Ok(transactions) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /api/src/stat/internals.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | use once_cell::sync::Lazy; 3 | use r2d2::PooledConnection; 4 | use r2d2_sqlite::SqliteConnectionManager; 5 | use tokio::task; 6 | 7 | use std::sync::RwLock; 8 | 9 | use crate::shared::models::GAS_PRECISION; 10 | use crate::ConnectionPool; 11 | 12 | use super::models::{NetworkStatistics, ShrikeStats}; 13 | 14 | pub static CURRENT_NETWORK_STATISTICS: Lazy> = Lazy::new(|| { 15 | let s = NetworkStatistics { 16 | total_transactions: 0, 17 | total_addresses: 0, 18 | total_contracts: 0, 19 | current_week_transactions: 0, 20 | current_week_addresses: 0, 21 | current_week_contracts: 0, 22 | }; 23 | RwLock::new(s) 24 | }); 25 | 26 | pub static CURRENT_STATS: Lazy> = Lazy::new(|| { 27 | let s = ShrikeStats { 28 | total_blocks: 0, 29 | total_transactions: 0, 30 | total_sysfee: 0.0, 31 | total_transfers: 0, 32 | total_senders: 0, 33 | total_contracts: 0, 34 | }; 35 | RwLock::new(s) 36 | }); 37 | 38 | pub fn get_stat_internal( 39 | conn: &PooledConnection, 40 | sql: &str, 41 | ) -> T { 42 | let mut stmt = conn.prepare(sql).unwrap(); 43 | let total: Result = stmt.query_row([], |row| row.get(0)); 44 | 45 | total.unwrap() 46 | } 47 | 48 | pub async fn set_stats_internal(pool: web::Data) { 49 | let conn1 = pool.connection.clone().get().unwrap(); 50 | 51 | let blocks = task::spawn_blocking(move || get_blocks_internal(&conn1)) 52 | .await 53 | .unwrap(); 54 | 55 | let current_block = CURRENT_STATS.read().unwrap().total_blocks; 56 | 57 | if blocks > current_block { 58 | let conn2 = pool.connection.clone().get().unwrap(); 59 | let conn3 = pool.connection.clone().get().unwrap(); 60 | let conn4 = pool.connection.clone().get().unwrap(); 61 | let conn5 = pool.connection.clone().get().unwrap(); 62 | let conn6 = pool.connection.clone().get().unwrap(); 63 | let conn7 = pool.connection.clone().get().unwrap(); 64 | let conn8 = pool.connection.clone().get().unwrap(); 65 | let conn9 = pool.connection.clone().get().unwrap(); 66 | let conn10 = pool.connection.clone().get().unwrap(); 67 | 68 | let transactions = task::spawn_blocking(move || get_transactions_internal(&conn2)); 69 | 70 | let sysfees = task::spawn_blocking(move || get_sysfee_internal(&conn3)); 71 | 72 | let transfers = task::spawn_blocking(move || get_transfers_internal(&conn4)); 73 | 74 | let senders = task::spawn_blocking(move || get_senders_internal(&conn5)); 75 | 76 | let contracts = task::spawn_blocking(move || get_contracts_internal(&conn6)); 77 | 78 | let addresses = task::spawn_blocking(move || get_addresses_internal(&conn7)); 79 | 80 | let current_week_contracts = 81 | task::spawn_blocking(move || get_contracts_current_week_internal(&conn8)); 82 | 83 | let current_week_transactions = 84 | task::spawn_blocking(move || get_transactions_current_week_internal(&conn9)); 85 | 86 | let current_week_addresses = 87 | task::spawn_blocking(move || get_addresses_current_week_internal(&conn10)); 88 | 89 | let results = tokio::join!( 90 | transactions, 91 | sysfees, 92 | transfers, 93 | senders, 94 | contracts, 95 | addresses, 96 | current_week_contracts, 97 | current_week_transactions, 98 | current_week_addresses, 99 | ); 100 | 101 | let total_transactions = results.0.unwrap(); 102 | let total_contracts = results.4.unwrap(); 103 | 104 | { 105 | let mut w = CURRENT_STATS.write().unwrap(); 106 | 107 | w.total_blocks = blocks; 108 | w.total_transactions = total_transactions; 109 | w.total_sysfee = results.1.unwrap(); 110 | w.total_transfers = results.2.unwrap(); 111 | w.total_senders = results.3.unwrap(); 112 | w.total_contracts = total_contracts; 113 | } 114 | 115 | { 116 | let mut w = CURRENT_NETWORK_STATISTICS.write().unwrap(); 117 | 118 | w.total_transactions = total_transactions; 119 | w.total_addresses = results.5.unwrap(); 120 | w.total_contracts = total_contracts; 121 | w.current_week_contracts = results.6.unwrap(); 122 | w.current_week_transactions = results.7.unwrap(); 123 | w.current_week_addresses = results.8.unwrap(); 124 | } 125 | } else { 126 | // println!("No cache updated needed.") 127 | } 128 | println!("Stats refreshed. Current height is {}.", blocks); 129 | } 130 | 131 | pub fn get_blocks_internal(conn: &PooledConnection) -> u64 { 132 | let sql = "SELECT id FROM blocks WHERE id=(SELECT max(id) FROM blocks)"; 133 | get_stat_internal::(conn, sql) 134 | } 135 | 136 | pub fn get_transactions_internal(conn: &PooledConnection) -> u64 { 137 | let sql = "SELECT id FROM transactions WHERE id=(SELECT max(id) FROM transactions)"; 138 | get_stat_internal::(conn, sql) 139 | } 140 | 141 | pub fn get_sysfee_internal(conn: &PooledConnection) -> f64 { 142 | let sql = "SELECT sum(sysfee) FROM transactions"; 143 | get_stat_internal::(conn, sql) / GAS_PRECISION 144 | } 145 | 146 | pub fn get_transfers_internal(conn: &PooledConnection) -> u64 { 147 | let sql = "SELECT SUM(LENGTH(notifications) - LENGTH(REPLACE(notifications, 'Transfer', ''))) / 8 FROM transactions WHERE notifications LIKE '%Transfer%'"; 148 | get_stat_internal::(conn, sql) 149 | } 150 | 151 | pub fn get_senders_internal(conn: &PooledConnection) -> u64 { 152 | let sql = "SELECT COUNT(DISTINCT sender) FROM transactions"; 153 | get_stat_internal::(conn, sql) 154 | } 155 | 156 | pub fn get_contracts_internal(conn: &PooledConnection) -> u64 { 157 | let sql = "SELECT COUNT() FROM contracts"; 158 | 159 | let native_contracts_count = 9; // fetch natives properly in future 160 | get_stat_internal::(conn, &sql) + native_contracts_count 161 | } 162 | 163 | pub fn get_addresses_internal(conn: &PooledConnection) -> u64 { 164 | let sql = "SELECT COUNT(DISTINCT address) FROM addresses"; 165 | get_stat_internal::(conn, sql) 166 | } 167 | 168 | pub fn get_contracts_current_week_internal( 169 | conn: &PooledConnection, 170 | ) -> u64 { 171 | let sql = "SELECT COUNT(*) 172 | FROM contracts 173 | INNER JOIN blocks ON blocks.id = block_index 174 | WHERE time >= strftime('%s', 'now', '-7 days') * 1000"; 175 | get_stat_internal::(conn, sql) 176 | } 177 | 178 | pub fn get_addresses_current_week_internal( 179 | conn: &PooledConnection, 180 | ) -> u64 { 181 | let sql = "SELECT COUNT(*) 182 | FROM addresses AS a 183 | WHERE a.block_index IN ( 184 | SELECT b.id 185 | FROM blocks AS b 186 | WHERE b.time >= strftime('%s', 'now', '-7 days') * 1000 187 | ) 188 | AND NOT EXISTS ( 189 | SELECT 1 190 | FROM addresses AS a2 191 | INNER JOIN blocks AS b2 ON a2.block_index = b2.id 192 | WHERE a2.address = a.address 193 | AND b2.time < strftime('%s', 'now', '-7 days') * 1000 194 | )"; 195 | get_stat_internal::(conn, sql) 196 | } 197 | 198 | pub fn get_transactions_current_week_internal( 199 | conn: &PooledConnection, 200 | ) -> u64 { 201 | let sql = "SELECT COUNT(*) 202 | FROM transactions 203 | INNER JOIN blocks ON blocks.id = block_index 204 | WHERE time >= strftime('%s', 'now', '-7 days') * 1000"; 205 | get_stat_internal::(conn, sql) 206 | } 207 | -------------------------------------------------------------------------------- /indexer/src/spawn/indexer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use futures::future::join_all; 3 | use log::{error, info}; 4 | use tokio::time::sleep; 5 | 6 | use std::time::{Duration, SystemTime}; 7 | 8 | use crate::config::AppConfig; 9 | use crate::db::database::Database; 10 | use crate::rpc::client::Client; 11 | use crate::rpc::models::TransactionResult; 12 | use crate::utils::{conversion, logger}; 13 | pub struct Indexer { 14 | client: Client, 15 | db: Database, 16 | config: AppConfig, 17 | } 18 | 19 | impl Indexer { 20 | pub fn new(client: Client, db: Database, config: AppConfig) -> Self { 21 | Self { client, db, config } 22 | } 23 | 24 | pub async fn run(&self) -> Result<(), anyhow::Error> { 25 | let current_height = self.client.get_current_height().await?; 26 | let stored_height = self.db.get_last_index("blocks")?; 27 | info!("Chain height is {}.", current_height); 28 | 29 | // Ensure chain height isn't lower than stored height 30 | if current_height < stored_height { 31 | error!("Chain height is lower than stored height. Exiting.."); 32 | 33 | Ok(()) 34 | } else { 35 | let start_height = stored_height + 1; 36 | let index_start = SystemTime::now(); 37 | info!("Started indexing."); 38 | info!( 39 | "Start height is {}. {} blocks to process.", 40 | start_height, 41 | current_height - start_height 42 | ); 43 | 44 | self.initial_sync(start_height, current_height, self.config.batch_size) 45 | .await?; 46 | 47 | let index_end = SystemTime::now(); 48 | let index_duration = index_end.duration_since(index_start)?; 49 | let new_stored_height = self 50 | .db 51 | .get_last_index("blocks") 52 | .context("Failed to get latest stored index")?; 53 | info!("Indexing completed in {} ms.", index_duration.as_millis()); 54 | info!("New stored height is {}.", new_stored_height); 55 | 56 | if self.config.keep_alive { 57 | self.continuous_sync(new_stored_height + 1, self.config.keep_alive_interval) 58 | .await?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | async fn initial_sync( 66 | &self, 67 | mut start_height: u64, 68 | current_height: u64, 69 | batch_size: u64, 70 | ) -> Result<(), anyhow::Error> { 71 | let mut count = 0; 72 | info!("Updating tables:"); 73 | while start_height < current_height { 74 | let end_height = std::cmp::min(start_height + batch_size, current_height); 75 | 76 | self.sync_between(start_height, end_height) 77 | .await 78 | .context("Failed to synchronize block range")?; 79 | 80 | count += end_height - start_height; 81 | start_height = end_height; 82 | 83 | logger::inline_print(&format!("\rIndexed {count} block(s).")); 84 | } 85 | println!(); 86 | Ok(()) 87 | } 88 | 89 | async fn sync_between(&self, start_height: u64, end_height: u64) -> Result<(), anyhow::Error> { 90 | let future_blocks = (start_height..end_height).map(|i| self.client.fetch_full_block(i)); 91 | let all_blocks = join_all(future_blocks).await; 92 | 93 | // Have to clone to keep all_blocks unmoved for future steps 94 | let transactions_with_index: Vec<(TransactionResult, u64)> = all_blocks 95 | .iter() 96 | .filter_map(|result| { 97 | if let Ok((block, _)) = result { 98 | Some( 99 | block 100 | .tx 101 | .iter() 102 | .map(move |tx| { 103 | ( 104 | TransactionResult { 105 | hash: tx.hash.clone(), 106 | blockhash: Some(block.hash.clone()), 107 | size: tx.size, 108 | version: tx.version, 109 | nonce: tx.nonce, 110 | sender: tx.sender.clone(), 111 | sysfee: tx.sysfee.clone(), 112 | netfee: tx.netfee.clone(), 113 | validuntilblock: tx.validuntilblock, 114 | signers: tx.signers.clone(), 115 | script: tx.script.clone(), 116 | witnesses: tx.witnesses.clone(), 117 | }, 118 | block.index, 119 | ) 120 | }) 121 | .collect::>(), 122 | ) 123 | } else { 124 | None 125 | } 126 | }) 127 | .flatten() 128 | .collect(); 129 | 130 | let (transactions, block_indexes): (Vec, Vec) = 131 | transactions_with_index.into_iter().unzip(); 132 | 133 | let future_transactions = transactions 134 | .into_iter() 135 | .map(|tx| self.client.fetch_full_transaction(tx)); 136 | let all_transactions = join_all(future_transactions).await; 137 | 138 | let all_transactions_with_index = 139 | all_transactions.into_iter().zip(block_indexes.into_iter()); 140 | 141 | let prepped_blocks = all_blocks.into_iter().filter_map(|result| match result { 142 | Ok((b, a)) => Some(conversion::convert_block_result(b, &a)), 143 | Err(e) => { 144 | panic!("Error fetching or converting block: {e:?}"); 145 | } 146 | }); 147 | 148 | let prepped_tx: Vec<_> = all_transactions_with_index 149 | .into_iter() 150 | .filter_map(|(result, block_index)| match result { 151 | Ok((t, a)) => Some(conversion::convert_transaction_result(t, &a, block_index)), 152 | Err(e) => { 153 | panic!("Error fetching or converting transaction: {e:?}"); 154 | } 155 | }) 156 | .collect(); 157 | 158 | let prepped_contracts = prepped_tx.iter().flat_map(|transaction| { 159 | conversion::convert_contract_result( 160 | transaction.script.clone(), 161 | serde_json::from_str(&transaction.notifications).unwrap(), 162 | transaction.block_index, 163 | ) 164 | }); 165 | 166 | let prepped_addresses = prepped_tx.iter().flat_map(|transaction| { 167 | conversion::convert_address_result( 168 | serde_json::from_str(&transaction.notifications).unwrap(), 169 | transaction.block_index, 170 | ) 171 | }); 172 | 173 | // synced rollback point 174 | self.db 175 | .insert_blocks_transactions(prepped_blocks, prepped_tx.iter().cloned()) 176 | .context("Failed to insert data")?; 177 | 178 | self.db 179 | .insert_contracts(prepped_contracts) 180 | .context("Failed to insert contracts")?; 181 | 182 | self.db 183 | .insert_addresses(prepped_addresses) 184 | .context("Failed to insert addresses")?; 185 | 186 | Ok(()) 187 | } 188 | 189 | async fn continuous_sync(&self, start_height: u64, interval: u64) -> Result<(), anyhow::Error> { 190 | let mut current_height = start_height; 191 | 192 | info!("Listening for new blocks:"); 193 | loop { 194 | let new_height = self.client.get_current_height().await?; 195 | if new_height > current_height { 196 | self.sync_between(current_height, new_height).await?; 197 | 198 | logger::inline_print(&format!("\rCurrent synced height: {new_height}")); 199 | current_height = new_height; 200 | } 201 | sleep(Duration::from_secs(interval)).await; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /indexer/src/utils/conversion_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::utils::conversion::{convert_address_result, convert_contract_result}; 4 | use serde_json::json; 5 | 6 | #[test] 7 | fn test_convert_contract_result() { 8 | let script = "0d64077b226e616d65223a22436f6d6d6974746565496e666f436f6e7472616374222c2267726f757073223a5b5d2c226665617475726573223a7b7d2c22737570706f727465647374616e6461726473223a5b5d2c22616269223a7b226d6574686f6473223a5b7b226e616d65223a22766572696679222c22706172616d6574657273223a5b5d2c2272657475726e74797065223a22426f6f6c65616e222c226f6666736574223a302c2273616665223a66616c73657d2c7b226e616d65223a2267657441646d696e222c22706172616d6574657273223a5b5d2c2272657475726e74797065223a2248617368313630222c226f6666736574223a31342c2273616665223a66616c73657d2c7b226e616d65223a2273657441646d696e222c22706172616d6574657273223a5b7b226e616d65223a2261646d696e222c2274797065223a2248617368313630227d5d2c2272657475726e74797065223a22426f6f6c65616e222c226f6666736574223a39322c2273616665223a66616c73657d2c7b226e616d65223a22757064617465222c22706172616d6574657273223a5b7b226e616d65223a226e656646696c65222c2274797065223a22427974654172726179227d2c7b226e616d65223a226d616e6966657374222c2274797065223a22537472696e67227d2c7b226e616d65223a2264617461222c2274797065223a22416e79227d5d2c2272657475726e74797065223a22566f6964222c226f6666736574223a3136382c2273616665223a66616c73657d2c7b226e616d65223a22736574496e666f222c22706172616d6574657273223a5b7b226e616d65223a2273656e646572222c2274797065223a2248617368313630227d2c7b226e616d65223a226e616d65222c2274797065223a22537472696e67227d2c7b226e616d65223a226c6f636174696f6e222c2274797065223a22537472696e67227d2c7b226e616d65223a2277656273697465222c2274797065223a22537472696e67227d2c7b226e616d65223a22656d61696c222c2274797065223a22537472696e67227d2c7b226e616d65223a22676974687562222c2274797065223a22537472696e67227d2c7b226e616d65223a2274656c656772616d222c2274797065223a22537472696e67227d2c7b226e616d65223a2274776974746572222c2274797065223a22537472696e67227d2c7b226e616d65223a226465736372697074696f6e222c2274797065223a22537472696e67227d2c7b226e616d65223a226c6f676f222c2274797065223a22537472696e67227d5d2c2272657475726e74797065223a22426f6f6c65616e222c226f6666736574223a3232342c2273616665223a66616c73657d2c7b226e616d65223a22676574496e666f222c22706172616d6574657273223a5b7b226e616d65223a2263616e646964617465222c2274797065223a2248617368313630227d5d2c2272657475726e74797065223a22416e79222c226f6666736574223a3434382c2273616665223a66616c73657d2c7b226e616d65223a22676574416c6c496e666f222c22706172616d6574657273223a5b5d2c2272657475726e74797065223a224172726179222c226f6666736574223a3530372c2273616665223a66616c73657d2c7b226e616d65223a2264656c657465496e666f222c22706172616d6574657273223a5b7b226e616d65223a2263616e646964617465222c2274797065223a2248617368313630227d5d2c2272657475726e74797065223a22426f6f6c65616e222c226f6666736574223a3538392c2273616665223a66616c73657d2c7b226e616d65223a225f696e697469616c697a65222c22706172616d6574657273223a5b5d2c2272657475726e74797065223a22566f6964222c226f6666736574223a3639342c2273616665223a66616c73657d5d2c226576656e7473223a5b5d7d2c227065726d697373696f6e73223a5b7b22636f6e7472616374223a22307837323663623665306364383632386131333530613631313338343638383931316162373566353162222c226d6574686f6473223a5b22726970656d64313630222c22736861323536225d7d2c7b22636f6e7472616374223a22307861636365366664383064343465313739366161306332633632356539653465306365333965666330222c226d6574686f6473223a5b22646573657269616c697a65222c2273657269616c697a65225d7d2c7b22636f6e7472616374223a22307865663430373361306632623330356133386563343035306534643364323862633430656136336635222c226d6574686f6473223a5b2267657443616e64696461746573225d7d2c7b22636f6e7472616374223a22307866666664633933373634646261646464393763343866323532613533656134363433666161336664222c226d6574686f6473223a5b22757064617465225d7d5d2c22747275737473223a5b5d2c226578747261223a7b22417574686f72223a224e454f222c22456d61696c223a22646576656c6f706572406e656f2e6f7267222c224465736372697074696f6e223a22546869732069732061204e656f3320436f6e7472616374227d7d0d03044e4546334e656f2e436f6d70696c65722e43536861727020332e302e30000000000000000000000000000000000000000000000000000000000000000000000000000000000006fda3fa4346ea532a258fc497ddaddb6437c9fdff067570646174650300000ff563ea40bc283d4d0e05c48ea305b3f2a07340ef0d67657443616e646964617465730000010f1bf575ab1189688413610a35a12886cde0b66c7209726970656d643136300100010f1bf575ab1189688413610a35a12886cde0b66c72067368613235360100010fc0ef39cee0e4e925c6c2a06a79e1440dd86fceac0973657269616c697a650100010fc0ef39cee0e4e925c6c2a06a79e1440dd86fceac0b646573657269616c697a650100010f0000fde702340e41f827ec8c4041f827ec8c405701000c0a737570657241646d696e342070684ad82403ca0014972610684ad824094aca001428033a22035822024057000178419bf667ce41925de83122024041925de83140419bf667ce40ca405700010c09466f7262696464656e34a441f827ec8c3417780c0a737570657241646d696e341211db2022024057000278aa2604793a405700027978419bf667ce41e63f18844041e63f1884405700033555ffffffaa26160c114e6f20617574686f72697a6174696f6e2e3a7a797837000040370000405700015978db308b408b40db304057080a0c09466f7262696464656e7841f827ec8c34943701007010db2071684a72ca731074221f6a6ccec14575766d34617707786f0797260a11db204a714522096c9c746c6b30e10c1753656e646572206973206e6f742043616e646964617465693546ffffff7f097f087f077e7d7c7b7a79781ac04a344b726a370400783573ffffff344211db20220240370100405702015a78db308b5b8b7068db2837030037020071694ad824094aca001428033a220240db30403702004037030040db2840570001405700027978419bf667ce41e63f18844041e63f1884403704004057010178350effffff341770684ad82403ca10b726086837050022050b22024057000178419bf667ce41925de83122024041925de83140370500405703005934287010c4007168419c08ed9c26176841f354bf1d726a11ce0b982607696a11cecf22e5692202405700011a78419bf667ce41df30b89a22024041df30b89a40419c08ed9c4041f354bf1d40cf405702010c09466f7262696464656e7841f827ec8c260711db2022073598fdffff351bfeffff78355ffeffff70783558feffff3561ffffff71694ad82403ca10b7260a68340d11db20220710db2022024057000178db28419bf667ce412f58c5ed40412f58c5ed40cf4056040c14c045430c6122560cbdc5868c3a4ce02f02ddbcc1600c020c21db30620c054156e7b327db30630c0177db3061409ae617b512c01f0c066465706c6f790c14fda3fa4346ea532a258fc497ddaddb6437c9fdff41627d5b52".to_string(); 9 | 10 | let notifications = json!([ 11 | { 12 | "contract": "0xfffdc93764dbaddd97c48f252a53ea4643faa3fd", 13 | "eventname": "Deploy", 14 | "state": { 15 | "type": "Array", 16 | "value": [ 17 | { 18 | "type": "ByteString", 19 | "value": "4RvlQ9qY2B3u+HBeVhEMrbavdrc=" 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "eventname": "OnDeploy", 26 | "state": { 27 | "value": [] 28 | } 29 | } 30 | ]); 31 | 32 | let block_height = 210; 33 | 34 | let result = convert_contract_result(script, notifications, block_height); 35 | 36 | assert_eq!(result.len(), 1); 37 | let contract = &result[0]; 38 | assert_eq!(contract.block_index, block_height); 39 | assert_eq!(contract.hash, "0xb776afb6ad0c11565e70f8ee1dd898da43e51be1"); 40 | assert_eq!(contract.contract_type, "[]"); 41 | } 42 | 43 | #[test] 44 | fn test_convert_address_result() { 45 | let notifications = json!([ 46 | { 47 | "contract": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", 48 | "eventname": "Transfer", 49 | "state": { 50 | "type": "Array", 51 | "value": [ 52 | { 53 | "type": "ByteString", 54 | "value": "axI92L7HGGSIUrvHhZXjU2oFj58=" 55 | }, 56 | { 57 | "type": "ByteString", 58 | "value": "dVE6zv92GLfukg8P5gFa0cDxb/0=" 59 | }, 60 | { 61 | "type": "Integer", 62 | "value": "100000" 63 | } 64 | ] 65 | } 66 | }, 67 | { 68 | "contract": "0xd2a4cff31913016155e38e474a2c06d08be276cf", 69 | "eventname": "Transfer", 70 | "state": { 71 | "type": "Array", 72 | "value": [ 73 | { 74 | "type": "Any", 75 | "value": null 76 | }, 77 | { 78 | "type": "ByteString", 79 | "value": "axI92L7HGGSIUrvHhZXjU2oFj58=" 80 | }, 81 | { 82 | "type": "Integer", 83 | "value": "40300000" 84 | } 85 | ] 86 | } 87 | } 88 | ]); 89 | 90 | let block_height = 210; 91 | 92 | let result = convert_address_result(notifications, block_height); 93 | 94 | assert_eq!(result.len(), 2); 95 | 96 | let sender = &result[0]; 97 | 98 | assert_eq!(sender.block_index, block_height); 99 | assert_eq!(sender.address, "NVg7LjGcUSrgxgjX3zEgqaksfMaiS8Z6e1"); 100 | assert_eq!(sender.balances, "{}"); 101 | 102 | let recipient = &result[1]; 103 | assert_eq!(recipient.block_index, block_height); 104 | assert_eq!(recipient.address, "NWcHZ95TNzfVCfvK2AvY5xyEw6ur3oD3wL"); 105 | assert_eq!(recipient.balances, "{}"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /indexer/src/db/database.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use rusqlite::{params, Connection, Result}; 3 | 4 | use crate::config::AppConfig; 5 | 6 | use super::model::{Address, Block, Contract, Transaction}; 7 | 8 | pub struct Database { 9 | conn: Connection, 10 | } 11 | 12 | impl Database { 13 | pub fn new(config: &AppConfig) -> Result { 14 | if config.test_db { 15 | info!("Using test database."); 16 | let conn = Connection::open("shrike_test.db3")?; 17 | 18 | Ok(Database { conn }) 19 | } else { 20 | info!("Using database at {}.", config.db_path); 21 | let conn = Connection::open(&config.db_path)?; 22 | 23 | Ok(Database { conn }) 24 | } 25 | } 26 | 27 | pub fn set_to_wal(&self) -> Result<()> { 28 | let wal_active: String = self 29 | .conn 30 | .query_row("PRAGMA journal_mode", [], |row| row.get(0))?; 31 | if wal_active == "wal" { 32 | info!("WAL mode already active."); 33 | } else { 34 | let _: String = self 35 | .conn 36 | .query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))?; 37 | info!("Set db to WAL mode."); 38 | } 39 | 40 | Ok(()) 41 | } 42 | 43 | pub fn create_index(&self, name: &str, table: &str, column: &str) -> Result { 44 | let sql = format!("CREATE INDEX IF NOT EXISTS {name} ON {table} ({column})"); 45 | let result = self.conn.execute(&sql, [])?; 46 | 47 | Ok(result) 48 | } 49 | 50 | pub fn create_block_table(&self) -> Result { 51 | let result = self.conn.execute( 52 | "CREATE TABLE IF NOT EXISTS blocks ( 53 | id INTEGER PRIMARY KEY AUTOINCREMENT, 54 | hash TEXT NOT NULL UNIQUE, 55 | size INTEGER NOT NULL, 56 | version INTEGER NOT NULL, 57 | merkle_root TEXT NOT NULL, 58 | time INTEGER NOT NULL, 59 | nonce TEXT NOT NULL, 60 | speaker INTEGER NOT NULL, 61 | next_consensus TEXT NOT NULL, 62 | reward FLOAT NOT NULL, 63 | reward_receiver TEXT NOT NULL, 64 | witnesses TEXT NOT NULL 65 | )", 66 | [], 67 | )?; 68 | 69 | Ok(result) 70 | } 71 | 72 | pub fn create_transaction_table(&self) -> Result { 73 | let result = self.conn.execute( 74 | "CREATE TABLE IF NOT EXISTS transactions ( 75 | id INTEGER PRIMARY KEY AUTOINCREMENT, 76 | hash TEXT NOT NULL UNIQUE, 77 | block_index INTEGER NOT NULL, 78 | vm_state TEXT NOT NULL, 79 | size INTEGER NOT NULL, 80 | version INTEGER NOT NULL, 81 | nonce INTEGER NOT NULL, 82 | sender TEXT NOT NULL, 83 | sysfee TEXT NOT NULL, 84 | netfee TEXT NOT NULL, 85 | valid_until INTEGER NOT NULL, 86 | signers TEXT NOT NULL, 87 | script TEXT NOT NULL, 88 | witnesses TEXT NOT NULL, 89 | stack_result TEXT, 90 | notifications TEXT, 91 | FOREIGN KEY (block_index) REFERENCES blocks (id) 92 | )", 93 | [], 94 | )?; 95 | 96 | Ok(result) 97 | } 98 | 99 | pub fn create_address_table(&self) -> Result { 100 | let result = self.conn.execute( 101 | "CREATE TABLE IF NOT EXISTS addresses ( 102 | id INTEGER PRIMARY KEY AUTOINCREMENT, 103 | block_index INTEGER NOT NULL, 104 | address TEXT NOT NULL, 105 | balances TEXT NOT NULL, 106 | FOREIGN KEY (block_index) REFERENCES blocks (id) 107 | )", 108 | [], 109 | )?; 110 | 111 | Ok(result) 112 | } 113 | 114 | pub fn create_contract_table(&self) -> Result { 115 | let result = self.conn.execute( 116 | "CREATE TABLE IF NOT EXISTS contracts ( 117 | id INTEGER PRIMARY KEY AUTOINCREMENT, 118 | block_index INTEGER NOT NULL, 119 | hash TEXT NOT NULL UNIQUE, 120 | contract_type TEXT NOT NULL, 121 | FOREIGN KEY (block_index) REFERENCES blocks (id) 122 | )", 123 | [], 124 | )?; 125 | 126 | Ok(result) 127 | } 128 | 129 | pub fn insert_into_block_table(&self, block: &Block) -> Result { 130 | let sql = "INSERT INTO blocks ( 131 | id, hash, size, version, merkle_root, time, 132 | nonce, speaker, next_consensus, reward, reward_receiver, witnesses 133 | ) VALUES (0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"; 134 | 135 | let result = self.conn.execute( 136 | sql, 137 | params![ 138 | block.hash, 139 | block.size, 140 | block.version, 141 | block.merkle_root, 142 | block.time, 143 | block.nonce, 144 | block.speaker, 145 | block.next_consensus, 146 | block.reward, 147 | block.reward_receiver, 148 | block.witnesses 149 | ], 150 | )?; 151 | 152 | Ok(result) 153 | } 154 | 155 | pub fn insert_contracts(&self, contracts: impl Iterator) -> Result<()> { 156 | let tx = self.conn.unchecked_transaction()?; 157 | 158 | let mut stmt = self.conn.prepare_cached( 159 | "INSERT INTO contracts ( 160 | block_index, hash, contract_type 161 | ) VALUES (?1, ?2, ?3)", 162 | )?; 163 | 164 | for contract in contracts { 165 | stmt.execute(params![ 166 | contract.block_index, 167 | contract.hash, 168 | contract.contract_type 169 | ])?; 170 | } 171 | 172 | let result = tx.commit(); 173 | if let Err(e) = result { 174 | println!("Error committing transaction: {e:?}"); 175 | } 176 | 177 | Ok(()) 178 | } 179 | 180 | pub fn insert_addresses(&self, addresses: impl Iterator) -> Result<()> { 181 | let tx = self.conn.unchecked_transaction()?; 182 | 183 | let mut stmt = self.conn.prepare_cached( 184 | "INSERT INTO addresses ( 185 | block_index, address, balances 186 | ) VALUES (?1, ?2, ?3)", 187 | )?; 188 | 189 | for address in addresses { 190 | stmt.execute(params![ 191 | address.block_index, 192 | address.address, 193 | address.balances 194 | ])?; 195 | } 196 | 197 | let result = tx.commit(); 198 | if let Err(e) = result { 199 | println!("Error committing transaction: {e:?}"); 200 | } 201 | 202 | Ok(()) 203 | } 204 | 205 | // synced rollback for both tables 206 | pub fn insert_blocks_transactions( 207 | &self, 208 | blocks: impl Iterator, 209 | transactions: impl Iterator, 210 | ) -> Result<()> { 211 | let tx = self.conn.unchecked_transaction()?; 212 | 213 | let mut block_stmt = self.conn.prepare_cached( 214 | "INSERT INTO blocks ( 215 | hash, size, version, merkle_root, time, 216 | nonce, speaker, next_consensus, reward, reward_receiver, witnesses 217 | ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", 218 | )?; 219 | 220 | for block in blocks { 221 | block_stmt.execute(params![ 222 | block.hash, 223 | block.size, 224 | block.version, 225 | block.merkle_root, 226 | block.time, 227 | block.nonce, 228 | block.speaker, 229 | block.next_consensus, 230 | block.reward, 231 | block.reward_receiver, 232 | block.witnesses 233 | ])?; 234 | } 235 | 236 | let mut tx_stmt = self.conn.prepare_cached( 237 | "INSERT INTO transactions ( 238 | hash, block_index, vm_state, size, version, nonce, sender, sysfee, netfee, 239 | valid_until, signers, script, witnesses, stack_result, notifications 240 | ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", 241 | )?; 242 | 243 | for transaction in transactions { 244 | let _ = tx_stmt.execute(params![ 245 | transaction.hash, 246 | transaction.block_index, 247 | transaction.vm_state, 248 | transaction.size, 249 | transaction.version, 250 | transaction.nonce, 251 | transaction.sender, 252 | transaction.sysfee, 253 | transaction.netfee, 254 | transaction.valid_until, 255 | transaction.signers, 256 | transaction.script, 257 | transaction.witnesses, 258 | transaction.stack_result, 259 | transaction.notifications 260 | ]); 261 | } 262 | 263 | let result = tx.commit(); 264 | if let Err(e) = result { 265 | println!("Error committing transaction: {e:?}"); 266 | } 267 | 268 | Ok(()) 269 | } 270 | 271 | pub fn get_last_index(&self, table: &str) -> Result { 272 | let sql = &format!("SELECT id FROM {table} WHERE id=(SELECT max(id) FROM {table})"); 273 | let mut stmt = self.conn.prepare(sql)?; 274 | let index: u64 = stmt.query_row([], |row| row.get(0))?; 275 | 276 | Ok(index) 277 | } 278 | 279 | #[allow(dead_code)] 280 | pub fn drop_table(&self, table: &str) -> Result { 281 | let result = self.conn.execute(&format!("DROP TABLE {table}"), [])?; 282 | 283 | Ok(result) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /lib/src/neo.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use once_cell::sync::Lazy; 3 | use sha2::{Digest, Sha256}; 4 | use std::collections::HashMap; 5 | 6 | #[derive(Debug)] 7 | struct OpcodeData { 8 | name: &'static str, 9 | size: usize, 10 | } 11 | 12 | static OPCODETABLE: Lazy> = Lazy::new(|| { 13 | [ 14 | ( 15 | 0x00, 16 | OpcodeData { 17 | name: "PUSHINT8", 18 | size: 1, 19 | }, 20 | ), 21 | ( 22 | 0x01, 23 | OpcodeData { 24 | name: "PUSHINT16", 25 | size: 2, 26 | }, 27 | ), 28 | ( 29 | 0x02, 30 | OpcodeData { 31 | name: "PUSHINT32", 32 | size: 4, 33 | }, 34 | ), 35 | ( 36 | 0x03, 37 | OpcodeData { 38 | name: "PUSHINT64", 39 | size: 8, 40 | }, 41 | ), 42 | ( 43 | 0x04, 44 | OpcodeData { 45 | name: "PUSHINT128", 46 | size: 16, 47 | }, 48 | ), 49 | ( 50 | 0x05, 51 | OpcodeData { 52 | name: "PUSHINT256", 53 | size: 32, 54 | }, 55 | ), 56 | ( 57 | 0x08, 58 | OpcodeData { 59 | name: "PUSHT", 60 | size: 0, 61 | }, 62 | ), 63 | ( 64 | 0x09, 65 | OpcodeData { 66 | name: "PUSHF", 67 | size: 0, 68 | }, 69 | ), 70 | ( 71 | 0x0a, 72 | OpcodeData { 73 | name: "PUSHA", 74 | size: 4, 75 | }, 76 | ), 77 | ( 78 | 0x0b, 79 | OpcodeData { 80 | name: "PUSHNULL", 81 | size: 0, 82 | }, 83 | ), 84 | ( 85 | 0x0c, 86 | OpcodeData { 87 | name: "PUSHDATA1", 88 | size: 1, 89 | }, 90 | ), 91 | ( 92 | 0x0d, 93 | OpcodeData { 94 | name: "PUSHDATA2", 95 | size: 2, 96 | }, 97 | ), 98 | ( 99 | 0x0e, 100 | OpcodeData { 101 | name: "PUSHDATA4", 102 | size: 4, 103 | }, 104 | ), 105 | ( 106 | 0x0f, 107 | OpcodeData { 108 | name: "PUSHM1", 109 | size: 0, 110 | }, 111 | ), 112 | ( 113 | 0x10, 114 | OpcodeData { 115 | name: "PUSH0", 116 | size: 0, 117 | }, 118 | ), 119 | ( 120 | 0x11, 121 | OpcodeData { 122 | name: "PUSH1", 123 | size: 0, 124 | }, 125 | ), 126 | ( 127 | 0x12, 128 | OpcodeData { 129 | name: "PUSH2", 130 | size: 0, 131 | }, 132 | ), 133 | ( 134 | 0x13, 135 | OpcodeData { 136 | name: "PUSH3", 137 | size: 0, 138 | }, 139 | ), 140 | ( 141 | 0x14, 142 | OpcodeData { 143 | name: "PUSH4", 144 | size: 0, 145 | }, 146 | ), 147 | ( 148 | 0x15, 149 | OpcodeData { 150 | name: "PUSH5", 151 | size: 0, 152 | }, 153 | ), 154 | ( 155 | 0x16, 156 | OpcodeData { 157 | name: "PUSH6", 158 | size: 0, 159 | }, 160 | ), 161 | ( 162 | 0x17, 163 | OpcodeData { 164 | name: "PUSH7", 165 | size: 0, 166 | }, 167 | ), 168 | ( 169 | 0x18, 170 | OpcodeData { 171 | name: "PUSH8", 172 | size: 0, 173 | }, 174 | ), 175 | ( 176 | 0x19, 177 | OpcodeData { 178 | name: "PUSH9", 179 | size: 0, 180 | }, 181 | ), 182 | ( 183 | 0x1a, 184 | OpcodeData { 185 | name: "PUSH10", 186 | size: 0, 187 | }, 188 | ), 189 | ( 190 | 0x1b, 191 | OpcodeData { 192 | name: "PUSH11", 193 | size: 0, 194 | }, 195 | ), 196 | ( 197 | 0x1c, 198 | OpcodeData { 199 | name: "PUSH12", 200 | size: 0, 201 | }, 202 | ), 203 | ( 204 | 0x1d, 205 | OpcodeData { 206 | name: "PUSH13", 207 | size: 0, 208 | }, 209 | ), 210 | ( 211 | 0x1e, 212 | OpcodeData { 213 | name: "PUSH14", 214 | size: 0, 215 | }, 216 | ), 217 | ( 218 | 0x1f, 219 | OpcodeData { 220 | name: "PUSH15", 221 | size: 0, 222 | }, 223 | ), 224 | ( 225 | 0x20, 226 | OpcodeData { 227 | name: "PUSH16", 228 | size: 0, 229 | }, 230 | ), 231 | ( 232 | 0x21, 233 | OpcodeData { 234 | name: "NOP", 235 | size: 0, 236 | }, 237 | ), 238 | ( 239 | 0x22, 240 | OpcodeData { 241 | name: "JMP", 242 | size: 1, 243 | }, 244 | ), 245 | ( 246 | 0x23, 247 | OpcodeData { 248 | name: "JMP_L", 249 | size: 4, 250 | }, 251 | ), 252 | ( 253 | 0x24, 254 | OpcodeData { 255 | name: "JMPIF", 256 | size: 1, 257 | }, 258 | ), 259 | ( 260 | 0x25, 261 | OpcodeData { 262 | name: "JMPIF_L", 263 | size: 4, 264 | }, 265 | ), 266 | ( 267 | 0x26, 268 | OpcodeData { 269 | name: "JMPIFNOT", 270 | size: 1, 271 | }, 272 | ), 273 | ( 274 | 0x27, 275 | OpcodeData { 276 | name: "JMPIFNOT_L", 277 | size: 4, 278 | }, 279 | ), 280 | ( 281 | 0x28, 282 | OpcodeData { 283 | name: "JMPEQ", 284 | size: 1, 285 | }, 286 | ), 287 | ( 288 | 0x29, 289 | OpcodeData { 290 | name: "JMPEQ_L", 291 | size: 4, 292 | }, 293 | ), 294 | ( 295 | 0x2a, 296 | OpcodeData { 297 | name: "JMPNE", 298 | size: 1, 299 | }, 300 | ), 301 | ( 302 | 0x2b, 303 | OpcodeData { 304 | name: "JMPNE_L", 305 | size: 4, 306 | }, 307 | ), 308 | ( 309 | 0x2c, 310 | OpcodeData { 311 | name: "JMPGT", 312 | size: 1, 313 | }, 314 | ), 315 | ( 316 | 0x2d, 317 | OpcodeData { 318 | name: "JMPGT_L", 319 | size: 4, 320 | }, 321 | ), 322 | ( 323 | 0x2e, 324 | OpcodeData { 325 | name: "JMPGE", 326 | size: 1, 327 | }, 328 | ), 329 | ( 330 | 0x2f, 331 | OpcodeData { 332 | name: "JMPGE_L", 333 | size: 4, 334 | }, 335 | ), 336 | ( 337 | 0x30, 338 | OpcodeData { 339 | name: "JMPLT", 340 | size: 1, 341 | }, 342 | ), 343 | ( 344 | 0x31, 345 | OpcodeData { 346 | name: "JMPLT_L", 347 | size: 4, 348 | }, 349 | ), 350 | ( 351 | 0x32, 352 | OpcodeData { 353 | name: "JMPLE", 354 | size: 1, 355 | }, 356 | ), 357 | ( 358 | 0x33, 359 | OpcodeData { 360 | name: "JMPLE_L", 361 | size: 4, 362 | }, 363 | ), 364 | ( 365 | 0x34, 366 | OpcodeData { 367 | name: "CALL", 368 | size: 1, 369 | }, 370 | ), 371 | ( 372 | 0x35, 373 | OpcodeData { 374 | name: "CALL_L", 375 | size: 4, 376 | }, 377 | ), 378 | ( 379 | 0x36, 380 | OpcodeData { 381 | name: "CALLA", 382 | size: 0, 383 | }, 384 | ), 385 | ( 386 | 0x37, 387 | OpcodeData { 388 | name: "CALLT", 389 | size: 2, 390 | }, 391 | ), 392 | ( 393 | 0x38, 394 | OpcodeData { 395 | name: "ABORT", 396 | size: 0, 397 | }, 398 | ), 399 | ( 400 | 0x39, 401 | OpcodeData { 402 | name: "ASSERT", 403 | size: 0, 404 | }, 405 | ), 406 | ( 407 | 0x3a, 408 | OpcodeData { 409 | name: "THROW", 410 | size: 0, 411 | }, 412 | ), 413 | ( 414 | 0x3b, 415 | OpcodeData { 416 | name: "TRY", 417 | size: 2, 418 | }, 419 | ), 420 | ( 421 | 0x3c, 422 | OpcodeData { 423 | name: "TRY_L", 424 | size: 8, 425 | }, 426 | ), 427 | ( 428 | 0x3d, 429 | OpcodeData { 430 | name: "ENDTRY", 431 | size: 1, 432 | }, 433 | ), 434 | ( 435 | 0x3e, 436 | OpcodeData { 437 | name: "ENDTRY_L", 438 | size: 4, 439 | }, 440 | ), 441 | ( 442 | 0x3f, 443 | OpcodeData { 444 | name: "ENDFINALLY", 445 | size: 0, 446 | }, 447 | ), 448 | ( 449 | 0x40, 450 | OpcodeData { 451 | name: "RET", 452 | size: 0, 453 | }, 454 | ), 455 | ( 456 | 0x41, 457 | OpcodeData { 458 | name: "SYSCALL", 459 | size: 4, 460 | }, 461 | ), 462 | ( 463 | 0x43, 464 | OpcodeData { 465 | name: "DEPTH", 466 | size: 0, 467 | }, 468 | ), 469 | ( 470 | 0x45, 471 | OpcodeData { 472 | name: "DROP", 473 | size: 0, 474 | }, 475 | ), 476 | ( 477 | 0x46, 478 | OpcodeData { 479 | name: "NIP", 480 | size: 0, 481 | }, 482 | ), 483 | ( 484 | 0x48, 485 | OpcodeData { 486 | name: "XDROP", 487 | size: 0, 488 | }, 489 | ), 490 | ( 491 | 0x49, 492 | OpcodeData { 493 | name: "CLEAR", 494 | size: 0, 495 | }, 496 | ), 497 | ( 498 | 0x4a, 499 | OpcodeData { 500 | name: "DUP", 501 | size: 0, 502 | }, 503 | ), 504 | ( 505 | 0x4b, 506 | OpcodeData { 507 | name: "OVER", 508 | size: 0, 509 | }, 510 | ), 511 | ( 512 | 0x4d, 513 | OpcodeData { 514 | name: "PICK", 515 | size: 0, 516 | }, 517 | ), 518 | ( 519 | 0x4e, 520 | OpcodeData { 521 | name: "TUCK", 522 | size: 0, 523 | }, 524 | ), 525 | ( 526 | 0x50, 527 | OpcodeData { 528 | name: "SWAP", 529 | size: 0, 530 | }, 531 | ), 532 | ( 533 | 0x51, 534 | OpcodeData { 535 | name: "ROT", 536 | size: 0, 537 | }, 538 | ), 539 | ( 540 | 0x52, 541 | OpcodeData { 542 | name: "ROLL", 543 | size: 0, 544 | }, 545 | ), 546 | ( 547 | 0x53, 548 | OpcodeData { 549 | name: "REVERSE3", 550 | size: 0, 551 | }, 552 | ), 553 | ( 554 | 0x54, 555 | OpcodeData { 556 | name: "REVERSE4", 557 | size: 0, 558 | }, 559 | ), 560 | ( 561 | 0x55, 562 | OpcodeData { 563 | name: "REVERSEN", 564 | size: 0, 565 | }, 566 | ), 567 | ( 568 | 0x56, 569 | OpcodeData { 570 | name: "INITSSLOT", 571 | size: 1, 572 | }, 573 | ), 574 | ( 575 | 0x57, 576 | OpcodeData { 577 | name: "INITSLOT", 578 | size: 2, 579 | }, 580 | ), 581 | ] 582 | .into() 583 | }); 584 | 585 | pub const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; 586 | 587 | pub fn base64_to_hex(encoded: &str) -> String { 588 | let bytes = base64::decode(encoded).unwrap(); 589 | hex::encode(bytes) 590 | } 591 | 592 | pub fn hex_to_base64(hex: &str) -> String { 593 | let bytes = hex::decode(hex).unwrap(); 594 | base64::encode(&bytes) 595 | } 596 | 597 | pub fn hex_decode(encoded: &str) -> Vec { 598 | hex::decode(encoded).unwrap() 599 | } 600 | 601 | pub fn base64_to_script_hash(encoded: &str) -> String { 602 | let hex = base64_to_hex(encoded); 603 | let mut value = hex::decode(hex).unwrap(); 604 | value.reverse(); 605 | format!("0x{}", hex::encode(value)) 606 | } 607 | 608 | pub fn scripthash_to_address(script_hash: &str) -> String { 609 | let script_hash = hex::decode(script_hash).unwrap(); 610 | 611 | let mut addr = [0u8; 25]; 612 | addr[0] = 53; 613 | addr[1..21].copy_from_slice(&script_hash); 614 | 615 | let sum = &checksum(&addr[0..21])[0..4]; 616 | addr[21..25].copy_from_slice(sum); 617 | 618 | bytes_to_base58(&addr) 619 | } 620 | 621 | pub fn base64_to_address(encoded: &str) -> String { 622 | let script_hash = base64_to_hex(encoded); 623 | scripthash_to_address(&script_hash) 624 | } 625 | 626 | pub fn checksum(data: &[u8]) -> Vec { 627 | Sha256::digest(Sha256::digest(data)).to_vec() 628 | } 629 | 630 | #[allow(dead_code)] 631 | pub fn reverse_hex(hex: &str) -> String { 632 | let mut value = hex::decode(hex).unwrap(); 633 | value.reverse(); 634 | 635 | hex::encode(value) 636 | } 637 | 638 | pub fn bytes_to_base58(bytes: &[u8]) -> String { 639 | let zcount = bytes.iter().take_while(|x| **x == 0).count(); 640 | let size = (bytes.len() - zcount) * 138 / 100 + 1; 641 | let mut buffer = vec![0u8; size]; 642 | 643 | let mut i = zcount; 644 | let mut high = size - 1; 645 | 646 | while i < bytes.len() { 647 | let mut carry = bytes[i] as u32; 648 | let mut j = size - 1; 649 | 650 | while j > high || carry != 0 { 651 | carry += 256 * buffer[j] as u32; 652 | buffer[j] = (carry % 58) as u8; 653 | carry /= 58; 654 | 655 | if j > 0 { 656 | j = j.saturating_sub(1); 657 | } 658 | } 659 | 660 | i += 1; 661 | high = j; 662 | } 663 | 664 | let mut j = buffer.iter().take_while(|x| **x == 0).count(); 665 | 666 | let mut result = String::new(); 667 | for _ in 0..zcount { 668 | result.push('1'); 669 | } 670 | 671 | while j < size { 672 | result.push(ALPHABET[buffer[j] as usize] as char); 673 | j += 1; 674 | } 675 | 676 | result 677 | } 678 | 679 | // added this then realized I didn't need it... oh well, one day maybe 680 | #[allow(clippy::same_item_push)] 681 | pub fn base58_to_bytes(base58: &str) -> Vec { 682 | let zcount = base58.chars().take_while(|x| *x == '1').count(); 683 | let size = (base58.len() - zcount) * 733 / 1000 + 1; 684 | let mut buffer = vec![0u8; size]; 685 | 686 | let mut i = zcount; 687 | let mut high = size - 1; 688 | 689 | while i < base58.len() { 690 | let mut carry = ALPHABET 691 | .iter() 692 | .position(|&x| x == base58.as_bytes()[i]) 693 | .unwrap() as u32; 694 | let mut j = size - 1; 695 | 696 | while j > high || carry != 0 { 697 | carry += 58 * buffer[j] as u32; 698 | buffer[j] = (carry % 256) as u8; 699 | carry /= 256; 700 | 701 | if j > 0 { 702 | j = j.saturating_sub(1); 703 | } 704 | } 705 | 706 | i += 1; 707 | high = j; 708 | } 709 | 710 | let mut j = buffer.iter().take_while(|x| **x == 0).count(); 711 | 712 | let mut result = Vec::new(); 713 | for _ in 0..zcount { 714 | result.push(0); 715 | } 716 | 717 | while j < size { 718 | result.push(buffer[j]); 719 | j += 1; 720 | } 721 | 722 | result 723 | } 724 | 725 | pub fn address_to_base64(address: &str) -> String { 726 | let bytes = base58_to_bytes(address); 727 | base64::encode(&bytes[1..21]) 728 | } 729 | 730 | pub fn neo3_disassemble(base64_encoded_script: &str) -> String { 731 | let mut out = String::new(); 732 | let script = base64::decode(base64_encoded_script).expect("Invalid base64 encoding"); 733 | 734 | let interopmethod: HashMap = HashMap::new(); 735 | 736 | let mut ip = 0; 737 | 738 | while ip < script.len() { 739 | let opcode = script[ip]; 740 | if let Some(opcodedata) = OPCODETABLE.get(&opcode) { 741 | let inst = &opcodedata.name; 742 | 743 | if inst == &"SYSCALL" { 744 | let hash = u32::from_le_bytes([ 745 | script[ip + 1], 746 | script[ip + 2], 747 | script[ip + 3], 748 | script[ip + 4], 749 | ]); 750 | let interop_name = if let Some(name) = interopmethod.get(&hash) { 751 | name.clone() 752 | } else { 753 | hash.to_string() 754 | }; 755 | out.push_str(&format!("{} {}\n", inst, interop_name)); 756 | ip += 4; 757 | } else if opcodedata.size == 0 { 758 | out.push_str(&format!("{}\n", inst)); 759 | } else { 760 | if inst == &"PUSHDATA1" || inst == &"PUSHDATA2" || inst == &"PUSHDATA4" { 761 | let data_size = match opcodedata.size { 762 | 1 => script[ip + 1] as usize, 763 | 2 => u16::from_le_bytes([script[ip + 1], script[ip + 2]]) as usize, 764 | 4 => u32::from_le_bytes([ 765 | script[ip + 1], 766 | script[ip + 2], 767 | script[ip + 3], 768 | script[ip + 4], 769 | ]) as usize, 770 | _ => { 771 | out.push_str(&format!( 772 | "SOMEBODY MESSED UP THE PUSHDATA SIZE for {} at index {} (size {})\n", 773 | opcodedata.name, ip, opcodedata.size 774 | )); 775 | return out; 776 | } 777 | }; 778 | 779 | let data_start_idx = ip + opcodedata.size + 1; 780 | let data = &script[data_start_idx..data_start_idx + data_size]; 781 | out.push_str(&format!("{} {}\n", inst, hex::encode(data))); 782 | ip += opcodedata.size + data_size; 783 | } else { 784 | let data = &script[ip + 1..ip + 1 + opcodedata.size]; 785 | out.push_str(&format!("{} {}\n", inst, hex::encode(data))); 786 | ip += opcodedata.size; 787 | } 788 | } 789 | } else { 790 | out.push_str(&format!("INVALID OPCODE {}\n", opcode)); 791 | } 792 | ip += 1; 793 | } 794 | out 795 | } 796 | -------------------------------------------------------------------------------- /api/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "actix-codec" 7 | version = "0.5.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" 10 | dependencies = [ 11 | "bitflags", 12 | "bytes", 13 | "futures-core", 14 | "futures-sink", 15 | "log", 16 | "memchr", 17 | "pin-project-lite", 18 | "tokio", 19 | "tokio-util", 20 | ] 21 | 22 | [[package]] 23 | name = "actix-http" 24 | version = "3.2.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "0c83abf9903e1f0ad9973cc4f7b9767fd5a03a583f51a5b7a339e07987cd2724" 27 | dependencies = [ 28 | "actix-codec", 29 | "actix-rt", 30 | "actix-service", 31 | "actix-utils", 32 | "ahash", 33 | "base64", 34 | "bitflags", 35 | "brotli", 36 | "bytes", 37 | "bytestring", 38 | "derive_more", 39 | "encoding_rs", 40 | "flate2", 41 | "futures-core", 42 | "h2", 43 | "http", 44 | "httparse", 45 | "httpdate", 46 | "itoa", 47 | "language-tags", 48 | "local-channel", 49 | "mime", 50 | "percent-encoding", 51 | "pin-project-lite", 52 | "rand", 53 | "sha1", 54 | "smallvec", 55 | "tracing", 56 | "zstd", 57 | ] 58 | 59 | [[package]] 60 | name = "actix-macros" 61 | version = "0.2.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" 64 | dependencies = [ 65 | "quote", 66 | "syn", 67 | ] 68 | 69 | [[package]] 70 | name = "actix-router" 71 | version = "0.5.1" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" 74 | dependencies = [ 75 | "bytestring", 76 | "http", 77 | "regex", 78 | "serde", 79 | "tracing", 80 | ] 81 | 82 | [[package]] 83 | name = "actix-rt" 84 | version = "2.7.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" 87 | dependencies = [ 88 | "futures-core", 89 | "tokio", 90 | ] 91 | 92 | [[package]] 93 | name = "actix-server" 94 | version = "2.1.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" 97 | dependencies = [ 98 | "actix-rt", 99 | "actix-service", 100 | "actix-utils", 101 | "futures-core", 102 | "futures-util", 103 | "mio", 104 | "num_cpus", 105 | "socket2", 106 | "tokio", 107 | "tracing", 108 | ] 109 | 110 | [[package]] 111 | name = "actix-service" 112 | version = "2.0.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 115 | dependencies = [ 116 | "futures-core", 117 | "paste", 118 | "pin-project-lite", 119 | ] 120 | 121 | [[package]] 122 | name = "actix-utils" 123 | version = "3.0.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 126 | dependencies = [ 127 | "local-waker", 128 | "pin-project-lite", 129 | ] 130 | 131 | [[package]] 132 | name = "actix-web" 133 | version = "4.2.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "d48f7b6534e06c7bfc72ee91db7917d4af6afe23e7d223b51e68fffbb21e96b9" 136 | dependencies = [ 137 | "actix-codec", 138 | "actix-http", 139 | "actix-macros", 140 | "actix-router", 141 | "actix-rt", 142 | "actix-server", 143 | "actix-service", 144 | "actix-utils", 145 | "actix-web-codegen", 146 | "ahash", 147 | "bytes", 148 | "bytestring", 149 | "cfg-if", 150 | "cookie", 151 | "derive_more", 152 | "encoding_rs", 153 | "futures-core", 154 | "futures-util", 155 | "http", 156 | "itoa", 157 | "language-tags", 158 | "log", 159 | "mime", 160 | "once_cell", 161 | "pin-project-lite", 162 | "regex", 163 | "serde", 164 | "serde_json", 165 | "serde_urlencoded", 166 | "smallvec", 167 | "socket2", 168 | "time", 169 | "url", 170 | ] 171 | 172 | [[package]] 173 | name = "actix-web-codegen" 174 | version = "4.1.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" 177 | dependencies = [ 178 | "actix-router", 179 | "proc-macro2", 180 | "quote", 181 | "syn", 182 | ] 183 | 184 | [[package]] 185 | name = "adler" 186 | version = "1.0.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 189 | 190 | [[package]] 191 | name = "ahash" 192 | version = "0.7.6" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 195 | dependencies = [ 196 | "getrandom", 197 | "once_cell", 198 | "version_check", 199 | ] 200 | 201 | [[package]] 202 | name = "aho-corasick" 203 | version = "0.7.20" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 206 | dependencies = [ 207 | "memchr", 208 | ] 209 | 210 | [[package]] 211 | name = "alloc-no-stdlib" 212 | version = "2.0.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 215 | 216 | [[package]] 217 | name = "alloc-stdlib" 218 | version = "0.2.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 221 | dependencies = [ 222 | "alloc-no-stdlib", 223 | ] 224 | 225 | [[package]] 226 | name = "autocfg" 227 | version = "1.1.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 230 | 231 | [[package]] 232 | name = "base64" 233 | version = "0.13.1" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 236 | 237 | [[package]] 238 | name = "bitflags" 239 | version = "1.3.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 242 | 243 | [[package]] 244 | name = "block-buffer" 245 | version = "0.10.3" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" 248 | dependencies = [ 249 | "generic-array", 250 | ] 251 | 252 | [[package]] 253 | name = "brotli" 254 | version = "3.3.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" 257 | dependencies = [ 258 | "alloc-no-stdlib", 259 | "alloc-stdlib", 260 | "brotli-decompressor", 261 | ] 262 | 263 | [[package]] 264 | name = "brotli-decompressor" 265 | version = "2.3.2" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" 268 | dependencies = [ 269 | "alloc-no-stdlib", 270 | "alloc-stdlib", 271 | ] 272 | 273 | [[package]] 274 | name = "bytes" 275 | version = "1.3.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 278 | 279 | [[package]] 280 | name = "bytestring" 281 | version = "1.2.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "f7f83e57d9154148e355404702e2694463241880b939570d7c97c014da7a69a1" 284 | dependencies = [ 285 | "bytes", 286 | ] 287 | 288 | [[package]] 289 | name = "cc" 290 | version = "1.0.78" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 293 | dependencies = [ 294 | "jobserver", 295 | ] 296 | 297 | [[package]] 298 | name = "cfg-if" 299 | version = "1.0.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 302 | 303 | [[package]] 304 | name = "convert_case" 305 | version = "0.4.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 308 | 309 | [[package]] 310 | name = "cookie" 311 | version = "0.16.2" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 314 | dependencies = [ 315 | "percent-encoding", 316 | "time", 317 | "version_check", 318 | ] 319 | 320 | [[package]] 321 | name = "cpufeatures" 322 | version = "0.2.5" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 325 | dependencies = [ 326 | "libc", 327 | ] 328 | 329 | [[package]] 330 | name = "crc32fast" 331 | version = "1.3.2" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 334 | dependencies = [ 335 | "cfg-if", 336 | ] 337 | 338 | [[package]] 339 | name = "crypto-common" 340 | version = "0.1.6" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 343 | dependencies = [ 344 | "generic-array", 345 | "typenum", 346 | ] 347 | 348 | [[package]] 349 | name = "derive_more" 350 | version = "0.99.17" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 353 | dependencies = [ 354 | "convert_case", 355 | "proc-macro2", 356 | "quote", 357 | "rustc_version", 358 | "syn", 359 | ] 360 | 361 | [[package]] 362 | name = "digest" 363 | version = "0.10.6" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 366 | dependencies = [ 367 | "block-buffer", 368 | "crypto-common", 369 | ] 370 | 371 | [[package]] 372 | name = "encoding_rs" 373 | version = "0.8.31" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 376 | dependencies = [ 377 | "cfg-if", 378 | ] 379 | 380 | [[package]] 381 | name = "fallible-iterator" 382 | version = "0.2.0" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 385 | 386 | [[package]] 387 | name = "fallible-streaming-iterator" 388 | version = "0.1.9" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 391 | 392 | [[package]] 393 | name = "flate2" 394 | version = "1.0.25" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 397 | dependencies = [ 398 | "crc32fast", 399 | "miniz_oxide", 400 | ] 401 | 402 | [[package]] 403 | name = "fnv" 404 | version = "1.0.7" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 407 | 408 | [[package]] 409 | name = "form_urlencoded" 410 | version = "1.1.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 413 | dependencies = [ 414 | "percent-encoding", 415 | ] 416 | 417 | [[package]] 418 | name = "futures-core" 419 | version = "0.3.25" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 422 | 423 | [[package]] 424 | name = "futures-sink" 425 | version = "0.3.25" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" 428 | 429 | [[package]] 430 | name = "futures-task" 431 | version = "0.3.25" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" 434 | 435 | [[package]] 436 | name = "futures-util" 437 | version = "0.3.25" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" 440 | dependencies = [ 441 | "futures-core", 442 | "futures-task", 443 | "pin-project-lite", 444 | "pin-utils", 445 | ] 446 | 447 | [[package]] 448 | name = "generic-array" 449 | version = "0.14.6" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 452 | dependencies = [ 453 | "typenum", 454 | "version_check", 455 | ] 456 | 457 | [[package]] 458 | name = "getrandom" 459 | version = "0.2.8" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 462 | dependencies = [ 463 | "cfg-if", 464 | "libc", 465 | "wasi", 466 | ] 467 | 468 | [[package]] 469 | name = "h2" 470 | version = "0.3.15" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" 473 | dependencies = [ 474 | "bytes", 475 | "fnv", 476 | "futures-core", 477 | "futures-sink", 478 | "futures-util", 479 | "http", 480 | "indexmap", 481 | "slab", 482 | "tokio", 483 | "tokio-util", 484 | "tracing", 485 | ] 486 | 487 | [[package]] 488 | name = "hashbrown" 489 | version = "0.12.3" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 492 | dependencies = [ 493 | "ahash", 494 | ] 495 | 496 | [[package]] 497 | name = "hashlink" 498 | version = "0.8.1" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" 501 | dependencies = [ 502 | "hashbrown", 503 | ] 504 | 505 | [[package]] 506 | name = "hermit-abi" 507 | version = "0.2.6" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 510 | dependencies = [ 511 | "libc", 512 | ] 513 | 514 | [[package]] 515 | name = "http" 516 | version = "0.2.8" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 519 | dependencies = [ 520 | "bytes", 521 | "fnv", 522 | "itoa", 523 | ] 524 | 525 | [[package]] 526 | name = "httparse" 527 | version = "1.8.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 530 | 531 | [[package]] 532 | name = "httpdate" 533 | version = "1.0.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 536 | 537 | [[package]] 538 | name = "idna" 539 | version = "0.3.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 542 | dependencies = [ 543 | "unicode-bidi", 544 | "unicode-normalization", 545 | ] 546 | 547 | [[package]] 548 | name = "indexmap" 549 | version = "1.9.2" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 552 | dependencies = [ 553 | "autocfg", 554 | "hashbrown", 555 | ] 556 | 557 | [[package]] 558 | name = "itoa" 559 | version = "1.0.5" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 562 | 563 | [[package]] 564 | name = "jobserver" 565 | version = "0.1.25" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" 568 | dependencies = [ 569 | "libc", 570 | ] 571 | 572 | [[package]] 573 | name = "language-tags" 574 | version = "0.3.2" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 577 | 578 | [[package]] 579 | name = "libc" 580 | version = "0.2.139" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 583 | 584 | [[package]] 585 | name = "libsqlite3-sys" 586 | version = "0.25.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" 589 | dependencies = [ 590 | "cc", 591 | "pkg-config", 592 | "vcpkg", 593 | ] 594 | 595 | [[package]] 596 | name = "local-channel" 597 | version = "0.1.3" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" 600 | dependencies = [ 601 | "futures-core", 602 | "futures-sink", 603 | "futures-util", 604 | "local-waker", 605 | ] 606 | 607 | [[package]] 608 | name = "local-waker" 609 | version = "0.1.3" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" 612 | 613 | [[package]] 614 | name = "lock_api" 615 | version = "0.4.9" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 618 | dependencies = [ 619 | "autocfg", 620 | "scopeguard", 621 | ] 622 | 623 | [[package]] 624 | name = "log" 625 | version = "0.4.17" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 628 | dependencies = [ 629 | "cfg-if", 630 | ] 631 | 632 | [[package]] 633 | name = "memchr" 634 | version = "2.5.0" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 637 | 638 | [[package]] 639 | name = "mime" 640 | version = "0.3.16" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 643 | 644 | [[package]] 645 | name = "miniz_oxide" 646 | version = "0.6.2" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 649 | dependencies = [ 650 | "adler", 651 | ] 652 | 653 | [[package]] 654 | name = "mio" 655 | version = "0.8.5" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 658 | dependencies = [ 659 | "libc", 660 | "log", 661 | "wasi", 662 | "windows-sys", 663 | ] 664 | 665 | [[package]] 666 | name = "num_cpus" 667 | version = "1.15.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 670 | dependencies = [ 671 | "hermit-abi", 672 | "libc", 673 | ] 674 | 675 | [[package]] 676 | name = "once_cell" 677 | version = "1.17.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 680 | 681 | [[package]] 682 | name = "parking_lot" 683 | version = "0.12.1" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 686 | dependencies = [ 687 | "lock_api", 688 | "parking_lot_core", 689 | ] 690 | 691 | [[package]] 692 | name = "parking_lot_core" 693 | version = "0.9.5" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" 696 | dependencies = [ 697 | "cfg-if", 698 | "libc", 699 | "redox_syscall", 700 | "smallvec", 701 | "windows-sys", 702 | ] 703 | 704 | [[package]] 705 | name = "paste" 706 | version = "1.0.11" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" 709 | 710 | [[package]] 711 | name = "percent-encoding" 712 | version = "2.2.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 715 | 716 | [[package]] 717 | name = "pin-project-lite" 718 | version = "0.2.9" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 721 | 722 | [[package]] 723 | name = "pin-utils" 724 | version = "0.1.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 727 | 728 | [[package]] 729 | name = "pkg-config" 730 | version = "0.3.26" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 733 | 734 | [[package]] 735 | name = "ppv-lite86" 736 | version = "0.2.17" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 739 | 740 | [[package]] 741 | name = "proc-macro2" 742 | version = "1.0.49" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 745 | dependencies = [ 746 | "unicode-ident", 747 | ] 748 | 749 | [[package]] 750 | name = "quote" 751 | version = "1.0.23" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 754 | dependencies = [ 755 | "proc-macro2", 756 | ] 757 | 758 | [[package]] 759 | name = "r2d2" 760 | version = "0.8.10" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" 763 | dependencies = [ 764 | "log", 765 | "parking_lot", 766 | "scheduled-thread-pool", 767 | ] 768 | 769 | [[package]] 770 | name = "r2d2_sqlite" 771 | version = "0.21.0" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "b4f5d0337e99cd5cacd91ffc326c6cc9d8078def459df560c4f9bf9ba4a51034" 774 | dependencies = [ 775 | "r2d2", 776 | "rusqlite", 777 | ] 778 | 779 | [[package]] 780 | name = "rand" 781 | version = "0.8.5" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 784 | dependencies = [ 785 | "libc", 786 | "rand_chacha", 787 | "rand_core", 788 | ] 789 | 790 | [[package]] 791 | name = "rand_chacha" 792 | version = "0.3.1" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 795 | dependencies = [ 796 | "ppv-lite86", 797 | "rand_core", 798 | ] 799 | 800 | [[package]] 801 | name = "rand_core" 802 | version = "0.6.4" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 805 | dependencies = [ 806 | "getrandom", 807 | ] 808 | 809 | [[package]] 810 | name = "redox_syscall" 811 | version = "0.2.16" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 814 | dependencies = [ 815 | "bitflags", 816 | ] 817 | 818 | [[package]] 819 | name = "regex" 820 | version = "1.7.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 823 | dependencies = [ 824 | "aho-corasick", 825 | "memchr", 826 | "regex-syntax", 827 | ] 828 | 829 | [[package]] 830 | name = "regex-syntax" 831 | version = "0.6.28" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 834 | 835 | [[package]] 836 | name = "rusqlite" 837 | version = "0.28.0" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" 840 | dependencies = [ 841 | "bitflags", 842 | "fallible-iterator", 843 | "fallible-streaming-iterator", 844 | "hashlink", 845 | "libsqlite3-sys", 846 | "serde_json", 847 | "smallvec", 848 | ] 849 | 850 | [[package]] 851 | name = "rustc_version" 852 | version = "0.4.0" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 855 | dependencies = [ 856 | "semver", 857 | ] 858 | 859 | [[package]] 860 | name = "ryu" 861 | version = "1.0.12" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 864 | 865 | [[package]] 866 | name = "scheduled-thread-pool" 867 | version = "0.2.6" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf" 870 | dependencies = [ 871 | "parking_lot", 872 | ] 873 | 874 | [[package]] 875 | name = "scopeguard" 876 | version = "1.1.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 879 | 880 | [[package]] 881 | name = "semver" 882 | version = "1.0.16" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" 885 | 886 | [[package]] 887 | name = "serde" 888 | version = "1.0.152" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 891 | dependencies = [ 892 | "serde_derive", 893 | ] 894 | 895 | [[package]] 896 | name = "serde_derive" 897 | version = "1.0.152" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 900 | dependencies = [ 901 | "proc-macro2", 902 | "quote", 903 | "syn", 904 | ] 905 | 906 | [[package]] 907 | name = "serde_json" 908 | version = "1.0.91" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 911 | dependencies = [ 912 | "itoa", 913 | "ryu", 914 | "serde", 915 | ] 916 | 917 | [[package]] 918 | name = "serde_urlencoded" 919 | version = "0.7.1" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 922 | dependencies = [ 923 | "form_urlencoded", 924 | "itoa", 925 | "ryu", 926 | "serde", 927 | ] 928 | 929 | [[package]] 930 | name = "sha1" 931 | version = "0.10.5" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 934 | dependencies = [ 935 | "cfg-if", 936 | "cpufeatures", 937 | "digest", 938 | ] 939 | 940 | [[package]] 941 | name = "api" 942 | version = "0.1.0" 943 | dependencies = [ 944 | "actix-web", 945 | "r2d2", 946 | "r2d2_sqlite", 947 | "rusqlite", 948 | "serde", 949 | "serde_json", 950 | ] 951 | 952 | [[package]] 953 | name = "signal-hook-registry" 954 | version = "1.4.0" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 957 | dependencies = [ 958 | "libc", 959 | ] 960 | 961 | [[package]] 962 | name = "slab" 963 | version = "0.4.7" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 966 | dependencies = [ 967 | "autocfg", 968 | ] 969 | 970 | [[package]] 971 | name = "smallvec" 972 | version = "1.10.0" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 975 | 976 | [[package]] 977 | name = "socket2" 978 | version = "0.4.7" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 981 | dependencies = [ 982 | "libc", 983 | "winapi", 984 | ] 985 | 986 | [[package]] 987 | name = "syn" 988 | version = "1.0.107" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 991 | dependencies = [ 992 | "proc-macro2", 993 | "quote", 994 | "unicode-ident", 995 | ] 996 | 997 | [[package]] 998 | name = "time" 999 | version = "0.3.17" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" 1002 | dependencies = [ 1003 | "itoa", 1004 | "serde", 1005 | "time-core", 1006 | "time-macros", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "time-core" 1011 | version = "0.1.0" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 1014 | 1015 | [[package]] 1016 | name = "time-macros" 1017 | version = "0.2.6" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" 1020 | dependencies = [ 1021 | "time-core", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "tinyvec" 1026 | version = "1.6.0" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1029 | dependencies = [ 1030 | "tinyvec_macros", 1031 | ] 1032 | 1033 | [[package]] 1034 | name = "tinyvec_macros" 1035 | version = "0.1.0" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1038 | 1039 | [[package]] 1040 | name = "tokio" 1041 | version = "1.23.0" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" 1044 | dependencies = [ 1045 | "autocfg", 1046 | "bytes", 1047 | "libc", 1048 | "memchr", 1049 | "mio", 1050 | "parking_lot", 1051 | "pin-project-lite", 1052 | "signal-hook-registry", 1053 | "socket2", 1054 | "windows-sys", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "tokio-util" 1059 | version = "0.7.4" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 1062 | dependencies = [ 1063 | "bytes", 1064 | "futures-core", 1065 | "futures-sink", 1066 | "pin-project-lite", 1067 | "tokio", 1068 | "tracing", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "tracing" 1073 | version = "0.1.37" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1076 | dependencies = [ 1077 | "cfg-if", 1078 | "log", 1079 | "pin-project-lite", 1080 | "tracing-core", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "tracing-core" 1085 | version = "0.1.30" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1088 | dependencies = [ 1089 | "once_cell", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "typenum" 1094 | version = "1.16.0" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1097 | 1098 | [[package]] 1099 | name = "unicode-bidi" 1100 | version = "0.3.8" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1103 | 1104 | [[package]] 1105 | name = "unicode-ident" 1106 | version = "1.0.6" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 1109 | 1110 | [[package]] 1111 | name = "unicode-normalization" 1112 | version = "0.1.22" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1115 | dependencies = [ 1116 | "tinyvec", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "url" 1121 | version = "2.3.1" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1124 | dependencies = [ 1125 | "form_urlencoded", 1126 | "idna", 1127 | "percent-encoding", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "vcpkg" 1132 | version = "0.2.15" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1135 | 1136 | [[package]] 1137 | name = "version_check" 1138 | version = "0.9.4" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1141 | 1142 | [[package]] 1143 | name = "wasi" 1144 | version = "0.11.0+wasi-snapshot-preview1" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1147 | 1148 | [[package]] 1149 | name = "winapi" 1150 | version = "0.3.9" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1153 | dependencies = [ 1154 | "winapi-i686-pc-windows-gnu", 1155 | "winapi-x86_64-pc-windows-gnu", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "winapi-i686-pc-windows-gnu" 1160 | version = "0.4.0" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1163 | 1164 | [[package]] 1165 | name = "winapi-x86_64-pc-windows-gnu" 1166 | version = "0.4.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1169 | 1170 | [[package]] 1171 | name = "windows-sys" 1172 | version = "0.42.0" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1175 | dependencies = [ 1176 | "windows_aarch64_gnullvm", 1177 | "windows_aarch64_msvc", 1178 | "windows_i686_gnu", 1179 | "windows_i686_msvc", 1180 | "windows_x86_64_gnu", 1181 | "windows_x86_64_gnullvm", 1182 | "windows_x86_64_msvc", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "windows_aarch64_gnullvm" 1187 | version = "0.42.0" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 1190 | 1191 | [[package]] 1192 | name = "windows_aarch64_msvc" 1193 | version = "0.42.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 1196 | 1197 | [[package]] 1198 | name = "windows_i686_gnu" 1199 | version = "0.42.0" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 1202 | 1203 | [[package]] 1204 | name = "windows_i686_msvc" 1205 | version = "0.42.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 1208 | 1209 | [[package]] 1210 | name = "windows_x86_64_gnu" 1211 | version = "0.42.0" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 1214 | 1215 | [[package]] 1216 | name = "windows_x86_64_gnullvm" 1217 | version = "0.42.0" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 1220 | 1221 | [[package]] 1222 | name = "windows_x86_64_msvc" 1223 | version = "0.42.0" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 1226 | 1227 | [[package]] 1228 | name = "zstd" 1229 | version = "0.11.2+zstd.1.5.2" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" 1232 | dependencies = [ 1233 | "zstd-safe", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "zstd-safe" 1238 | version = "5.0.2+zstd.1.5.2" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" 1241 | dependencies = [ 1242 | "libc", 1243 | "zstd-sys", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "zstd-sys" 1248 | version = "2.0.4+zstd.1.5.2" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" 1251 | dependencies = [ 1252 | "cc", 1253 | "libc", 1254 | ] 1255 | --------------------------------------------------------------------------------