180 lines
7.7 KiB
Rust
180 lines
7.7 KiB
Rust
// file: crates/ksp-offchain-transport-lib/src/market_price_kraken.rs
|
|
// version: 1
|
|
|
|
//! Kraken Spot SOL/USD market-price adapter using the public REST ticker directly through `reqwest`.
|
|
|
|
const KRAKEN_PROVIDER_ID: &str = "kraken";
|
|
const KRAKEN_SOL_USD_PAIR: &str = "SOLUSD";
|
|
const KRAKEN_TICKER_URL: &str = "https://api.kraken.com/0/public/Ticker";
|
|
|
|
/// Runtime settings for the keyless Kraken Spot market-price adapter.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct MarketPriceKrakenSettings {
|
|
common: crate::MarketPriceProviderCommonSettings,
|
|
}
|
|
|
|
impl crate::MarketPriceKrakenSettings {
|
|
/// Creates Kraken Spot settings for the public keyless REST surface.
|
|
pub fn new(enabled: bool) -> ksp_core_lib::Result<Self> {
|
|
let provider_id = match crate::MarketPriceProviderId::new(KRAKEN_PROVIDER_ID) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(Self { common: crate::MarketPriceProviderCommonSettings::new(provider_id, enabled) });
|
|
}
|
|
|
|
/// Returns common provider identity and enablement settings.
|
|
#[must_use]
|
|
pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings {
|
|
return &self.common;
|
|
}
|
|
}
|
|
|
|
/// Kraken Spot SOL/USD provider adapter.
|
|
pub struct MarketPriceKrakenProvider {
|
|
admission: crate::HttpAdmissionController,
|
|
descriptor: crate::MarketPriceProviderDescriptor,
|
|
http: crate::HttpRestClient,
|
|
settings: crate::MarketPriceKrakenSettings,
|
|
}
|
|
|
|
impl crate::MarketPriceKrakenProvider {
|
|
/// Builds one Kraken Spot provider from validated runtime settings.
|
|
pub fn new(settings: crate::MarketPriceKrakenSettings) -> ksp_core_lib::Result<Self> {
|
|
let descriptor = match descriptor_for(settings.common().provider_id().clone()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let runtime = match crate::provider_http_runtime(descriptor.rate_limit()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(Self { admission: runtime.1, descriptor, http: runtime.0, settings });
|
|
}
|
|
|
|
/// Returns the provider-neutral Kraken Spot capability descriptor.
|
|
#[must_use]
|
|
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
|
|
return &self.descriptor;
|
|
}
|
|
|
|
/// Returns the validated Kraken Spot runtime settings.
|
|
#[must_use]
|
|
pub const fn settings(&self) -> &crate::MarketPriceKrakenSettings {
|
|
return &self.settings;
|
|
}
|
|
|
|
/// Fetches one normalized SOL/USD last-trade observation from Kraken Spot.
|
|
pub async fn fetch_sol_usd(&self) -> ksp_core_lib::Result<crate::MarketPriceObservation> {
|
|
if !self.settings.common().enabled() {
|
|
return std::result::Result::Err(crate::provider_disabled_error(KRAKEN_PROVIDER_ID));
|
|
}
|
|
if let std::result::Result::Err(error) = crate::admit_request(KRAKEN_PROVIDER_ID, &self.admission) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let request_started_at = match crate::current_timestamp() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let request = match build_request() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let document = match crate::get_json(&self.http, &self.admission, KRAKEN_PROVIDER_ID, request).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let received_at = match crate::current_timestamp() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return parse_response(document.as_bytes(), self.settings.common().provider_id().clone(), request_started_at, received_at);
|
|
}
|
|
}
|
|
|
|
fn build_request() -> ksp_core_lib::Result<crate::HttpGetRequest> {
|
|
let mut request = match crate::HttpGetRequest::new_https(KRAKEN_TICKER_URL) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
request.append_query_pair("pair", KRAKEN_SOL_USD_PAIR);
|
|
return std::result::Result::Ok(request);
|
|
}
|
|
|
|
fn descriptor_for(provider_id: crate::MarketPriceProviderId) -> ksp_core_lib::Result<crate::MarketPriceProviderDescriptor> {
|
|
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Ip) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::MarketPriceProviderDescriptor::new(
|
|
provider_id,
|
|
"Kraken",
|
|
crate::MarketPriceSemantics::ExchangeLastTrade,
|
|
crate::MarketPriceProviderAuthMode::None,
|
|
rate_limit,
|
|
std::option::Option::None,
|
|
true,
|
|
);
|
|
}
|
|
|
|
fn parse_response(
|
|
bytes: &[u8],
|
|
provider_id: crate::MarketPriceProviderId,
|
|
request_started_at: crate::MarketPriceTimestamp,
|
|
received_at: crate::MarketPriceTimestamp,
|
|
) -> ksp_core_lib::Result<crate::MarketPriceObservation> {
|
|
let wire = match serde_json::from_slice::<KrakenWireResponse>(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(crate::invalid_provider_response_with_source(KRAKEN_PROVIDER_ID, "response", error));
|
|
},
|
|
};
|
|
if !wire.error.is_empty() {
|
|
return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "error"));
|
|
}
|
|
if wire.result.len() != 1 {
|
|
return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "result"));
|
|
}
|
|
let ticker = match wire.result.get(KRAKEN_SOL_USD_PAIR) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "identity")),
|
|
};
|
|
let last_trade = match ticker.last_trade.first() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "last_trade")),
|
|
};
|
|
let price = match crate::MarketPriceDecimal::parse_json_raw(last_trade.as_ref()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let provenance = match crate::MarketPriceProvenance::new("kraken:SOLUSD:last_trade") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::MarketPriceObservation::new(
|
|
provider_id,
|
|
price,
|
|
crate::MarketPriceSemantics::ExchangeLastTrade,
|
|
request_started_at,
|
|
received_at,
|
|
std::option::Option::None,
|
|
provenance,
|
|
);
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct KrakenWireResponse {
|
|
error: std::vec::Vec<std::string::String>,
|
|
result: std::collections::BTreeMap<std::string::String, KrakenWireTicker>,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct KrakenWireTicker {
|
|
#[serde(rename = "c")]
|
|
last_trade: std::vec::Vec<std::boxed::Box<serde_json::value::RawValue>>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/market_price_kraken.rs"]
|
|
mod tests;
|