use std::{collections::BTreeMap, sync::Weak};
use futures::{Stream, StreamExt};
use tari_indexer_client::{
error::IndexerRestClientError,
protobuf::{self, UtxoUpdatePayload},
protobuf_stream::ProtobufStreamError,
rest_api_client::IndexerRestApiClient,
types::GetUtxoUpdatesRequest,
};
use tari_ootle_common_types::{Epoch, NumPreshards, StateVersion, array_utils::copy_fixed_checked, shard::Shard};
use tari_template_lib_types::{
ResourceAddress,
UtxoId,
crypto::{RistrettoPublicKeyBytes, UtxoTag},
};
use tracing::error;
#[derive(Debug, Clone)]
pub struct StealthUtxoWatchRequest {
pub resource_address: ResourceAddress,
pub from_epoch: Epoch,
pub shard_state_versions: Vec<(Shard, StateVersion)>,
pub unspent_only: bool,
pub per_shard_limit: u32,
}
impl StealthUtxoWatchRequest {
fn into_request(self) -> GetUtxoUpdatesRequest {
GetUtxoUpdatesRequest {
from_epoch: self.from_epoch,
shard_state_versions: self.shard_state_versions,
resource_address: self.resource_address,
unspent_only: self.unspent_only,
per_shard_limit: self.per_shard_limit,
}
}
}
#[derive(Debug, Clone)]
pub enum StealthUtxoFrame {
StartOfShard {
shard: Shard,
max_state_version: StateVersion,
num_updates: u32,
},
Unspent {
tag: UtxoTag,
public_nonce: RistrettoPublicKeyBytes,
},
Spent {
id: UtxoId,
version: u32,
},
Burnt {
id: UtxoId,
version: u32,
},
EndOfShard {
shard: Shard,
max_state_version: StateVersion,
},
}
#[derive(Debug, thiserror::Error)]
pub enum UtxoWatcherError {
#[error("Indexer REST client has been dropped")]
ClientDropped,
#[error("Indexer REST client error: {0}")]
IndexerClientError(#[from] IndexerRestClientError),
#[error("UTXO stream error: {0}")]
StreamError(#[from] ProtobufStreamError),
#[error("Failed to decode UTXO update frame: {0}")]
DecodeError(String),
}
pub struct StealthUtxoStream {
client: Weak<IndexerRestApiClient>,
request: StealthUtxoWatchRequest,
}
impl StealthUtxoStream {
pub(crate) fn new(client: Weak<IndexerRestApiClient>, request: StealthUtxoWatchRequest) -> Self {
Self { client, request }
}
pub fn into_stream(self) -> impl Stream<Item = Result<StealthUtxoFrame, UtxoWatcherError>> {
async_stream::stream! {
let client = match self.client.upgrade() {
Some(client) => client,
None => {
error!("Indexer REST client has been dropped");
yield Err(UtxoWatcherError::ClientDropped);
return;
},
};
let mut stream = match client.stream_utxo_updates_protobuf(self.request.into_request()).await {
Ok(stream) => stream,
Err(err) => {
error!(%err, "Failed to start stealth UTXO update stream");
yield Err(UtxoWatcherError::IndexerClientError(err));
return;
},
};
let mut current_shard: Option<Shard> = None;
loop {
match stream.next().await {
Some(Ok(payload)) => {
let UtxoUpdatePayload { sos, update, eos } = payload;
if let Some(sos) = sos {
let shard = Shard::from(sos.shard);
current_shard = Some(shard);
yield Ok(StealthUtxoFrame::StartOfShard {
shard,
max_state_version: StateVersion::from(sos.max_state_version),
num_updates: sos.num_updates,
});
}
if let Some(update) = update {
match convert_update(update) {
Ok(frame) => yield Ok(frame),
Err(err) => {
yield Err(err);
return;
},
}
}
if let Some(eos) = eos {
let Some(shard) = current_shard.take() else {
yield Err(UtxoWatcherError::DecodeError(
"EndOfShard received before any StartOfShard".to_string(),
));
return;
};
yield Ok(StealthUtxoFrame::EndOfShard {
shard,
max_state_version: StateVersion::from(eos.max_state_version),
});
}
},
Some(Err(err)) => {
error!(%err, "Error receiving stealth UTXO update");
yield Err(UtxoWatcherError::StreamError(err));
return;
},
None => return,
}
}
}
}
}
fn convert_update(update: protobuf::WalletUtxoUpdate) -> Result<StealthUtxoFrame, UtxoWatcherError> {
match update {
protobuf::WalletUtxoUpdate::Unspent(unspent) => {
let public_nonce = RistrettoPublicKeyBytes::from_bytes(&unspent.public_nonce)
.map_err(|e| UtxoWatcherError::DecodeError(format!("public nonce: {e}")))?;
Ok(StealthUtxoFrame::Unspent {
tag: unspent.tag.into(),
public_nonce,
})
},
protobuf::WalletUtxoUpdate::Spent(spent) => {
let id = copy_fixed_checked(&spent.id)
.map(UtxoId::from_array)
.ok_or_else(|| UtxoWatcherError::DecodeError("UTXO id: incorrect length".to_string()))?;
Ok(StealthUtxoFrame::Spent {
id,
version: spent.version,
})
},
protobuf::WalletUtxoUpdate::Burnt(burnt) => {
let id = copy_fixed_checked(&burnt.id)
.map(UtxoId::from_array)
.ok_or_else(|| UtxoWatcherError::DecodeError("UTXO id: incorrect length".to_string()))?;
Ok(StealthUtxoFrame::Burnt {
id,
version: burnt.version,
})
},
}
}
#[derive(Debug, Clone, Default)]
pub struct ShardCursor {
versions: BTreeMap<Shard, StateVersion>,
}
impl ShardCursor {
pub fn genesis(num_preshards: NumPreshards) -> Self {
Self {
versions: num_preshards
.all_shards_iter()
.map(|shard| (shard, StateVersion::zero()))
.collect(),
}
}
pub fn from_pairs(pairs: impl IntoIterator<Item = (Shard, StateVersion)>) -> Self {
Self {
versions: pairs.into_iter().collect(),
}
}
pub fn to_pairs(&self) -> Vec<(Shard, StateVersion)> {
self.versions.iter().map(|(shard, v)| (*shard, *v)).collect()
}
pub fn get(&self, shard: Shard) -> StateVersion {
self.versions.get(&shard).copied().unwrap_or_else(StateVersion::zero)
}
pub fn observe(&mut self, shard: Shard, max_state_version: StateVersion) {
let entry = self.versions.entry(shard).or_insert_with(StateVersion::zero);
if max_state_version > *entry {
*entry = max_state_version;
}
}
}