v0.2.11-pre.006
This commit is contained in:
268
crates/ksp-offchain-transport-lib/src/market_price_jupiter.rs
Normal file
268
crates/ksp-offchain-transport-lib/src/market_price_jupiter.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_jupiter.rs
|
||||
// version: 1
|
||||
|
||||
//! Jupiter Price V3 SOL/USD adapter using the current Developer Platform REST surface.
|
||||
|
||||
const JUPITER_API_KEY_HEADER: &str = "x-api-key";
|
||||
const JUPITER_PRICE_V3_URL: &str = "https://api.jup.ag/price/v3";
|
||||
const JUPITER_PROVIDER_ID: &str = "jupiter";
|
||||
const JUPITER_SOL_DECIMALS: u8 = 9;
|
||||
const JUPITER_SOL_MINT: &str = "So11111111111111111111111111111111111111112";
|
||||
|
||||
/// Jupiter V1 access mode supported by Off-chain Transport.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MarketPriceJupiterAccessMode {
|
||||
/// Free Developer Platform plan using a provider-issued API key.
|
||||
Free,
|
||||
/// Keyless Developer Platform access with the lower documented request cadence.
|
||||
Keyless,
|
||||
}
|
||||
|
||||
/// Runtime settings for the Jupiter Price V3 market-price adapter.
|
||||
pub struct MarketPriceJupiterSettings {
|
||||
access_mode: crate::MarketPriceJupiterAccessMode,
|
||||
api_key: std::option::Option<crate::MarketPriceApiKey>,
|
||||
common: crate::MarketPriceProviderCommonSettings,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceJupiterSettings {
|
||||
/// Creates keyless Jupiter settings without accepting a credential.
|
||||
pub fn keyless(enabled: bool) -> ksp_core_lib::Result<Self> {
|
||||
return Self::new(enabled, crate::MarketPriceJupiterAccessMode::Keyless, std::option::Option::None);
|
||||
}
|
||||
|
||||
/// Creates Free-plan Jupiter settings. An API key is mandatory while the provider is enabled.
|
||||
pub fn free(enabled: bool, api_key: std::option::Option<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
return Self::new(enabled, crate::MarketPriceJupiterAccessMode::Free, api_key);
|
||||
}
|
||||
|
||||
/// Returns the configured Jupiter access mode.
|
||||
#[must_use]
|
||||
pub const fn access_mode(&self) -> crate::MarketPriceJupiterAccessMode {
|
||||
return self.access_mode;
|
||||
}
|
||||
|
||||
/// Returns common provider identity and enablement settings.
|
||||
#[must_use]
|
||||
pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings {
|
||||
return &self.common;
|
||||
}
|
||||
|
||||
fn new(enabled: bool, access_mode: crate::MarketPriceJupiterAccessMode, api_key: std::option::Option<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new(JUPITER_PROVIDER_ID) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let api_key = match access_mode {
|
||||
crate::MarketPriceJupiterAccessMode::Keyless => {
|
||||
if api_key.is_some() {
|
||||
return std::result::Result::Err(provider_settings_error("api_key"));
|
||||
}
|
||||
std::option::Option::None
|
||||
},
|
||||
crate::MarketPriceJupiterAccessMode::Free => match api_key {
|
||||
std::option::Option::Some(value) => match crate::MarketPriceApiKey::new(JUPITER_PROVIDER_ID, value) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None if enabled => return std::result::Result::Err(provider_settings_error("api_key")),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
},
|
||||
};
|
||||
let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled);
|
||||
return std::result::Result::Ok(Self { access_mode, api_key, common });
|
||||
}
|
||||
|
||||
fn api_key(&self) -> std::option::Option<&crate::MarketPriceApiKey> {
|
||||
return self.api_key.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::MarketPriceJupiterSettings {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("MarketPriceJupiterSettings")
|
||||
.field("access_mode", &self.access_mode)
|
||||
.field("api_key_present", &self.api_key.is_some())
|
||||
.field("common", &self.common)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Jupiter Price V3 SOL/USD provider adapter.
|
||||
pub struct MarketPriceJupiterProvider {
|
||||
admission: crate::HttpAdmissionController,
|
||||
descriptor: crate::MarketPriceProviderDescriptor,
|
||||
http: crate::HttpRestClient,
|
||||
settings: crate::MarketPriceJupiterSettings,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceJupiterProvider {
|
||||
/// Builds one Jupiter Price V3 provider from validated runtime settings.
|
||||
pub fn new(settings: crate::MarketPriceJupiterSettings) -> ksp_core_lib::Result<Self> {
|
||||
let descriptor = match descriptor_for(settings.access_mode(), 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 Jupiter capability descriptor.
|
||||
#[must_use]
|
||||
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
|
||||
return &self.descriptor;
|
||||
}
|
||||
|
||||
/// Returns the validated Jupiter runtime settings without exposing credential material.
|
||||
#[must_use]
|
||||
pub const fn settings(&self) -> &crate::MarketPriceJupiterSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Fetches one normalized SOL/USD heuristic observation from Jupiter Price V3.
|
||||
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(JUPITER_PROVIDER_ID));
|
||||
}
|
||||
if let std::result::Result::Err(error) = crate::admit_request(JUPITER_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(&self.settings) {
|
||||
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, JUPITER_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(settings: &crate::MarketPriceJupiterSettings) -> ksp_core_lib::Result<crate::HttpGetRequest> {
|
||||
let mut request = match crate::HttpGetRequest::new_https(JUPITER_PRICE_V3_URL) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
request.append_query_pair("ids", JUPITER_SOL_MINT);
|
||||
if let std::option::Option::Some(api_key) = settings.api_key()
|
||||
&& let std::result::Result::Err(error) = request.insert_sensitive_header(JUPITER_API_KEY_HEADER, api_key.as_str())
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(request);
|
||||
}
|
||||
|
||||
fn descriptor_for(
|
||||
access_mode: crate::MarketPriceJupiterAccessMode,
|
||||
provider_id: crate::MarketPriceProviderId,
|
||||
) -> ksp_core_lib::Result<crate::MarketPriceProviderDescriptor> {
|
||||
let (auth_mode, rate_limit) = match access_mode {
|
||||
crate::MarketPriceJupiterAccessMode::Keyless => {
|
||||
let rate_limit =
|
||||
match crate::MarketPriceProviderRateLimit::fixed(1, 2, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Unspecified) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
(crate::MarketPriceProviderAuthMode::None, rate_limit)
|
||||
},
|
||||
crate::MarketPriceJupiterAccessMode::Free => {
|
||||
let rate_limit =
|
||||
match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Account) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
(crate::MarketPriceProviderAuthMode::RequiredApiKey, rate_limit)
|
||||
},
|
||||
};
|
||||
return crate::MarketPriceProviderDescriptor::new(
|
||||
provider_id,
|
||||
"Jupiter Price V3",
|
||||
crate::MarketPriceSemantics::SolanaHeuristic,
|
||||
auth_mode,
|
||||
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::<std::collections::BTreeMap<std::string::String, JupiterWirePrice>>(bytes) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(crate::invalid_provider_response_with_source(JUPITER_PROVIDER_ID, "response", error));
|
||||
},
|
||||
};
|
||||
if wire.len() != 1 {
|
||||
return std::result::Result::Err(crate::invalid_provider_response(JUPITER_PROVIDER_ID, "response.identity"));
|
||||
}
|
||||
let item = match wire.get(JUPITER_SOL_MINT) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(JUPITER_PROVIDER_ID, "response.identity")),
|
||||
};
|
||||
if item.decimals != std::option::Option::Some(JUPITER_SOL_DECIMALS) {
|
||||
return std::result::Result::Err(crate::invalid_provider_response(JUPITER_PROVIDER_ID, "decimals"));
|
||||
}
|
||||
let price_raw = match item.usd_price.as_deref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(JUPITER_PROVIDER_ID, "usdPrice")),
|
||||
};
|
||||
let price = match crate::MarketPriceDecimal::parse_json_raw(price_raw) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let block_id = match item.block_id {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(JUPITER_PROVIDER_ID, "blockId")),
|
||||
};
|
||||
let provenance = match crate::MarketPriceProvenance::new(std::format!("jupiter:price_v3:{JUPITER_SOL_MINT}:block:{block_id}")) {
|
||||
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::SolanaHeuristic,
|
||||
request_started_at,
|
||||
received_at,
|
||||
std::option::Option::None,
|
||||
provenance,
|
||||
);
|
||||
}
|
||||
|
||||
fn provider_settings_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID, "Jupiter market-price settings are invalid")
|
||||
.with_context("provider", JUPITER_PROVIDER_ID)
|
||||
.with_context("field", field);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct JupiterWirePrice {
|
||||
#[serde(rename = "blockId")]
|
||||
block_id: std::option::Option<u64>,
|
||||
decimals: std::option::Option<u8>,
|
||||
#[serde(rename = "usdPrice")]
|
||||
usd_price: std::option::Option<std::boxed::Box<serde_json::value::RawValue>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/market_price_jupiter.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user