v0.2.11-pre.006

This commit is contained in:
2026-08-25 23:06:26 +02:00
parent 4021d08541
commit 98093d859b
12 changed files with 1079 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/http_client.rs
// version: 3
// version: 4
//! Crate-wide hardened REST client used internally by off-chain capability adapters.
@@ -36,6 +36,22 @@ impl crate::HttpGetRequest {
return Self::parse(url, false);
}
/// Appends one validated non-secret path segment to a crate-owned official provider base URL.
pub(crate) fn append_path_segment(&mut self, value: &str) -> ksp_core_lib::Result<()> {
let segments_result = self.url.path_segments_mut();
let mut segments = match segments_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(()) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_INVALID, "Off-chain provider URL cannot accept a path segment")
.with_context("field", "provider_url_path"),
);
},
};
segments.push(value);
return std::result::Result::Ok(());
}
/// Appends one non-secret query pair using URL encoding.
pub(crate) fn append_query_pair(&mut self, name: &'static str, value: &str) {
self.url.query_pairs_mut().append_pair(name, value);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/lib.rs
// version: 7
// version: 8
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -7,10 +7,10 @@
//! KSP-owned off-chain transport foundation.
//!
//! `0.2.11-pre.005` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only) and extends the direct-REST surface with
//! Kraken Spot and Coinbase Exchange. Exchange adapters preserve `ExchangeLastTrade` semantics instead of pretending to be aggregated market prices.
//! Provider wire DTOs remain private, no provider SDK is used, and the crate-wide hardened `http_*` internals remain unavailable as a generic consumer
//! HTTP client. Config integration and generic registry/refresh orchestration remain outside this tranche.
//! `0.2.11-pre.006` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only) and adds Jupiter Price V3 plus DexScreener.
//! Jupiter preserves its Solana heuristic semantics and block provenance without inventing a price timestamp. DexScreener is bound to one explicitly
//! configured Solana pair address, validates that the returned pair is the configured pair with wrapped SOL as base identity, and consumes only `priceUsd`.
//! Provider wire DTOs remain private, no provider SDK is used, and Config plus generic registry/refresh orchestration remain outside this tranche.
mod constants;
mod error;
@@ -24,6 +24,8 @@ mod market_price_coingecko;
mod market_price_coinmarketcap;
mod market_price_coinpaprika;
mod market_price_decimal;
mod market_price_dexscreener;
mod market_price_jupiter;
mod market_price_kraken;
mod market_price_observation;
mod market_price_provider;
@@ -95,6 +97,16 @@ pub use self::market_price_decimal::MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES;
pub use self::market_price_decimal::MARKET_PRICE_DECIMAL_MAX_SCALE;
/// Exact positive decimal value used by the public market-price contract.
pub use self::market_price_decimal::MarketPriceDecimal;
/// DexScreener SOL/USD provider adapter bound to one configured Solana pair.
pub use self::market_price_dexscreener::MarketPriceDexScreenerProvider;
/// DexScreener runtime settings carrying the explicit Solana pair address.
pub use self::market_price_dexscreener::MarketPriceDexScreenerSettings;
/// Jupiter Developer Platform access mode.
pub use self::market_price_jupiter::MarketPriceJupiterAccessMode;
/// Jupiter Price V3 SOL/USD provider adapter.
pub use self::market_price_jupiter::MarketPriceJupiterProvider;
/// Jupiter Price V3 runtime settings.
pub use self::market_price_jupiter::MarketPriceJupiterSettings;
/// Kraken Spot SOL/USD provider adapter.
pub use self::market_price_kraken::MarketPriceKrakenProvider;
/// Kraken Spot runtime settings.

View File

@@ -0,0 +1,233 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_dexscreener.rs
// version: 1
//! DexScreener SOL/USD adapter bound to one explicitly configured Solana pair address.
const DEXSCREENER_PAIR_BASE_URL: &str = "https://api.dexscreener.com/latest/dex/pairs/solana";
const DEXSCREENER_PROVIDER_ID: &str = "dexscreener";
const DEXSCREENER_SOL_MINT: &str = "So11111111111111111111111111111111111111112";
/// Runtime settings for the keyless DexScreener market-price adapter.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MarketPriceDexScreenerSettings {
common: crate::MarketPriceProviderCommonSettings,
pair_address: ksp_core_lib::Pubkey,
}
impl crate::MarketPriceDexScreenerSettings {
/// Creates DexScreener settings for one explicit Solana pair address.
pub fn new(enabled: bool, pair_address: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
let provider_id = match crate::MarketPriceProviderId::new(DEXSCREENER_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pair_address = match pair_address.into().parse::<ksp_core_lib::Pubkey>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(provider_settings_error("pair_address")),
};
let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled);
return std::result::Result::Ok(Self { common, pair_address });
}
/// Returns common provider identity and enablement settings.
#[must_use]
pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings {
return &self.common;
}
/// Returns the explicit Solana pair address used by this adapter.
#[must_use]
pub const fn pair_address(&self) -> &ksp_core_lib::Pubkey {
return &self.pair_address;
}
}
/// DexScreener SOL/USD provider adapter for one configured pair.
pub struct MarketPriceDexScreenerProvider {
admission: crate::HttpAdmissionController,
descriptor: crate::MarketPriceProviderDescriptor,
http: crate::HttpRestClient,
settings: crate::MarketPriceDexScreenerSettings,
}
impl crate::MarketPriceDexScreenerProvider {
/// Builds one DexScreener provider from validated runtime settings.
pub fn new(settings: crate::MarketPriceDexScreenerSettings) -> 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 DexScreener capability descriptor.
#[must_use]
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
return &self.descriptor;
}
/// Returns the validated DexScreener settings including the safe configured pair address.
#[must_use]
pub const fn settings(&self) -> &crate::MarketPriceDexScreenerSettings {
return &self.settings;
}
/// Fetches one normalized SOL/USD observation from the configured DexScreener pair.
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(DEXSCREENER_PROVIDER_ID));
}
if let std::result::Result::Err(error) = crate::admit_request(DEXSCREENER_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.pair_address()) {
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, DEXSCREENER_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(),
self.settings.pair_address(),
request_started_at,
received_at,
);
}
}
fn build_request(pair_address: &ksp_core_lib::Pubkey) -> ksp_core_lib::Result<crate::HttpGetRequest> {
let mut request = match crate::HttpGetRequest::new_https(DEXSCREENER_PAIR_BASE_URL) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pair_address = pair_address.to_string();
if let std::result::Result::Err(error) = request.append_path_segment(pair_address.as_str()) {
return std::result::Result::Err(error);
}
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(300, 60, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Unspecified)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::MarketPriceProviderDescriptor::new(
provider_id,
"DexScreener",
crate::MarketPriceSemantics::DexPairUsd,
crate::MarketPriceProviderAuthMode::None,
rate_limit,
std::option::Option::None,
true,
);
}
fn parse_response(
bytes: &[u8],
provider_id: crate::MarketPriceProviderId,
configured_pair_address: &ksp_core_lib::Pubkey,
request_started_at: crate::MarketPriceTimestamp,
received_at: crate::MarketPriceTimestamp,
) -> ksp_core_lib::Result<crate::MarketPriceObservation> {
let wire = match serde_json::from_slice::<DexScreenerWireResponse>(bytes) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(crate::invalid_provider_response_with_source(DEXSCREENER_PROVIDER_ID, "response", error));
},
};
let pairs = match wire.pairs {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs")),
};
if pairs.len() != 1 {
return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs"));
}
let pair = &pairs[0];
if pair.chain_id != "solana" {
return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.chainId"));
}
let response_pair_address = match pair.pair_address.parse::<ksp_core_lib::Pubkey>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.pairAddress")),
};
if response_pair_address != *configured_pair_address {
return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.pairAddress"));
}
let base_token = match &pair.base_token {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.baseToken")),
};
if base_token.address != DEXSCREENER_SOL_MINT {
return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.baseToken.address"));
}
let price_raw = match pair.price_usd.as_deref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.priceUsd")),
};
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 provenance = match crate::MarketPriceProvenance::new(std::format!("dexscreener:solana:{configured_pair_address}:priceUsd")) {
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::DexPairUsd,
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, "DexScreener market-price settings are invalid")
.with_context("provider", DEXSCREENER_PROVIDER_ID)
.with_context("field", field);
}
#[derive(serde::Deserialize)]
struct DexScreenerWirePair {
#[serde(rename = "baseToken")]
base_token: std::option::Option<DexScreenerWireToken>,
#[serde(rename = "chainId")]
chain_id: std::string::String,
#[serde(rename = "pairAddress")]
pair_address: std::string::String,
#[serde(rename = "priceUsd")]
price_usd: std::option::Option<std::boxed::Box<serde_json::value::RawValue>>,
}
#[derive(serde::Deserialize)]
struct DexScreenerWireResponse {
pairs: std::option::Option<std::vec::Vec<DexScreenerWirePair>>,
}
#[derive(serde::Deserialize)]
struct DexScreenerWireToken {
address: std::string::String,
}
#[cfg(test)]
#[path = "../unit_tests/market_price_dexscreener.rs"]
mod tests;

View 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;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/tests/dependency_boundary.rs
// version: 6
// version: 7
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -68,3 +68,22 @@ fn pre_005_provider_modules_are_market_price_scoped_and_fixed_origin() {
assert!(!coinpaprika.contains("std::env"));
assert!(!kraken.contains("std::env"));
}
#[test]
fn pre_006_jupiter_and_dexscreener_stay_market_price_scoped_fixed_origin_and_discovery_free() {
let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("mod market_price_dexscreener;"));
assert!(crate_root.contains("mod market_price_jupiter;"));
let dexscreener = include_str!("../src/market_price_dexscreener.rs");
let jupiter = include_str!("../src/market_price_jupiter.rs");
assert!(dexscreener.contains("https://api.dexscreener.com/latest/dex/pairs/solana"));
assert!(dexscreener.contains("Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") || dexscreener.contains("pair_address"));
assert!(!dexscreener.contains("latest/dex/search"));
assert!(!dexscreener.contains("token-pairs/v1"));
assert!(!dexscreener.contains("tokens/v1"));
assert!(jupiter.contains("https://api.jup.ag/price/v3"));
assert!(jupiter.contains("So11111111111111111111111111111111111111112"));
assert!(!jupiter.contains("/quote"));
assert!(!dexscreener.contains("std::env"));
assert!(!jupiter.contains("std::env"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
// version: 6
// version: 7
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -122,6 +122,31 @@ fn public_pre_005_exchange_settings_and_adapters_are_available_from_crate_root()
return std::result::Result::Ok(());
}
#[test]
fn public_pre_006_jupiter_and_dexscreener_settings_and_adapters_are_available_from_crate_root() -> ksp_core_lib::Result<()> {
let jupiter = match ksp_offchain_transport_lib::MarketPriceJupiterSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let jupiter_provider = match ksp_offchain_transport_lib::MarketPriceJupiterProvider::new(jupiter) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(jupiter_provider.descriptor().id().as_str(), "jupiter");
assert_eq!(jupiter_provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic);
let dexscreener = match ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dexscreener_provider = match ksp_offchain_transport_lib::MarketPriceDexScreenerProvider::new(dexscreener) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(dexscreener_provider.descriptor().id().as_str(), "dexscreener");
assert_eq!(dexscreener_provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::DexPairUsd);
return std::result::Result::Ok(());
}
#[test]
fn offchain_error_codes_use_owned_domain() {
let codes = [

View File

@@ -0,0 +1,105 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_dexscreener.rs
// version: 1
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
#[test]
fn dexscreener_fixture_validates_chain_pair_and_sol_identity_and_maps_exact_pair_usd() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::DEXSCREENER_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pair_address = match TEST_PAIR.parse::<ksp_core_lib::Pubkey>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(super::provider_settings_error("test_pair")),
};
let fixture = concat!(
r#"{"schemaVersion":"1.0.0","pairs":[{"chainId":"solana","dexId":"orca","pairAddress":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","#,
r#""baseToken":{"address":"So11111111111111111111111111111111111111112","name":"Wrapped SOL","symbol":"SOL"},"#,
r#""quoteToken":{"address":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","name":"USD Coin","symbol":"USDC"},"#,
r#""priceNative":"151.987654321012345678","priceUsd":"151.987654321012345678"}]}"#
)
.as_bytes();
let started = crate::MarketPriceTimestamp::from_unix_millis(10);
let received = crate::MarketPriceTimestamp::from_unix_millis(20);
let observation = match super::parse_response(fixture, provider_id, &pair_address, started, received) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(observation.price().to_canonical_string(), "151.987654321012345678");
assert_eq!(observation.semantics(), crate::MarketPriceSemantics::DexPairUsd);
assert_eq!(observation.provider_timestamp(), std::option::Option::None);
assert_eq!(observation.provenance().as_str(), "dexscreener:solana:Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE:priceUsd");
return std::result::Result::Ok(());
}
#[test]
fn dexscreener_settings_and_descriptor_keep_pair_explicit_and_pair_endpoint_rate_limited() -> ksp_core_lib::Result<()> {
assert!(crate::MarketPriceDexScreenerSettings::new(true, "not-a-pubkey").is_err());
let settings = match crate::MarketPriceDexScreenerSettings::new(true, TEST_PAIR) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(settings.pair_address().to_string(), TEST_PAIR);
let request = match super::build_request(settings.pair_address()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(request.url_for_test().as_str(), "https://api.dexscreener.com/latest/dex/pairs/solana/Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE");
let provider = match crate::MarketPriceDexScreenerProvider::new(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::None);
assert_eq!(provider.descriptor().semantics(), crate::MarketPriceSemantics::DexPairUsd);
assert_eq!(provider.descriptor().rate_limit().requests(), std::option::Option::Some(300));
assert_eq!(provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(60));
assert_eq!(provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Unspecified);
return std::result::Result::Ok(());
}
#[test]
fn dexscreener_rejects_wrong_chain_pair_base_identity_multiple_pairs_and_missing_price() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::DEXSCREENER_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pair_address = match TEST_PAIR.parse::<ksp_core_lib::Pubkey>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(super::provider_settings_error("test_pair")),
};
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(10);
let wrong_chain = concat!(
r#"{"pairs":[{"chainId":"ethereum","pairAddress":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","#,
r#""baseToken":{"address":"So11111111111111111111111111111111111111112"},"priceUsd":"151.98"}]}"#
)
.as_bytes();
assert!(super::parse_response(wrong_chain, provider_id.clone(), &pair_address, timestamp, timestamp).is_err());
let wrong_pair = concat!(
r#"{"pairs":[{"chainId":"solana","pairAddress":"58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2","#,
r#""baseToken":{"address":"So11111111111111111111111111111111111111112"},"priceUsd":"151.98"}]}"#
)
.as_bytes();
assert!(super::parse_response(wrong_pair, provider_id.clone(), &pair_address, timestamp, timestamp).is_err());
let wrong_base = concat!(
r#"{"pairs":[{"chainId":"solana","pairAddress":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","#,
r#""baseToken":{"address":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"},"priceUsd":"1.00"}]}"#
)
.as_bytes();
assert!(super::parse_response(wrong_base, provider_id.clone(), &pair_address, timestamp, timestamp).is_err());
let multiple = concat!(
r#"{"pairs":[{"chainId":"solana","pairAddress":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","#,
r#""baseToken":{"address":"So11111111111111111111111111111111111111112"},"priceUsd":"151.98"},{"#,
r#""chainId":"solana","pairAddress":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","#,
r#""baseToken":{"address":"So11111111111111111111111111111111111111112"},"priceUsd":"151.98"}]}"#
)
.as_bytes();
assert!(super::parse_response(multiple, provider_id.clone(), &pair_address, timestamp, timestamp).is_err());
let missing_price = concat!(
r#"{"pairs":[{"chainId":"solana","pairAddress":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","#,
r#""baseToken":{"address":"So11111111111111111111111111111111111111112"},"priceUsd":null}]}"#
)
.as_bytes();
assert!(super::parse_response(missing_price, provider_id, &pair_address, timestamp, timestamp).is_err());
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,81 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_jupiter.rs
// version: 1
#[test]
fn jupiter_fixture_maps_exact_heuristic_price_and_block_provenance_without_fake_timestamp() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::JUPITER_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture = concat!(
r#"{"So11111111111111111111111111111111111111112":{"createdAt":"2024-06-05T08:55:25.527Z","liquidity":621679197.67,"#,
r#""usdPrice":151.987654321012345678,"blockId":348004023,"decimals":9,"priceChange24h":1.29}}"#
)
.as_bytes();
let started = crate::MarketPriceTimestamp::from_unix_millis(10);
let received = crate::MarketPriceTimestamp::from_unix_millis(20);
let observation = match super::parse_response(fixture, provider_id, started, received) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(observation.price().to_canonical_string(), "151.987654321012345678");
assert_eq!(observation.semantics(), crate::MarketPriceSemantics::SolanaHeuristic);
assert_eq!(observation.provider_timestamp(), std::option::Option::None);
assert_eq!(observation.provenance().as_str(), "jupiter:price_v3:So11111111111111111111111111111111111111112:block:348004023");
return std::result::Result::Ok(());
}
#[test]
fn jupiter_modes_match_current_keyless_and_free_rate_limits_and_redact_key() -> ksp_core_lib::Result<()> {
let keyless = match crate::MarketPriceJupiterSettings::keyless(true) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let keyless_provider = match crate::MarketPriceJupiterProvider::new(keyless) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(keyless_provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::None);
assert_eq!(keyless_provider.descriptor().semantics(), crate::MarketPriceSemantics::SolanaHeuristic);
assert_eq!(keyless_provider.descriptor().rate_limit().requests(), std::option::Option::Some(1));
assert_eq!(keyless_provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(2));
assert_eq!(keyless_provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Unspecified);
let free = match crate::MarketPriceJupiterSettings::free(true, std::option::Option::Some("jup-free-canary-secret".to_owned())) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!std::format!("{free:?}").contains("jup-free-canary-secret"));
let request = match super::build_request(&free) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(request.has_header_for_test(super::JUPITER_API_KEY_HEADER));
assert_eq!(request.url_for_test().query(), std::option::Option::Some("ids=So11111111111111111111111111111111111111112"));
let free_provider = match crate::MarketPriceJupiterProvider::new(free) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(free_provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::RequiredApiKey);
assert_eq!(free_provider.descriptor().rate_limit().requests(), std::option::Option::Some(1));
assert_eq!(free_provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(1));
assert_eq!(free_provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Account);
return std::result::Result::Ok(());
}
#[test]
fn jupiter_rejects_keyless_credential_missing_price_wrong_identity_and_wrong_decimals() -> ksp_core_lib::Result<()> {
assert!(crate::MarketPriceJupiterSettings::free(true, std::option::Option::None).is_err());
assert!(crate::MarketPriceJupiterSettings::keyless(true).is_ok());
let provider_id = match crate::MarketPriceProviderId::new(super::JUPITER_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(10);
let missing_price = br#"{"So11111111111111111111111111111111111111112":{"usdPrice":null,"blockId":348004023,"decimals":9}}"#;
assert!(super::parse_response(missing_price, provider_id.clone(), timestamp, timestamp).is_err());
let wrong_identity = br#"{"JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN":{"usdPrice":1.0,"blockId":348004023,"decimals":6}}"#;
assert!(super::parse_response(wrong_identity, provider_id.clone(), timestamp, timestamp).is_err());
let wrong_decimals = br#"{"So11111111111111111111111111111111111111112":{"usdPrice":151.98,"blockId":348004023,"decimals":8}}"#;
assert!(super::parse_response(wrong_decimals, provider_id, timestamp, timestamp).is_err());
return std::result::Result::Ok(());
}