use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use chia_protocol::{Bytes32, CoinState, CoinStateFilters, Program, SpendBundle};
use chia_wallet_sdk::client::Peer;
use tokio::sync::RwLock;
use super::error::LightClientError;
use crate::peer::connect::PeerOrigin;
use crate::peer::PeerBackend;
const MAX_PUZZLE_STATE_PAGES: usize = 10_000;
const MAX_ACCUMULATED_COIN_STATES: usize = 500_000;
#[async_trait]
pub trait CoinStateFetcher: Send + Sync {
async fn coin_states(
&self,
coin_ids: Vec<Bytes32>,
subscribe: bool,
) -> Result<Vec<CoinState>, LightClientError>;
async fn puzzle_states(
&self,
puzzle_hashes: Vec<Bytes32>,
filters: CoinStateFilters,
subscribe: bool,
) -> Result<Vec<CoinState>, LightClientError>;
async fn children(&self, coin_id: Bytes32) -> Result<Vec<CoinState>, LightClientError>;
async fn puzzle_and_solution(
&self,
coin_id: Bytes32,
height: u32,
) -> Result<(Program, Program), LightClientError>;
}
#[derive(Clone)]
pub struct PooledFetcher {
backend: Arc<PeerBackend>,
anchor: Arc<RwLock<Option<Anchor>>>,
request_timeout: Duration,
}
#[derive(Clone)]
pub(super) struct Anchor {
pub(super) peer: Peer,
pub(super) address: SocketAddr,
pub(super) origin: PeerOrigin,
}
impl PooledFetcher {
pub(super) fn new(backend: Arc<PeerBackend>, request_timeout: Duration) -> Self {
Self {
backend,
anchor: Arc::new(RwLock::new(None)),
request_timeout,
}
}
pub(super) async fn anchor(&self) -> Result<Anchor, LightClientError> {
let mut slot = self.anchor.write().await;
if let Some(existing) = slot.as_ref() {
return Ok(existing.clone());
}
let (peer, address) = self
.backend
.pick()
.await
.map_err(|e| LightClientError::Transport(e.to_string()))?;
let origin = self
.backend
.pool
.origin_of(address)
.await
.ok_or(LightClientError::NotConnected)?;
let anchor = Anchor {
peer,
address,
origin,
};
*slot = Some(anchor.clone());
Ok(anchor)
}
pub(super) async fn current_anchor(&self) -> Option<Anchor> {
self.anchor.read().await.clone()
}
pub(super) async fn anchor_address(&self) -> Option<SocketAddr> {
self.anchor.read().await.as_ref().map(|a| a.address)
}
pub(super) async fn release_anchor(&self, address: SocketAddr) {
let mut slot = self.anchor.write().await;
if slot.as_ref().is_some_and(|a| a.address == address) {
*slot = None;
}
}
async fn session(&self, subscribe: bool) -> Result<(Peer, SocketAddr), LightClientError> {
if subscribe {
let anchor = self.anchor().await?;
return Ok((anchor.peer, anchor.address));
}
self.backend
.pick()
.await
.map_err(|e| LightClientError::Transport(e.to_string()))
}
async fn discard(&self, address: SocketAddr) {
self.backend.pool.eject_peer(address).await;
self.release_anchor(address).await;
}
fn genesis_challenge(&self) -> Bytes32 {
self.backend.genesis_challenge()
}
pub(super) async fn send_transaction(
&self,
bundle: SpendBundle,
) -> Result<u8, LightClientError> {
let (peer, address) = self.session(true).await?;
let result = self
.with_timeout(peer.send_transaction(bundle))
.await
.and_then(|r| r.map_err(|e| LightClientError::Transport(e.to_string())));
match result {
Ok(ack) => Ok(ack.status),
Err(e) => {
self.discard(address).await;
Err(e)
}
}
}
pub(super) async fn remove_coin_subscriptions(
&self,
coin_ids: Vec<Bytes32>,
) -> Result<(), LightClientError> {
let (peer, address) = self.session(true).await?;
let result = self
.with_timeout(peer.remove_coin_subscriptions(Some(coin_ids)))
.await
.and_then(|r| r.map_err(|e| LightClientError::Transport(e.to_string())));
match result {
Ok(_) => Ok(()),
Err(e) => {
self.discard(address).await;
Err(e)
}
}
}
async fn with_timeout<T>(
&self,
fut: impl std::future::Future<Output = T>,
) -> Result<T, LightClientError> {
tokio::time::timeout(self.request_timeout, fut)
.await
.map_err(|_| LightClientError::Timeout)
}
}
struct PuzzleStatePage {
coin_states: Vec<CoinState>,
height: u32,
header_hash: Bytes32,
is_finished: bool,
}
async fn collect_paged<F, Fut>(
genesis_challenge: Bytes32,
mut fetch_page: F,
) -> Result<Vec<CoinState>, LightClientError>
where
F: FnMut(Option<u32>, Bytes32) -> Fut,
Fut: std::future::Future<Output = Result<PuzzleStatePage, LightClientError>>,
{
let mut all = Vec::new();
let mut previous_height: Option<u32> = None;
let mut header_hash = genesis_challenge;
for _page in 0..MAX_PUZZLE_STATE_PAGES {
let page = fetch_page(previous_height, header_hash).await?;
all.extend(page.coin_states);
if all.len() > MAX_ACCUMULATED_COIN_STATES {
return Err(LightClientError::Rejected(format!(
"puzzle-state response exceeded {MAX_ACCUMULATED_COIN_STATES} coins"
)));
}
if page.is_finished {
return Ok(all);
}
if previous_height.is_some_and(|prev| page.height <= prev) {
return Err(LightClientError::Rejected(
"puzzle-state paging did not advance the height".into(),
));
}
previous_height = Some(page.height);
header_hash = page.header_hash;
}
Err(LightClientError::Rejected(format!(
"puzzle-state paging exceeded {MAX_PUZZLE_STATE_PAGES} pages"
)))
}
#[async_trait]
impl CoinStateFetcher for PooledFetcher {
async fn coin_states(
&self,
coin_ids: Vec<Bytes32>,
subscribe: bool,
) -> Result<Vec<CoinState>, LightClientError> {
let (peer, address) = self.session(subscribe).await?;
let result = self
.with_timeout(peer.request_coin_state(
coin_ids,
None,
self.genesis_challenge(),
subscribe,
))
.await
.and_then(|r| r.map_err(|e| LightClientError::Transport(e.to_string())))
.and_then(|r| {
r.map_err(|_| LightClientError::Rejected("coin-state request rejected".into()))
});
match result {
Ok(response) => Ok(response.coin_states),
Err(e) => {
self.discard(address).await;
Err(e)
}
}
}
async fn puzzle_states(
&self,
puzzle_hashes: Vec<Bytes32>,
filters: CoinStateFilters,
subscribe: bool,
) -> Result<Vec<CoinState>, LightClientError> {
let (peer, address) = self.session(subscribe).await?;
let result = collect_paged(self.genesis_challenge(), |previous_height, header_hash| {
let peer = peer.clone();
let puzzle_hashes = puzzle_hashes.clone();
let filters = filters.clone();
let this = self;
async move {
let response = this
.with_timeout(peer.request_puzzle_state(
puzzle_hashes,
previous_height,
header_hash,
filters,
subscribe,
))
.await?
.map_err(|e| LightClientError::Transport(e.to_string()))?
.map_err(|_| {
LightClientError::Rejected("puzzle-state request rejected".into())
})?;
Ok(PuzzleStatePage {
coin_states: response.coin_states,
height: response.height,
header_hash: response.header_hash,
is_finished: response.is_finished,
})
}
})
.await;
if result.is_err() {
self.discard(address).await;
}
result
}
async fn children(&self, coin_id: Bytes32) -> Result<Vec<CoinState>, LightClientError> {
let (peer, address) = self.session(false).await?;
let result = self
.with_timeout(peer.request_children(coin_id))
.await
.and_then(|r| r.map_err(|e| LightClientError::Transport(e.to_string())));
match result {
Ok(response) => Ok(response.coin_states),
Err(e) => {
self.discard(address).await;
Err(e)
}
}
}
async fn puzzle_and_solution(
&self,
coin_id: Bytes32,
height: u32,
) -> Result<(Program, Program), LightClientError> {
let (peer, address) = self.session(false).await?;
let outcome = match self
.with_timeout(peer.request_puzzle_and_solution(coin_id, height))
.await
.and_then(|r| r.map_err(|e| LightClientError::Transport(e.to_string())))
{
Ok(outcome) => outcome,
Err(e) => {
self.discard(address).await;
return Err(e);
}
};
match outcome {
Ok(response) => Ok((response.puzzle, response.solution)),
Err(_) => {
self.discard(address).await;
Err(LightClientError::Rejected(
"peer rejected puzzle/solution for a known-spent coin".into(),
))
}
}
}
}