Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
./src/snapshots
/target/
src/snapshots/
config/config.toml
12 changes: 0 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ unicode-width = "0.2"
p2poolv2_config = { git = "https://github.com/p2poolv2/p2poolv2", package = "p2poolv2_config" }
bitcoin = "0.32.5"
toml_edit = "0.22"
reqwest = { version = "0.12", features = ["json", "rustls-tls", "blocking"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }

[dev-dependencies]
Expand Down
5 changes: 5 additions & 0 deletions config/config.sample.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[api]
host = "127.0.0.1"
port = 46884
auth_user = "p2pool"
auth_pass = "p2pool"
49 changes: 49 additions & 0 deletions dummy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

[network]
listen_address = "/ip4/127.0.0.1/tcp/6884"
dial_peers = []
max_pending_incoming = 10
max_pending_outgoing = 10
max_established_incoming = 50
max_established_outgoing = 50
max_established_per_peer = 1
max_workbase_per_second = 10
max_userworkbase_per_second = 10
max_miningshare_per_second = 100
max_inventory_per_second = 100
max_transaction_per_second = 100
max_requests_per_second = 100
dial_timeout_secs = 30

[store]
path = "./store.db"
background_task_frequency_hours = 24
pplns_ttl_days = 7

[stratum]
hostname = "pool.example.com"
port = 3333
start_difficulty = 10000
minimum_difficulty = 100
solo_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk"
bootstrap_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk"
zmqpubhashblock = "tcp://127.0.0.1:28332"
network = "signet"
version_mask = "1fffe000"
difficulty_multiplier = 1.0
pool_signature = "P2Poolv2"

[bitcoinrpc]
url = "http://127.0.0.1:38332"
username = "p2pool"
password = "p2pool"

[logging]
file = "./logs/p2pool.log"
console = true
level = "info"
stats_dir = "./logs/stats"

[api]
hostname = "127.0.0.1"
port = 46884
52 changes: 42 additions & 10 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use crate::bitcoin_config::ConfigEntry as BitcoinEntry;
use crate::components::bitcoin_config_view::BitcoinConfigView;
use crate::components::file_explorer::FileExplorer;
use crate::components::p2pool_client::{ChainInfo, P2PoolClient};
use crate::components::p2pool_client::{ChainInfo, P2PoolClient, PeerInfo};
use crate::components::p2pool_config_view::P2PoolConfigView;
use crate::components::settings_view::SettingsView;
use crate::settings::Settings;
Expand Down Expand Up @@ -34,7 +34,7 @@ pub const BITCOIN_STATUS_TABS: &[&str] = &["Chain Info", "System", "Logs", "Peer
pub const MAX_BITCOIN_STATUS_TAB: usize = BITCOIN_STATUS_TABS.len() - 1;

/// Tab labels for the P2Pool Status view
pub const P2POOL_STATUS_TABS: &[&str] = &["Chain Info"];
pub const P2POOL_STATUS_TABS: &[&str] = &["Chain Info", "Peers Info"];

pub const MAX_P2POOL_STATUS_TAB: usize = P2POOL_STATUS_TABS.len() - 1;

Expand Down Expand Up @@ -113,15 +113,21 @@ pub struct App {
pub p2pool_status_tab: usize,
pub chain_info: Option<ChainInfo>,
pub p2pool_chain_info_error: Option<String>,
// async channel to receive chain info updates from the background task that fetches it when the P2Pool Status screen is opened
pub peer_info: Option<Vec<PeerInfo>>,
pub p2pool_peer_info_error: Option<String>,
// async channel to receive chain info updates from the background task that
// fetches it when the P2Pool Status screen is opened.
pub chain_info_tx: mpsc::UnboundedSender<anyhow::Result<ChainInfo>>,
pub chain_info_rx: mpsc::UnboundedReceiver<anyhow::Result<ChainInfo>>,
pub peer_info_tx: mpsc::UnboundedSender<anyhow::Result<Vec<PeerInfo>>>,
pub peer_info_rx: mpsc::UnboundedReceiver<anyhow::Result<Vec<PeerInfo>>>,
}

impl App {
#[must_use]
pub fn new() -> App {
let (tx, rx) = mpsc::unbounded_channel();
let (chain_info_tx, chain_info_rx) = mpsc::unbounded_channel();
let (peer_info_tx, peer_info_rx) = mpsc::unbounded_channel();
App {
current_screen: CurrentScreen::Home,
sidebar_index: 0,
Expand All @@ -142,8 +148,12 @@ impl App {
p2pool_status_tab: 0,
chain_info: None,
p2pool_chain_info_error: None,
chain_info_tx: tx,
chain_info_rx: rx,
peer_info: None,
p2pool_peer_info_error: None,
chain_info_tx,
chain_info_rx,
peer_info_tx,
peer_info_rx,
}
}

Expand All @@ -170,6 +180,21 @@ impl App {
}
}

pub fn poll_peer_info(&mut self) {
while let Ok(result) = self.peer_info_rx.try_recv() {
match result {
Ok(info) => {
self.peer_info = Some(info);
self.p2pool_peer_info_error = None;
}
Err(e) => {
self.peer_info = None;
self.p2pool_peer_info_error = Some(e.to_string());
}
}
}
}

// Logic to switch between sidebar items
pub fn toggle_menu(&mut self) {
if self.current_screen == CurrentScreen::BitcoinConfig {
Expand All @@ -187,13 +212,20 @@ impl App {
if let Some(&(_, screen)) = SIDEBAR_ITEMS.get(self.sidebar_index) {
self.current_screen = screen;
if self.current_screen == CurrentScreen::P2PoolStatus {
let client = self.p2pool_client.clone();
let tx = self.chain_info_tx.clone();
let chain_client = self.p2pool_client.clone();
let chain_tx = self.chain_info_tx.clone();
let peer_client = self.p2pool_client.clone();
let peer_tx = self.peer_info_tx.clone();

if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
let res = client.fetch_chain_info().await;
let _ = tx.send(res.map_err(anyhow::Error::from));
let res = chain_client.fetch_chain_info().await;
let _ = chain_tx.send(res.map_err(anyhow::Error::from));
});

handle.spawn(async move {
let res = peer_client.fetch_peer_info().await;
let _ = peer_tx.send(res.map_err(anyhow::Error::from));
});
}
}
Expand Down
87 changes: 87 additions & 0 deletions src/components/p2pool_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ pub struct ChainInfo {
pub chain_tip_blockhash: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PeerInfo {
pub peer_id: String,
pub status: Option<String>,
}

fn build_client() -> Client {
Client::builder()
.timeout(Duration::from_secs(REQUEST_TIMEOUT_SECONDS))
Expand Down Expand Up @@ -81,6 +87,20 @@ impl P2PoolClient {

Ok(data)
}

pub async fn fetch_peer_info(&self) -> Result<Vec<PeerInfo>, reqwest::Error> {
let url = format!("{}/peers", self.base_url);
let mut request = self.client.get(url);

if let Some((user, pass)) = &self.auth_credentials {
request = request.basic_auth(user, Some(pass));
}

let response = request.send().await?.error_for_status()?;
let data = response.json::<Vec<PeerInfo>>().await?;

Ok(data)
}
}

impl Default for P2PoolClient {
Expand Down Expand Up @@ -152,6 +172,73 @@ mod tests {
mock.assert();
}

#[tokio::test]
async fn test_fetch_peer_info_success() {
let mut server = Server::new_async().await;

let mock = server
.mock("GET", "/peers")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!([
{
"peer_id": "12D3KooWPeerOne",
"status": "Connected"
}
])
.to_string(),
)
.create();

let client = P2PoolClient::with_base_url(server.url());
let result = client.fetch_peer_info().await.unwrap();

assert_eq!(result.len(), 1);
assert_eq!(result[0].peer_id, "12D3KooWPeerOne");
assert_eq!(result[0].status.as_deref(), Some("Connected"));
mock.assert();
}

#[tokio::test]
async fn test_fetch_peer_info_accepts_missing_status() {
let mut server = Server::new_async().await;

let mock = server
.mock("GET", "/peers")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!([{ "peer_id": "12D3KooWPeerOne" }]).to_string())
.create();

let client = P2PoolClient::with_base_url(server.url());
let result = client.fetch_peer_info().await.unwrap();

assert_eq!(result.len(), 1);
assert_eq!(result[0].peer_id, "12D3KooWPeerOne");
assert_eq!(result[0].status, None);
mock.assert();
}

#[tokio::test]
async fn test_fetch_peer_info_sends_basic_auth() {
let mut server = Server::new_async().await;

let mock = server
.mock("GET", "/peers")
.match_header("authorization", "Basic dXNlcjpwYXNzd29yZA==")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!([]).to_string())
.create();

let client =
P2PoolClient::with_base_url(server.url()).with_auth("user".into(), "password".into());

client.fetch_peer_info().await.unwrap();
mock.assert();
}

#[tokio::test]
async fn test_fetch_chain_info_errors_on_http_500() {
let mut server = Server::new_async().await;
Expand Down
Loading
Loading