diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f6699a6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + name: cargo clippy + runs-on: ubuntu-latest + needs: fmt + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + # No `-D warnings` — jsonic carries pre-existing dead_code + + # naming-convention warnings (jsonic-demo binary) that should be + # paid down deliberately, not fixed in the same PR that wires + # CI. Tighten to `-D warnings` once the backlog is resolved. + - run: cargo clippy --all-targets + + test: + name: cargo test + runs-on: ubuntu-latest + needs: fmt + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --all-targets diff --git a/src/api.rs b/src/api.rs index 2d28db6..2d6e529 100644 --- a/src/api.rs +++ b/src/api.rs @@ -20,19 +20,19 @@ use std::sync::Arc; use axum::{ + Router, extract::{Path, State}, http::StatusCode, response::{IntoResponse, Json}, routing::{get, post}, - Router, }; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use crate::core::dao::DAORegistry; use crate::core::heartbeat::JsonicNode; -use crate::core::reputation::{compute_pagerank, NodeId, PageRankConfig}; -use crate::core::types::{BalanceSheet, DAOId, MainChainBlock, NetworkMetrics, Transaction, DAO}; +use crate::core::reputation::{NodeId, PageRankConfig, compute_pagerank}; +use crate::core::types::{BalanceSheet, DAO, DAOId, MainChainBlock, NetworkMetrics, Transaction}; /// Shared, thread-safe handle to a Jsonic node. pub type SharedNode = Arc>; @@ -115,14 +115,14 @@ async fn health(State(node): State) -> Json { }) } -async fn register_dao( - State(node): State, - Json(dao): Json, -) -> impl IntoResponse { +async fn register_dao(State(node): State, Json(dao): Json) -> impl IntoResponse { let id = dao.id.clone(); let mut guard = node.write().await; guard.register_dao(dao); - (StatusCode::CREATED, Json(RegisterDaoResponse { dao_id: id })) + ( + StatusCode::CREATED, + Json(RegisterDaoResponse { dao_id: id }), + ) } async fn submit_transaction( @@ -187,8 +187,7 @@ async fn get_metrics( None => Err(( StatusCode::NOT_FOUND, Json(ErrorResponse { - error: "no main-chain blocks yet; run a heartbeat past a Solstice" - .to_string(), + error: "no main-chain blocks yet; run a heartbeat past a Solstice".to_string(), }), )), } @@ -228,7 +227,10 @@ async fn get_reputation( Path(dao_id): Path, ) -> Json { let guard = node.read().await; - let scores = compute_pagerank(&guard.main_chain.reputation_graph, &PageRankConfig::default()); + let scores = compute_pagerank( + &guard.main_chain.reputation_graph, + &PageRankConfig::default(), + ); let key = NodeId::DAO(dao_id.clone()); let pr = scores.pagerank.get(&key).copied().unwrap_or(0.0); let trust = (pr - scores.baseline_rank).max(0.0); @@ -252,9 +254,9 @@ fn registered(registry: &DAORegistry, dao_id: &DAOId) -> bool { mod tests { use super::*; use crate::core::dao::RegisteredDAO; - use axum::body::{to_bytes, Body}; + use axum::body::{Body, to_bytes}; use axum::http::{Request, StatusCode}; - use serde_json::{json, Value}; + use serde_json::{Value, json}; use tower::ServiceExt; fn fresh_node() -> SharedNode { @@ -264,7 +266,9 @@ mod tests { } async fn body_json(resp: axum::response::Response) -> Value { - let bytes = to_bytes(resp.into_body(), usize::MAX).await.expect("read body"); + let bytes = to_bytes(resp.into_body(), usize::MAX) + .await + .expect("read body"); serde_json::from_slice(&bytes).expect("parse json") } @@ -273,7 +277,12 @@ mod tests { let node = fresh_node(); let app = build_router(node); let resp = app - .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()) + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); diff --git a/src/bin/rpc.rs b/src/bin/rpc.rs index 8132323..eb6fd91 100644 --- a/src/bin/rpc.rs +++ b/src/bin/rpc.rs @@ -12,7 +12,7 @@ use std::env; use std::sync::Arc; -use jsonic_protocol::api::{build_router, SharedNode}; +use jsonic_protocol::api::{SharedNode, build_router}; use jsonic_protocol::core::heartbeat::JsonicNode; use jsonic_protocol::core::store::{ChainStore, SledStore}; use tokio::net::TcpListener; @@ -34,7 +34,10 @@ async fn main() -> Result<(), Box> { ); node.main_chain = restored; } else { - eprintln!("[jsonic-rpc] no prior chain at {}, starting fresh", data_dir); + eprintln!( + "[jsonic-rpc] no prior chain at {}, starting fresh", + data_dir + ); } let shared: SharedNode = Arc::new(RwLock::new(node)); diff --git a/src/core/crypto.rs b/src/core/crypto.rs index 649fde3..07a0fa2 100644 --- a/src/core/crypto.rs +++ b/src/core/crypto.rs @@ -134,7 +134,7 @@ mod tests { #[test] fn test_merkle_root_single() { let h = sha256_str("tx1"); - let root = merkle_root(&[h.clone()]); + let root = merkle_root(std::slice::from_ref(&h)); assert_eq!(root, h); } diff --git a/src/core/dao.rs b/src/core/dao.rs index b46991b..7e8b03c 100644 --- a/src/core/dao.rs +++ b/src/core/dao.rs @@ -6,10 +6,8 @@ use chrono::Utc; -use super::crypto::{derive_dao_id, generate_keypair, sign, KeyPair}; -use super::types::{ - Transaction, TransactionStatus, TransactionType, DAOId, DAO, DAOProfile, -}; +use super::crypto::{KeyPair, derive_dao_id, generate_keypair, sign}; +use super::types::{DAO, DAOId, DAOProfile, Transaction, TransactionStatus, TransactionType}; /// A registered DAO with its private signing key. /// The signing key never leaves the DAO's node in a real deployment. @@ -228,12 +226,8 @@ mod tests { let mut sender = RegisteredDAO::register("Acme Corp", "Technology"); let receiver = RegisteredDAO::register("Globex Inc", "Manufacturing"); - let invoice = sender.create_invoice( - receiver.id(), - 50_000.0, - "USD", - "Q1 consulting services", - ); + let invoice = + sender.create_invoice(receiver.id(), 50_000.0, "USD", "Q1 consulting services"); assert_eq!(invoice.tx_type, TransactionType::Invoice); assert_eq!(invoice.from, *sender.id()); diff --git a/src/core/heartbeat.rs b/src/core/heartbeat.rs index 9a976cf..1714ca2 100644 --- a/src/core/heartbeat.rs +++ b/src/core/heartbeat.rs @@ -15,7 +15,7 @@ use super::dao::DAORegistry; use super::mainchain::MainChain; use super::pot; use super::sidechain::SideChain; -use super::types::{DAOId, Transaction, TransactionStatus, TokenDistribution}; +use super::types::{DAOId, TokenDistribution, Transaction, TransactionStatus}; use std::collections::HashMap; @@ -80,7 +80,7 @@ impl JsonicNode { self.process_matching(); // Check if it's time for Solstice - if self.tick % self.solstice_interval == 0 { + if self.tick.is_multiple_of(self.solstice_interval) { Some(self.execute_solstice()) } else { None @@ -110,9 +110,7 @@ impl JsonicNode { (tx_j, tx_i, j, i) }; - if let pot::POTVerdict::Settled = - pot::settle_invoice(invoice, payment) - { + if let pot::POTVerdict::Settled = pot::settle_invoice(invoice, payment) { settlements.push((inv_idx, pay_idx)); self.main_chain.record_transaction_outcome(true); continue; @@ -146,11 +144,8 @@ impl JsonicNode { // Buyer (payment_from) -> Seller (invoice_from): edge in the // reputation graph, weighted by the settled value. - self.main_chain.record_settled_transaction( - &payment_from, - &invoice_from, - settled_value, - ); + self.main_chain + .record_settled_transaction(&payment_from, &invoice_from, settled_value); // Update invoice status to Settled on the issuer's side-chain if let Some(chain) = self.side_chains.get_mut(&invoice_from) { diff --git a/src/core/mainchain.rs b/src/core/mainchain.rs index a37cc7b..b892956 100644 --- a/src/core/mainchain.rs +++ b/src/core/mainchain.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use super::crypto::{merkle_root, sha256_str}; use super::pot; use super::reputation::{ - compute_dao_reward, compute_pagerank, NodeId, PageRankConfig, ReputationGraph, + NodeId, PageRankConfig, ReputationGraph, compute_dao_reward, compute_pagerank, }; use super::types::{ BlockHeader, DAOId, DAOSnapshot, Hash, MainChainBlock, NetworkMetrics, TokenDistribution, @@ -107,10 +107,8 @@ impl MainChain { // Compute network metrics let anxiety = pot::compute_anxiety(self.total_transactions, self.invalid_transactions); - let adrenaline = pot::compute_adrenaline( - self.current_heartbeat_tx_count, - TARGET_TX_PER_HEARTBEAT, - ); + let adrenaline = + pot::compute_adrenaline(self.current_heartbeat_tx_count, TARGET_TX_PER_HEARTBEAT); let heartbeat_ms = pot::adjusted_heartbeat_ms(BASE_HEARTBEAT_MS, adrenaline); let network_metrics = NetworkMetrics { @@ -139,10 +137,7 @@ impl MainChain { /// - Volume of matched transactions /// - Value of matched transactions /// - Ratio of matched vs unmatched (low Anxiety contribution) - fn compute_token_distribution( - &self, - snapshots: &[DAOSnapshot], - ) -> Vec { + fn compute_token_distribution(&self, snapshots: &[DAOSnapshot]) -> Vec { let total_relevance: f64 = snapshots.iter().map(|s| s.relevance_score).sum(); if total_relevance == 0.0 { @@ -312,10 +307,7 @@ mod tests { #[test] fn test_no_tokens_when_no_activity() { let mut chain = MainChain::new(); - let snapshots = vec![ - make_snapshot("dao1", 0, 0.0), - make_snapshot("dao2", 0, 0.0), - ]; + let snapshots = vec![make_snapshot("dao1", 0, 0.0), make_snapshot("dao2", 0, 0.0)]; let distributions = chain.solstice(snapshots); diff --git a/src/core/pot.rs b/src/core/pot.rs index dd34242..c5e9cc7 100644 --- a/src/core/pot.rs +++ b/src/core/pot.rs @@ -55,10 +55,7 @@ pub fn verify_sequence(tx: &Transaction, expected_next: u64) -> bool { /// For an invoice from DAO-A to DAO-B to be "matched", DAO-B must have a /// corresponding acknowledgment. In practice this means both DAOs recorded /// the same transaction (same ID, same amount, matching from/to). -pub fn match_transactions( - sender_tx: &Transaction, - receiver_tx: &Transaction, -) -> POTVerdict { +pub fn match_transactions(sender_tx: &Transaction, receiver_tx: &Transaction) -> POTVerdict { // Must reference the same transaction ID if sender_tx.id != receiver_tx.id { return POTVerdict::Invalid("Transaction IDs do not match".to_string()); @@ -96,10 +93,7 @@ pub fn match_transactions( /// - The payment amount matches the invoice amount /// - The payment is from the invoice's `to` DAO (the debtor pays) /// - The payment is to the invoice's `from` DAO (the creditor receives) -pub fn settle_invoice( - invoice: &Transaction, - payment: &Transaction, -) -> POTVerdict { +pub fn settle_invoice(invoice: &Transaction, payment: &Transaction) -> POTVerdict { if invoice.tx_type != TransactionType::Invoice { return POTVerdict::Invalid("First transaction is not an Invoice".to_string()); } @@ -117,24 +111,18 @@ pub fn settle_invoice( )); } None => { - return POTVerdict::Invalid( - "Payment does not reference any invoice".to_string(), - ); + return POTVerdict::Invalid("Payment does not reference any invoice".to_string()); } } // The payer (payment.from) should be the invoice's recipient (invoice.to) if payment.from != invoice.to { - return POTVerdict::Invalid( - "Payment sender does not match invoice recipient".to_string(), - ); + return POTVerdict::Invalid("Payment sender does not match invoice recipient".to_string()); } // The payee (payment.to) should be the invoice issuer (invoice.from) if payment.to != invoice.from { - return POTVerdict::Invalid( - "Payment recipient does not match invoice issuer".to_string(), - ); + return POTVerdict::Invalid("Payment recipient does not match invoice issuer".to_string()); } // Amounts must match @@ -206,7 +194,7 @@ mod tests { #[test] fn test_match_transactions_success() { let mut dao_a = RegisteredDAO::register("DAO-A", "Tech"); - let mut dao_b = RegisteredDAO::register("DAO-B", "Finance"); + let dao_b = RegisteredDAO::register("DAO-B", "Finance"); let invoice = dao_a.create_invoice(dao_b.id(), 5000.0, "USD", "Services"); @@ -215,8 +203,13 @@ mod tests { ack.signature = crate::core::crypto::sign( format!( "{}:{}:{}:{}:{}:{}:{}", - ack.id, ack.from, ack.to, ack.amount, ack.currency, - ack.sequence_number, ack.timestamp.to_rfc3339() + ack.id, + ack.from, + ack.to, + ack.amount, + ack.currency, + ack.sequence_number, + ack.timestamp.to_rfc3339() ) .as_bytes(), &dao_b.keypair.signing_key, @@ -265,13 +258,7 @@ mod tests { let invoice = dao_a.create_invoice(dao_b.id(), 10_000.0, "EUR", "Services"); // DAO-C tries to pay an invoice meant for DAO-B - let payment = dao_c.create_payment( - dao_a.id(), - 10_000.0, - "EUR", - &invoice.id, - "Wrong payer", - ); + let payment = dao_c.create_payment(dao_a.id(), 10_000.0, "EUR", &invoice.id, "Wrong payer"); let verdict = settle_invoice(&invoice, &payment); assert!(matches!(verdict, POTVerdict::Invalid(_))); diff --git a/src/core/reputation.rs b/src/core/reputation.rs index 8ce62ae..a1ea535 100644 --- a/src/core/reputation.rs +++ b/src/core/reputation.rs @@ -11,13 +11,13 @@ //! //! Where: //! - `d` = damping factor (0.85) — probability of following a transaction edge -//! vs. random jump +//! vs. random jump //! - `N` = total number of nodes in the graph //! - `Ti` = nodes that transact with A (in-edges: entities that bought from -//! or sold to A) +//! or sold to A) //! - `C(Ti)`= out-degree of Ti (number of unique trading partners) //! - `W(Ti → A)` = normalized edge weight from Ti to A, based on transaction -//! value and volume +//! value and volume //! //! # Jsonic-Specific Extensions //! @@ -100,13 +100,7 @@ impl ReputationGraph { /// Add or update a transaction edge between two nodes. /// If an edge already exists between (from, to), the counts and values /// are accumulated. - pub fn add_transaction( - &mut self, - from: NodeId, - to: NodeId, - tx_count: u64, - tx_value: f64, - ) { + pub fn add_transaction(&mut self, from: NodeId, to: NodeId, tx_count: u64, tx_value: f64) { self.nodes.insert(from.clone()); self.nodes.insert(to.clone()); @@ -240,10 +234,7 @@ pub struct ReputationScores { /// /// Dangling nodes (nodes with no outbound edges) distribute their rank /// uniformly across all nodes, just like in the original PageRank paper. -pub fn compute_pagerank( - graph: &ReputationGraph, - config: &PageRankConfig, -) -> ReputationScores { +pub fn compute_pagerank(graph: &ReputationGraph, config: &PageRankConfig) -> ReputationScores { let n = graph.node_count(); if n == 0 { return ReputationScores { @@ -392,11 +383,7 @@ pub fn compute_pagerank( /// transaction cannot dominate. /// - A Sybil ring of N fake buyers contributes ~0 because every fake's PR /// converges to the random-walk baseline. -pub fn compute_dao_reward( - dao: &NodeId, - graph: &ReputationGraph, - scores: &ReputationScores, -) -> f64 { +pub fn compute_dao_reward(dao: &NodeId, graph: &ReputationGraph, scores: &ReputationScores) -> f64 { let inbound_edges = match graph.inbound.get(dao) { Some(edges) => edges, None => return 0.0, diff --git a/src/core/sidechain.rs b/src/core/sidechain.rs index 9f6a7cd..68648e0 100644 --- a/src/core/sidechain.rs +++ b/src/core/sidechain.rs @@ -144,10 +144,7 @@ impl SideChain { } /// Apply a set of transactions to a balance sheet, producing updated balances. - fn apply_transactions( - opening: &BalanceSheet, - transactions: &[Transaction], - ) -> BalanceSheet { + fn apply_transactions(opening: &BalanceSheet, transactions: &[Transaction]) -> BalanceSheet { let mut balance = opening.clone(); for tx in transactions { diff --git a/src/main.rs b/src/main.rs index 20c44e5..5378208 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,10 @@ fn main() { let mut node = JsonicNode::new(); node.solstice_interval = 10; // Short interval for demo purposes - println!("▸ Network node initialized (Solstice every {} heartbeats)", node.solstice_interval); + println!( + "▸ Network node initialized (Solstice every {} heartbeats)", + node.solstice_interval + ); println!(); // ----------------------------------------------------------------------- @@ -36,9 +39,24 @@ fn main() { let mut initech = RegisteredDAO::register("Initech LLC", "Consulting"); println!("▸ Registered DAOs:"); - println!(" DAO 1: {} [{}] id={}…", acme.dao.profile.name, acme.dao.profile.sector, &acme.dao.id[..12]); - println!(" DAO 2: {} [{}] id={}…", globex.dao.profile.name, globex.dao.profile.sector, &globex.dao.id[..12]); - println!(" DAO 3: {} [{}] id={}…", initech.dao.profile.name, initech.dao.profile.sector, &initech.dao.id[..12]); + println!( + " DAO 1: {} [{}] id={}…", + acme.dao.profile.name, + acme.dao.profile.sector, + &acme.dao.id[..12] + ); + println!( + " DAO 2: {} [{}] id={}…", + globex.dao.profile.name, + globex.dao.profile.sector, + &globex.dao.id[..12] + ); + println!( + " DAO 3: {} [{}] id={}…", + initech.dao.profile.name, + initech.dao.profile.sector, + &initech.dao.id[..12] + ); println!(); let acme_id = acme.id().clone(); @@ -57,29 +75,56 @@ fn main() { // Acme invoices Globex for tech consulting let inv1 = acme.create_invoice(&globex_id, 50_000.0, "USD", "Tech consulting Q1 2024"); - println!(" 📄 Invoice: {} → {} | $50,000 | '{}'", acme.dao.profile.name, globex.dao.profile.name, inv1.description); + println!( + " 📄 Invoice: {} → {} | $50,000 | '{}'", + acme.dao.profile.name, globex.dao.profile.name, inv1.description + ); let inv1_id = inv1.id.clone(); node.submit_transaction(inv1); // Globex pays Acme's invoice - let pay1 = globex.create_payment(&acme_id, 50_000.0, "USD", &inv1_id, "Payment for tech consulting Q1"); - println!(" 💰 Payment: {} → {} | $50,000 | settles invoice", globex.dao.profile.name, acme.dao.profile.name); + let pay1 = globex.create_payment( + &acme_id, + 50_000.0, + "USD", + &inv1_id, + "Payment for tech consulting Q1", + ); + println!( + " 💰 Payment: {} → {} | $50,000 | settles invoice", + globex.dao.profile.name, acme.dao.profile.name + ); node.submit_transaction(pay1); // Initech invoices Acme for consulting let inv2 = initech.create_invoice(&acme_id, 30_000.0, "USD", "Strategy consulting Jan 2024"); - println!(" 📄 Invoice: {} → {} | $30,000 | '{}'", initech.dao.profile.name, acme.dao.profile.name, inv2.description); + println!( + " 📄 Invoice: {} → {} | $30,000 | '{}'", + initech.dao.profile.name, acme.dao.profile.name, inv2.description + ); let inv2_id = inv2.id.clone(); node.submit_transaction(inv2); // Acme pays Initech - let pay2 = acme.create_payment(&initech_id, 30_000.0, "USD", &inv2_id, "Payment for strategy consulting"); - println!(" 💰 Payment: {} → {} | $30,000 | settles invoice", acme.dao.profile.name, initech.dao.profile.name); + let pay2 = acme.create_payment( + &initech_id, + 30_000.0, + "USD", + &inv2_id, + "Payment for strategy consulting", + ); + println!( + " 💰 Payment: {} → {} | $30,000 | settles invoice", + acme.dao.profile.name, initech.dao.profile.name + ); node.submit_transaction(pay2); // Globex invoices Initech for manufacturing let inv3 = globex.create_invoice(&initech_id, 75_000.0, "EUR", "Custom parts manufacturing"); - println!(" 📄 Invoice: {} → {} | €75,000 | '{}'", globex.dao.profile.name, initech.dao.profile.name, inv3.description); + println!( + " 📄 Invoice: {} → {} | €75,000 | '{}'", + globex.dao.profile.name, initech.dao.profile.name, inv3.description + ); node.submit_transaction(inv3); // This invoice is NOT paid — it will remain unmatched @@ -94,7 +139,11 @@ fn main() { for tick in 1..=node.solstice_interval { match node.heartbeat() { Some(distributions) => { - println!(" ♥ Heartbeat #{}: ★ SOLSTICE — Main-chain block #{} created", tick, node.main_chain.height()); + println!( + " ♥ Heartbeat #{}: ★ SOLSTICE — Main-chain block #{} created", + tick, + node.main_chain.height() + ); println!(); // Print token distribution @@ -107,10 +156,7 @@ fn main() { .get(&dist.dao_id) .map(|d| d.profile.name.as_str()) .unwrap_or("Unknown"); - println!( - " │ {} : {:.4} tokens", - name, dist.tokens_awarded - ); + println!(" │ {} : {:.4} tokens", name, dist.tokens_awarded); println!(" │ └─ {}", dist.reason); } println!(" └─────────────────────────────────────────────────────┘"); @@ -136,7 +182,10 @@ fn main() { println!(); println!(" Main-chain height: {}", node.main_chain.height()); - println!(" Total token supply: {:.4}", node.main_chain.total_token_supply); + println!( + " Total token supply: {:.4}", + node.main_chain.total_token_supply + ); println!(" Network anxiety: {:.4}", node.main_chain.anxiety()); println!(); @@ -145,7 +194,10 @@ fn main() { let balance = chain.current_balance(); println!(" {} ({}…):", dao.profile.name, &id[..12]); println!(" Side-chain height: {}", chain.height()); - println!(" Accounts receivable: ${:.2}", balance.accounts_receivable); + println!( + " Accounts receivable: ${:.2}", + balance.accounts_receivable + ); println!(" Accounts payable: ${:.2}", balance.accounts_payable); println!(" Revenue: ${:.2}", balance.revenue); println!(" Expenses: ${:.2}", balance.expenses);