170 lines
5.4 KiB
Rust
170 lines
5.4 KiB
Rust
// file: crates/ksp-offchain-transport-lib/src/market_price_observation.rs
|
|
// version: 2
|
|
|
|
/// Maximum UTF-8 byte length accepted for safe provider provenance.
|
|
pub const PRICE_PROVENANCE_MAX_BYTES: usize = 256;
|
|
|
|
/// Millisecond UTC timestamp used by provider-neutral public projections.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
|
|
pub struct PriceTimestamp {
|
|
unix_millis: u64,
|
|
}
|
|
|
|
impl PriceTimestamp {
|
|
/// Creates a UTC timestamp from whole milliseconds since Unix epoch.
|
|
#[must_use]
|
|
pub const fn from_unix_millis(unix_millis: u64) -> Self {
|
|
return Self { unix_millis };
|
|
}
|
|
|
|
/// Returns whole milliseconds since Unix epoch.
|
|
#[must_use]
|
|
pub const fn unix_millis(&self) -> u64 {
|
|
return self.unix_millis;
|
|
}
|
|
}
|
|
|
|
/// Safe bounded provenance supplied by one provider adapter.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(try_from = "std::string::String", into = "std::string::String")]
|
|
pub struct PriceProvenance(std::string::String);
|
|
|
|
impl PriceProvenance {
|
|
/// Creates bounded non-empty provenance without accepting control characters.
|
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
|
let value = value.into();
|
|
if !valid_provenance(value.as_str()) {
|
|
return std::result::Result::Err(observation_error());
|
|
}
|
|
return std::result::Result::Ok(Self(value));
|
|
}
|
|
|
|
/// Returns the safe provider provenance.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
return self.0.as_str();
|
|
}
|
|
}
|
|
|
|
impl std::convert::TryFrom<std::string::String> for PriceProvenance {
|
|
type Error = ksp_core_lib::Error;
|
|
|
|
fn try_from(value: std::string::String) -> std::result::Result<Self, Self::Error> {
|
|
return crate::PriceProvenance::new(value);
|
|
}
|
|
}
|
|
|
|
impl std::convert::From<PriceProvenance> for std::string::String {
|
|
fn from(value: PriceProvenance) -> Self {
|
|
return value.0;
|
|
}
|
|
}
|
|
|
|
/// Public V1 SOL/USD observation normalized by Off-chain Transport.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
|
pub struct SolUsdPriceObservation {
|
|
pair: crate::PricePair,
|
|
price: crate::PriceDecimal,
|
|
provider_id: crate::PriceProviderId,
|
|
provider_timestamp: std::option::Option<crate::PriceTimestamp>,
|
|
provenance: crate::PriceProvenance,
|
|
received_at: crate::PriceTimestamp,
|
|
request_started_at: crate::PriceTimestamp,
|
|
semantics: crate::PriceSemantics,
|
|
}
|
|
|
|
impl SolUsdPriceObservation {
|
|
/// Creates one successful normalized SOL/USD observation.
|
|
pub fn new(
|
|
provider_id: crate::PriceProviderId,
|
|
price: crate::PriceDecimal,
|
|
semantics: crate::PriceSemantics,
|
|
request_started_at: crate::PriceTimestamp,
|
|
received_at: crate::PriceTimestamp,
|
|
provider_timestamp: std::option::Option<crate::PriceTimestamp>,
|
|
provenance: crate::PriceProvenance,
|
|
) -> ksp_core_lib::Result<Self> {
|
|
if received_at < request_started_at {
|
|
return std::result::Result::Err(observation_error());
|
|
}
|
|
return std::result::Result::Ok(Self {
|
|
pair: crate::PricePair::SolUsd,
|
|
price,
|
|
provider_id,
|
|
provider_timestamp,
|
|
provenance,
|
|
received_at,
|
|
request_started_at,
|
|
semantics,
|
|
});
|
|
}
|
|
|
|
/// Returns the only V1 pair represented by this observation.
|
|
#[must_use]
|
|
pub const fn pair(&self) -> crate::PricePair {
|
|
return self.pair;
|
|
}
|
|
|
|
/// Returns the exact positive SOL/USD price.
|
|
#[must_use]
|
|
pub const fn price(&self) -> crate::PriceDecimal {
|
|
return self.price;
|
|
}
|
|
|
|
/// Returns the opaque provider identifier.
|
|
#[must_use]
|
|
pub const fn provider_id(&self) -> &crate::PriceProviderId {
|
|
return &self.provider_id;
|
|
}
|
|
|
|
/// Returns a provider timestamp only when the provider adapter has a real price-time field.
|
|
#[must_use]
|
|
pub const fn provider_timestamp(&self) -> std::option::Option<crate::PriceTimestamp> {
|
|
return self.provider_timestamp;
|
|
}
|
|
|
|
/// Returns safe provider provenance.
|
|
#[must_use]
|
|
pub const fn provenance(&self) -> &crate::PriceProvenance {
|
|
return &self.provenance;
|
|
}
|
|
|
|
/// Returns the KSP wall-clock receipt timestamp.
|
|
#[must_use]
|
|
pub const fn received_at(&self) -> crate::PriceTimestamp {
|
|
return self.received_at;
|
|
}
|
|
|
|
/// Returns the KSP wall-clock request-start timestamp.
|
|
#[must_use]
|
|
pub const fn request_started_at(&self) -> crate::PriceTimestamp {
|
|
return self.request_started_at;
|
|
}
|
|
|
|
/// Returns the provider-specific semantic class retained by the normalized observation.
|
|
#[must_use]
|
|
pub const fn semantics(&self) -> crate::PriceSemantics {
|
|
return self.semantics;
|
|
}
|
|
}
|
|
|
|
fn observation_error() -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_PROVIDER_OBSERVATION_INVALID, "invalid off-chain price observation");
|
|
}
|
|
|
|
fn valid_provenance(value: &str) -> bool {
|
|
if value.is_empty() || value.len() > crate::PRICE_PROVENANCE_MAX_BYTES || value.trim() != value {
|
|
return false;
|
|
}
|
|
for character in value.chars() {
|
|
if character.is_control() {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/market_price_observation.rs"]
|
|
mod tests;
|