feat: initial commit - 4NK Certificator service
- Bitcoin mainnet anchoring service for 4NK relay data volumes - REST API for processes, metrics, and anchors - PostgreSQL database with SQLx - Docker support with compose file - Configuration via TOML - Periodic anchoring based on block intervals - Conditional payment system (priceMoSats, btcAddress) - Process state snapshot with cryptographic hash - Health check endpoint Version: 0.1.0 (MVP)
This commit is contained in:
commit
4acfa96a5c
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Rust
|
||||||
|
/target/
|
||||||
|
Cargo.lock
|
||||||
|
**/*.rs.bk
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# Config
|
||||||
|
config.toml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
/tmp/
|
||||||
|
/data/
|
39
CHANGELOG.md
Normal file
39
CHANGELOG.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to the 4NK Certificator project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial project structure
|
||||||
|
- Database models and schema (processes, metrics, anchors, payments)
|
||||||
|
- REST API endpoints:
|
||||||
|
- `GET /api/v1/processes` - List all monitored processes
|
||||||
|
- `GET /api/v1/processes/:id/metrics` - Get process metrics
|
||||||
|
- `GET /api/v1/processes/:id/anchors` - Get process anchors
|
||||||
|
- Configuration system (TOML)
|
||||||
|
- Docker support (Dockerfile + docker-compose)
|
||||||
|
- Health check endpoint
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
- Implement RelayMonitor (WebSocket connection to sdk_relay)
|
||||||
|
- Implement PaymentWatcher (Bitcoin RPC monitoring)
|
||||||
|
- Implement AnchorEngine (Bitcoin transaction creation and broadcast)
|
||||||
|
- Add WebSocket API for real-time events
|
||||||
|
- Add JWT authentication
|
||||||
|
- Add Prometheus metrics
|
||||||
|
- Write unit and integration tests
|
||||||
|
- Generate OpenAPI/Swagger documentation
|
||||||
|
|
||||||
|
## [0.1.0] - 2025-10-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial specification document (CERTIFICATOR_SPECIFICATION.md)
|
||||||
|
- Project structure and core files
|
||||||
|
- README and documentation
|
||||||
|
|
||||||
|
[Unreleased]: https://git.4nkweb.com/4nk/4NK_certificator/compare/v0.1.0...HEAD
|
||||||
|
[0.1.0]: https://git.4nkweb.com/4nk/4NK_certificator/releases/tag/v0.1.0
|
61
Cargo.toml
Normal file
61
Cargo.toml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
[package]
|
||||||
|
name = "4nk_certificator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["4NK Team"]
|
||||||
|
description = "Bitcoin mainnet anchoring service for 4NK relay data volumes"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Web framework
|
||||||
|
actix-web = "4"
|
||||||
|
actix-rt = "2"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-tungstenite = "0.20"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
# Bitcoin
|
||||||
|
bitcoin = "0.31"
|
||||||
|
bitcoincore-rpc = "0.18"
|
||||||
|
bdk = { version = "0.29", features = ["keys-bip39"] }
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "macros", "chrono"] }
|
||||||
|
redis = { version = "0.24", features = ["tokio-comp", "connection-manager"] }
|
||||||
|
|
||||||
|
# Crypto
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
toml = "0.8"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
anyhow = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
# Time
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
# Utils
|
||||||
|
lazy_static = "1"
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
jsonwebtoken = "9"
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
prometheus = "0.13"
|
||||||
|
|
||||||
|
# SDK common (local dependency)
|
||||||
|
sdk_common = { path = "../sdk_common" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockito = "1"
|
||||||
|
tempfile = "3"
|
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
FROM rust:1.75-slim as builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy manifests
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
# Build release
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# Runtime image
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
libpq5 \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binary from builder
|
||||||
|
COPY --from=builder /build/target/release/4nk_certificator /app/4nk_certificator
|
||||||
|
|
||||||
|
# Create logs directory
|
||||||
|
RUN mkdir -p /var/log/4nk_certificator
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8082
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8082/health || exit 1
|
||||||
|
|
||||||
|
# Run
|
||||||
|
CMD ["/app/4nk_certificator"]
|
170
README.md
Normal file
170
README.md
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# 4NK Certificator
|
||||||
|
|
||||||
|
Service d'ancrage cryptographique sur Bitcoin mainnet pour les volumes de données des relais 4NK.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Le **Certificator** enregistre périodiquement sur le mainnet Bitcoin l'empreinte des échanges de données transitant par les relais 4NK. Il fournit une preuve vérifiable et immuable du volume de données échangées, avec un système de paiement conditionnel pour activer l'ancrage.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Voir la spécification complète : [`../docs/CERTIFICATOR_SPECIFICATION.md`](../docs/CERTIFICATOR_SPECIFICATION.md)
|
||||||
|
|
||||||
|
## Fonctionnalités principales
|
||||||
|
|
||||||
|
- **Ancrage périodique** : Enregistrement mensuel (configurable) sur Bitcoin mainnet
|
||||||
|
- **Paiement conditionnel** : Ancrage activé uniquement si paiement détecté
|
||||||
|
- **Métriques en temps réel** : Surveillance des volumes de données par processus
|
||||||
|
- **API REST** : Accès aux métriques, ancrages et paiements
|
||||||
|
- **Vérification** : Validation cryptographique des ancrages on-chain
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Rust 1.70+
|
||||||
|
- PostgreSQL 15+
|
||||||
|
- Redis 7+
|
||||||
|
- Bitcoin Core (mainnet ou signet pour tests)
|
||||||
|
- sdk_relay en cours d'exécution
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copier le fichier de configuration exemple
|
||||||
|
cp config/certificator.toml.example config.toml
|
||||||
|
|
||||||
|
# Éditer la configuration
|
||||||
|
nano config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialisation de la base de données
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Créer la base de données
|
||||||
|
psql -U postgres -c "CREATE DATABASE certificator_db;"
|
||||||
|
psql -U postgres -c "CREATE USER certificator WITH PASSWORD 'secure_password';"
|
||||||
|
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE certificator_db TO certificator;"
|
||||||
|
|
||||||
|
# Les migrations sont appliquées automatiquement au démarrage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lancement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mode développement
|
||||||
|
RUST_LOG=info cargo run
|
||||||
|
|
||||||
|
# Mode production
|
||||||
|
./target/release/4nk_certificator
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### API REST
|
||||||
|
|
||||||
|
#### Lister les processus
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8082/api/v1/processes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Obtenir les métriques d'un processus
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8082/api/v1/processes/abc123:0/metrics?start_block=800000&end_block=804320"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Lister les ancrages d'un processus
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8082/api/v1/processes/abc123:0/anchors
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8082/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
4NK_certificator/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # Point d'entrée
|
||||||
|
│ ├── config.rs # Configuration
|
||||||
|
│ ├── models.rs # Modèles de données
|
||||||
|
│ ├── db.rs # Accès base de données
|
||||||
|
│ ├── api/ # API REST
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── processes.rs
|
||||||
|
│ │ ├── metrics.rs
|
||||||
|
│ │ └── anchors.rs
|
||||||
|
│ ├── monitor.rs # Surveillance relay
|
||||||
|
│ └── anchor.rs # Logique d'ancrage
|
||||||
|
├── config/
|
||||||
|
│ └── certificator.toml.example
|
||||||
|
├── Cargo.toml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t git.4nkweb.com/4nk/4nk_certificator:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f ../lecoffre_node/docker-compose.certificator.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Développement
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Formatage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statut du projet
|
||||||
|
|
||||||
|
**Version actuelle** : 0.1.0 (MVP en développement)
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
|
||||||
|
- [ ] Implémenter RelayMonitor (connexion WebSocket au relay)
|
||||||
|
- [ ] Implémenter PaymentWatcher (surveillance Bitcoin RPC)
|
||||||
|
- [ ] Implémenter AnchorEngine (création et broadcast des transactions)
|
||||||
|
- [ ] Ajouter WebSocket API pour events temps réel
|
||||||
|
- [ ] Implémenter JWT authentication
|
||||||
|
- [ ] Ajouter métriques Prometheus
|
||||||
|
- [ ] Tests unitaires et d'intégration
|
||||||
|
- [ ] Documentation API (OpenAPI/Swagger)
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Voir LICENSE dans le dépôt principal 4NK_env.
|
||||||
|
|
||||||
|
## Auteurs
|
||||||
|
|
||||||
|
Équipe 4NK - [https://4nkweb.com](https://4nkweb.com)
|
33
config/certificator.toml.example
Normal file
33
config/certificator.toml.example
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
[server]
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 8082
|
||||||
|
log_level = "info"
|
||||||
|
|
||||||
|
[bitcoin]
|
||||||
|
network = "mainnet"
|
||||||
|
rpc_url = "http://localhost:8332"
|
||||||
|
rpc_user = "bitcoin"
|
||||||
|
rpc_password = "your_password_here"
|
||||||
|
wallet_name = "certificator_wallet"
|
||||||
|
min_confirmations = 6
|
||||||
|
|
||||||
|
[relay]
|
||||||
|
websocket_url = "ws://sdk_relay:8090"
|
||||||
|
monitor_interval_secs = 60
|
||||||
|
|
||||||
|
[anchoring]
|
||||||
|
interval_blocks = 4320 # ~30 days (144 blocks/day)
|
||||||
|
auto_anchor = true
|
||||||
|
tx_fee_sat_per_vbyte = 10
|
||||||
|
|
||||||
|
[database]
|
||||||
|
url = "postgresql://certificator:password@localhost/certificator_db"
|
||||||
|
max_connections = 10
|
||||||
|
|
||||||
|
[redis]
|
||||||
|
url = "redis://localhost:6379"
|
||||||
|
cache_ttl_secs = 3600
|
||||||
|
|
||||||
|
[api]
|
||||||
|
jwt_secret = "your_secret_key_here_change_in_production"
|
||||||
|
cors_allowed_origins = ["https://dev4.4nkweb.com"]
|
22
src/anchor.rs
Normal file
22
src/anchor.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
pub async fn start_anchoring_loop(_config: Config, _db: Arc<Mutex<Database>>) -> Result<()> {
|
||||||
|
info!("⚓ Starting anchoring loop...");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// TODO: Check if anchoring interval reached
|
||||||
|
// TODO: For each process with payment confirmed:
|
||||||
|
// - Compute anchor hash
|
||||||
|
// - Create Bitcoin transaction
|
||||||
|
// - Broadcast
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(600)).await;
|
||||||
|
}
|
||||||
|
}
|
37
src/api/anchors.rs
Normal file
37
src/api/anchors.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
pub async fn get_anchors(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let process_id = path.into_inner();
|
||||||
|
let db = db.lock().await;
|
||||||
|
|
||||||
|
match db.get_anchors_for_process(&process_id).await {
|
||||||
|
Ok(anchors) => HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"process_id": process_id,
|
||||||
|
"anchors": anchors.iter().map(|a| {
|
||||||
|
serde_json::json!({
|
||||||
|
"anchor_hash": hex::encode(&a.anchor_hash),
|
||||||
|
"period_start_block": a.period_start_block,
|
||||||
|
"period_end_block": a.period_end_block,
|
||||||
|
"total_mb": a.total_mb,
|
||||||
|
"anchor_txid": a.anchor_txid,
|
||||||
|
"anchor_block": a.anchor_block,
|
||||||
|
"status": a.status,
|
||||||
|
"created_at": a.created_at,
|
||||||
|
"explorer_url": a.anchor_txid.as_ref().map(|txid| {
|
||||||
|
format!("https://mempool.space/tx/{}", txid)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
})),
|
||||||
|
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
|
||||||
|
"error": format!("Failed to fetch anchors: {}", e)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
50
src/api/metrics.rs
Normal file
50
src/api/metrics.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MetricsQuery {
|
||||||
|
start_block: Option<i32>,
|
||||||
|
end_block: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_metrics(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
query: web::Query<MetricsQuery>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let process_id = path.into_inner();
|
||||||
|
let db = db.lock().await;
|
||||||
|
|
||||||
|
let start = query.start_block.unwrap_or(0);
|
||||||
|
let end = query.end_block.unwrap_or(i32::MAX);
|
||||||
|
|
||||||
|
match db.get_metrics_for_period(&process_id, start, end).await {
|
||||||
|
Ok(metrics) => {
|
||||||
|
let total_sent: i64 = metrics.iter().map(|m| m.bytes_sent).sum();
|
||||||
|
let total_received: i64 = metrics.iter().map(|m| m.bytes_received).sum();
|
||||||
|
let total_messages: i32 = metrics.iter().map(|m| m.message_count).sum();
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"process_id": process_id,
|
||||||
|
"period": {
|
||||||
|
"start_block": start,
|
||||||
|
"end_block": end
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"total_bytes_sent": total_sent,
|
||||||
|
"total_bytes_received": total_received,
|
||||||
|
"total_mb": (total_sent + total_received) / 1_048_576,
|
||||||
|
"message_count": total_messages
|
||||||
|
},
|
||||||
|
"details": metrics
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
|
||||||
|
"error": format!("Failed to fetch metrics: {}", e)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
14
src/api/mod.rs
Normal file
14
src/api/mod.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
mod processes;
|
||||||
|
mod metrics;
|
||||||
|
mod anchors;
|
||||||
|
|
||||||
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(
|
||||||
|
web::scope("/processes")
|
||||||
|
.route("", web::get().to(processes::list_processes))
|
||||||
|
.route("/{process_id}/metrics", web::get().to(metrics::get_metrics))
|
||||||
|
.route("/{process_id}/anchors", web::get().to(anchors::get_anchors))
|
||||||
|
);
|
||||||
|
}
|
18
src/api/processes.rs
Normal file
18
src/api/processes.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
pub async fn list_processes(db: web::Data<Arc<Mutex<Database>>>) -> HttpResponse {
|
||||||
|
let db = db.lock().await;
|
||||||
|
|
||||||
|
match db.get_all_processes().await {
|
||||||
|
Ok(processes) => HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"processes": processes
|
||||||
|
})),
|
||||||
|
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
|
||||||
|
"error": format!("Failed to fetch processes: {}", e)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
70
src/config.rs
Normal file
70
src/config.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server: ServerConfig,
|
||||||
|
pub bitcoin: BitcoinConfig,
|
||||||
|
pub relay: RelayConfig,
|
||||||
|
pub anchoring: AnchoringConfig,
|
||||||
|
pub database: DatabaseConfig,
|
||||||
|
pub redis: RedisConfig,
|
||||||
|
pub api: ApiConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub log_level: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BitcoinConfig {
|
||||||
|
pub network: String,
|
||||||
|
pub rpc_url: String,
|
||||||
|
pub rpc_user: String,
|
||||||
|
pub rpc_password: String,
|
||||||
|
pub wallet_name: String,
|
||||||
|
pub min_confirmations: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RelayConfig {
|
||||||
|
pub websocket_url: String,
|
||||||
|
pub monitor_interval_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AnchoringConfig {
|
||||||
|
pub interval_blocks: u32,
|
||||||
|
pub auto_anchor: bool,
|
||||||
|
pub tx_fee_sat_per_vbyte: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DatabaseConfig {
|
||||||
|
pub url: String,
|
||||||
|
pub max_connections: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RedisConfig {
|
||||||
|
pub url: String,
|
||||||
|
pub cache_ttl_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ApiConfig {
|
||||||
|
pub jwt_secret: String,
|
||||||
|
pub cors_allowed_origins: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_file(path: &str) -> Result<Self> {
|
||||||
|
let contents = fs::read_to_string(path)?;
|
||||||
|
let config: Config = toml::from_str(&contents)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
237
src/db.rs
Normal file
237
src/db.rs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
use sqlx::{PgPool, postgres::PgPoolOptions, Row};
|
||||||
|
use anyhow::Result;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn new(database_url: &str) -> Result<Self> {
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(10)
|
||||||
|
.connect(database_url)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_migrations(&self) -> Result<()> {
|
||||||
|
// Create tables if they don't exist
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS processes (
|
||||||
|
process_id TEXT PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
last_updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
price_mo_sats BIGINT,
|
||||||
|
btc_address TEXT,
|
||||||
|
payment_status TEXT DEFAULT 'UNPAID',
|
||||||
|
total_paid_sats BIGINT DEFAULT 0,
|
||||||
|
state_merkle_root BYTEA
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS metrics (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
process_id TEXT REFERENCES processes(process_id),
|
||||||
|
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
block_height INTEGER NOT NULL,
|
||||||
|
bytes_sent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
bytes_received BIGINT NOT NULL DEFAULT 0,
|
||||||
|
message_count INTEGER NOT NULL DEFAULT 0
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS anchors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
process_id TEXT REFERENCES processes(process_id),
|
||||||
|
anchor_hash BYTEA NOT NULL,
|
||||||
|
period_start_block INTEGER NOT NULL,
|
||||||
|
period_end_block INTEGER NOT NULL,
|
||||||
|
total_mb BIGINT NOT NULL,
|
||||||
|
anchor_txid TEXT,
|
||||||
|
anchor_block INTEGER,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
status TEXT DEFAULT 'PENDING'
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS payments (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
process_id TEXT REFERENCES processes(process_id),
|
||||||
|
txid TEXT NOT NULL,
|
||||||
|
vout INTEGER NOT NULL,
|
||||||
|
amount_sats BIGINT NOT NULL,
|
||||||
|
received_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
block_height INTEGER,
|
||||||
|
confirmations INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
sqlx::query("CREATE INDEX IF NOT EXISTS idx_metrics_process_time ON metrics(process_id, timestamp)")
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query("CREATE INDEX IF NOT EXISTS idx_anchors_process ON anchors(process_id)")
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process operations
|
||||||
|
pub async fn upsert_process(&self, process: &Process) -> Result<()> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO processes (process_id, created_at, last_updated, price_mo_sats, btc_address, payment_status, total_paid_sats, state_merkle_root)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
ON CONFLICT (process_id) DO UPDATE SET
|
||||||
|
last_updated = $3,
|
||||||
|
price_mo_sats = $4,
|
||||||
|
btc_address = $5,
|
||||||
|
payment_status = $6,
|
||||||
|
total_paid_sats = $7,
|
||||||
|
state_merkle_root = $8
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&process.process_id)
|
||||||
|
.bind(&process.created_at)
|
||||||
|
.bind(&process.last_updated)
|
||||||
|
.bind(&process.price_mo_sats)
|
||||||
|
.bind(&process.btc_address)
|
||||||
|
.bind(&process.payment_status)
|
||||||
|
.bind(&process.total_paid_sats)
|
||||||
|
.bind(&process.state_merkle_root)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_processes(&self) -> Result<Vec<Process>> {
|
||||||
|
let processes = sqlx::query_as::<_, Process>("SELECT * FROM processes")
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(processes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_process(&self, process_id: &str) -> Result<Option<Process>> {
|
||||||
|
let process = sqlx::query_as::<_, Process>("SELECT * FROM processes WHERE process_id = $1")
|
||||||
|
.bind(process_id)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(process)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics operations
|
||||||
|
pub async fn insert_metric(&self, metric: &Metric) -> Result<()> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO metrics (process_id, timestamp, block_height, bytes_sent, bytes_received, message_count)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&metric.process_id)
|
||||||
|
.bind(&metric.timestamp)
|
||||||
|
.bind(&metric.block_height)
|
||||||
|
.bind(&metric.bytes_sent)
|
||||||
|
.bind(&metric.bytes_received)
|
||||||
|
.bind(&metric.message_count)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_metrics_for_period(
|
||||||
|
&self,
|
||||||
|
process_id: &str,
|
||||||
|
start_block: i32,
|
||||||
|
end_block: i32,
|
||||||
|
) -> Result<Vec<Metric>> {
|
||||||
|
let metrics = sqlx::query_as::<_, Metric>(
|
||||||
|
"SELECT * FROM metrics WHERE process_id = $1 AND block_height >= $2 AND block_height <= $3"
|
||||||
|
)
|
||||||
|
.bind(process_id)
|
||||||
|
.bind(start_block)
|
||||||
|
.bind(end_block)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchor operations
|
||||||
|
pub async fn insert_anchor(&self, anchor: &Anchor) -> Result<i64> {
|
||||||
|
let row = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO anchors (process_id, anchor_hash, period_start_block, period_end_block, total_mb, created_at, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&anchor.process_id)
|
||||||
|
.bind(&anchor.anchor_hash)
|
||||||
|
.bind(&anchor.period_start_block)
|
||||||
|
.bind(&anchor.period_end_block)
|
||||||
|
.bind(&anchor.total_mb)
|
||||||
|
.bind(&anchor.created_at)
|
||||||
|
.bind(&anchor.status)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row.get(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_anchors_for_process(&self, process_id: &str) -> Result<Vec<Anchor>> {
|
||||||
|
let anchors = sqlx::query_as::<_, Anchor>("SELECT * FROM anchors WHERE process_id = $1 ORDER BY created_at DESC")
|
||||||
|
.bind(process_id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(anchors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment operations
|
||||||
|
pub async fn insert_payment(&self, payment: &Payment) -> Result<()> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payments (process_id, txid, vout, amount_sats, received_at, block_height, confirmations)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(&payment.process_id)
|
||||||
|
.bind(&payment.txid)
|
||||||
|
.bind(&payment.vout)
|
||||||
|
.bind(&payment.amount_sats)
|
||||||
|
.bind(&payment.received_at)
|
||||||
|
.bind(&payment.block_height)
|
||||||
|
.bind(&payment.confirmations)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
96
src/main.rs
Normal file
96
src/main.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use actix_web::{web, App, HttpServer, HttpResponse};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::{info, error};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod monitor;
|
||||||
|
mod anchor;
|
||||||
|
mod models;
|
||||||
|
mod db;
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
|
use db::Database;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
info!("🚀 Starting 4NK Certificator service...");
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
let config = Config::from_file("config.toml")?;
|
||||||
|
info!("📄 Configuration loaded from config.toml");
|
||||||
|
|
||||||
|
// Initialize database connection
|
||||||
|
let database = Database::new(&config.database.url).await?;
|
||||||
|
info!("🗄️ Database connected");
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
database.run_migrations().await?;
|
||||||
|
info!("✅ Database migrations applied");
|
||||||
|
|
||||||
|
// Wrap database in Arc<Mutex> for shared state
|
||||||
|
let db = Arc::new(Mutex::new(database));
|
||||||
|
|
||||||
|
// Start background tasks
|
||||||
|
let monitor_handle = tokio::spawn({
|
||||||
|
let config = config.clone();
|
||||||
|
let db = db.clone();
|
||||||
|
async move {
|
||||||
|
monitor::start_monitoring(config, db).await
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let anchor_handle = tokio::spawn({
|
||||||
|
let config = config.clone();
|
||||||
|
let db = db.clone();
|
||||||
|
async move {
|
||||||
|
anchor::start_anchoring_loop(config, db).await
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
|
let bind_address = format!("{}:{}", config.server.host, config.server.port);
|
||||||
|
info!("🌐 Starting HTTP server on {}", bind_address);
|
||||||
|
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(web::Data::new(db.clone()))
|
||||||
|
.app_data(web::Data::new(config.clone()))
|
||||||
|
.route("/health", web::get().to(health_check))
|
||||||
|
.service(
|
||||||
|
web::scope("/api/v1")
|
||||||
|
.configure(api::configure_routes)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind(&bind_address)?
|
||||||
|
.run();
|
||||||
|
|
||||||
|
info!("✅ 4NK Certificator service started successfully");
|
||||||
|
|
||||||
|
// Await all tasks
|
||||||
|
tokio::select! {
|
||||||
|
res = server => {
|
||||||
|
error!("HTTP server stopped: {:?}", res);
|
||||||
|
}
|
||||||
|
res = monitor_handle => {
|
||||||
|
error!("Monitor task stopped: {:?}", res);
|
||||||
|
}
|
||||||
|
res = anchor_handle => {
|
||||||
|
error!("Anchor task stopped: {:?}", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health_check() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "4nk_certificator",
|
||||||
|
"version": env!("CARGO_PKG_VERSION")
|
||||||
|
}))
|
||||||
|
}
|
142
src/models.rs
Normal file
142
src/models.rs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use bitcoin::{OutPoint, Address, Txid};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
pub struct Process {
|
||||||
|
pub process_id: String, // OutPoint format "txid:vout"
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub last_updated: DateTime<Utc>,
|
||||||
|
pub price_mo_sats: Option<i64>,
|
||||||
|
pub btc_address: Option<String>,
|
||||||
|
pub payment_status: String,
|
||||||
|
pub total_paid_sats: i64,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub state_merkle_root: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PriceConfig {
|
||||||
|
pub price_mo_sats: u64,
|
||||||
|
pub btc_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PriceConfig {
|
||||||
|
pub fn validate(&self) -> anyhow::Result<()> {
|
||||||
|
if self.price_mo_sats == 0 {
|
||||||
|
return Err(anyhow::anyhow!("price_mo_sats must be > 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.price_mo_sats > 1000 {
|
||||||
|
return Err(anyhow::anyhow!("price_mo_sats too high (max 1000 sats/MB)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Bitcoin address
|
||||||
|
let _addr: Address = self.btc_address.parse()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid Bitcoin address: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_payment_condition(&self, total_mb: u64, paid_amount: u64) -> bool {
|
||||||
|
let required = self.price_mo_sats * total_mb;
|
||||||
|
paid_amount >= required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum PaymentStatus {
|
||||||
|
Unpaid,
|
||||||
|
Pending,
|
||||||
|
Paid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PaymentStatus {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PaymentStatus::Unpaid => write!(f, "UNPAID"),
|
||||||
|
PaymentStatus::Pending => write!(f, "PENDING"),
|
||||||
|
PaymentStatus::Paid => write!(f, "PAID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
pub struct Metric {
|
||||||
|
pub id: Option<i64>,
|
||||||
|
pub process_id: String,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub block_height: i32,
|
||||||
|
pub bytes_sent: i64,
|
||||||
|
pub bytes_received: i64,
|
||||||
|
pub message_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
pub struct Anchor {
|
||||||
|
pub id: Option<i64>,
|
||||||
|
pub process_id: String,
|
||||||
|
pub anchor_hash: Vec<u8>,
|
||||||
|
pub period_start_block: i32,
|
||||||
|
pub period_end_block: i32,
|
||||||
|
pub total_mb: i64,
|
||||||
|
pub anchor_txid: Option<String>,
|
||||||
|
pub anchor_block: Option<i32>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProcessStateSnapshot {
|
||||||
|
pub process_id: String,
|
||||||
|
pub period_start_block: u32,
|
||||||
|
pub period_end_block: u32,
|
||||||
|
pub total_bytes_sent: u64,
|
||||||
|
pub total_bytes_received: u64,
|
||||||
|
pub message_count: u64,
|
||||||
|
pub participants: Vec<String>,
|
||||||
|
pub state_merkle_root: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessStateSnapshot {
|
||||||
|
pub fn compute_anchor_hash(&self) -> [u8; 32] {
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
|
||||||
|
// Parse process_id (format: "txid:vout")
|
||||||
|
if let Some((txid_str, vout_str)) = self.process_id.split_once(':') {
|
||||||
|
if let Ok(txid_bytes) = hex::decode(txid_str) {
|
||||||
|
hasher.update(&txid_bytes);
|
||||||
|
}
|
||||||
|
if let Ok(vout) = vout_str.parse::<u32>() {
|
||||||
|
hasher.update(&vout.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher.update(&self.period_start_block.to_le_bytes());
|
||||||
|
hasher.update(&self.period_end_block.to_le_bytes());
|
||||||
|
hasher.update(&self.total_bytes_sent.to_le_bytes());
|
||||||
|
hasher.update(&self.total_bytes_received.to_le_bytes());
|
||||||
|
hasher.update(&self.message_count.to_le_bytes());
|
||||||
|
hasher.update(&self.state_merkle_root);
|
||||||
|
|
||||||
|
// Add protocol tag
|
||||||
|
let tag = b"4NK-CERTIFICATOR-V1";
|
||||||
|
hasher.update(tag);
|
||||||
|
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
pub struct Payment {
|
||||||
|
pub id: Option<i64>,
|
||||||
|
pub process_id: String,
|
||||||
|
pub txid: String,
|
||||||
|
pub vout: i32,
|
||||||
|
pub amount_sats: i64,
|
||||||
|
pub received_at: DateTime<Utc>,
|
||||||
|
pub block_height: Option<i32>,
|
||||||
|
pub confirmations: i32,
|
||||||
|
}
|
20
src/monitor.rs
Normal file
20
src/monitor.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
pub async fn start_monitoring(_config: Config, _db: Arc<Mutex<Database>>) -> Result<()> {
|
||||||
|
info!("📡 Starting relay monitoring...");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// TODO: Connect to relay WebSocket
|
||||||
|
// TODO: Monitor messages
|
||||||
|
// TODO: Record metrics
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user