v0.2.11-pre.010

This commit is contained in:
2026-08-26 12:06:51 +02:00
parent 69294a153f
commit 2db3c4be78
22 changed files with 1104 additions and 56 deletions

View File

@@ -6,7 +6,7 @@ resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.11-pre.9"
version = "0.2.11-pre.10"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# ksp-config-lib
@@ -23,7 +23,8 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
- la classification `Public`, `Internal`, `Secret` ;
- les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ;
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
- l'adapter du document Transport V1/V2 vers `HttpTransportSettings` et, en V2, `WsTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
- l'adapter du document Transport V1/V2/V3 vers `HttpTransportSettings`, `WsTransportSettings` et, en V3, `YellowstoneGrpcTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
- l'adapter de `cfg.std.offchain_transport` vers `ksp_offchain_transport_lib::MarketPriceService`, avec contrôle de provenance des credentials/public fields et sans rendre les limites provider configurables ;
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
@@ -35,10 +36,12 @@ Le registre par défaut connaît :
```text
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
cfg.std.logging -> config/std.logging.json
cfg.std.offchain_transport -> config/std.offchain_transport.json
cfg.std.transport -> config/std.transport.json
cfg.std.wallet -> config/std.wallet.json
schema.composite -> config/schemas/composite.schema.json
schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.offchain_transport -> config/schemas/std.offchain_transport.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json
schema.std.wallet -> config/schemas/std.wallet.schema.json
```
@@ -65,11 +68,11 @@ Les autres crates et applications KSP ne doivent pas :
- parser ou écrire directement `.env` ;
- ouvrir directement les documents Config connus par leur filename physique ;
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
- reconstruire elles-mêmes la configuration Logging, Transport ou Wallet depuis le JSON.
- reconstruire elles-mêmes la configuration Logging, On-chain Transport, Off-chain Transport ou Wallet depuis le JSON.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport. Le document Wallet reste un contrat de chemins/profils Config et nintroduit aucune dépendance Config -> `ksp-wallet-lib`.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, de `ksp-onchain-transport-lib` pour construire le contrat runtime On-chain Transport et de `ksp-offchain-transport-lib` pour construire le service market-price dans la direction Config -> Transport. Le document Wallet reste un contrat de chemins/profils Config et nintroduit aucune dépendance Config -> `ksp-wallet-lib`.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib`, `ksp-onchain-transport-lib` et `ksp-offchain-transport-lib` ne dépendent pas de Config.
Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`.
@@ -81,7 +84,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.offchain_transport` exige une provenance `Secret` pour les API keys effectives et une provenance `Public` pour la paire DexScreener lorsqu'elle vient de l'environnement ; il ne permet ni URL provider arbitraire ni override de rate limit. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
## Documentation
@@ -90,6 +93,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V3 HTTP + WebSocket + Yellowstone gRPC, avec lecture backward des V1/V2 ;
- [`../../config/std.offchain_transport.json`](../../config/std.offchain_transport.json) — document standard Off-chain Transport V1, actuellement limité au domaine `market_price` SOL/USD ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# Utilisation de ksp-config-lib
@@ -182,6 +182,38 @@ let _ = (root, profile_subdirectory, effective);
Les chemins Wallet refusent toute valeur `KSP_SECRET_*`. Les futurs `KSP_SECRET_WALLET_PASS_*` constituent un flux de secrets distinct et ne sont pas des champs de `std.wallet.json`.
### 4.3 Construire le service Off-chain Transport depuis Config
Le document `cfg.std.offchain_transport` possède actuellement le domaine `market_price`. Config résout les placeholders, vérifie la provenance des API keys et de la paire DexScreener, puis construit directement `MarketPriceService` :
```rust
let offchain = match engine.load_resolved_offchain_transport_config(
std::option::Option::None,
&environment,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for entry in offchain.service().registry().entries() {
println!(
"{} {:?}",
entry.descriptor().display_name(),
entry.state().availability(),
);
}
```
Le profil versionné `public_keyless` ne requiert aucun secret. Le profil `all_free` utilise les credentials `KSP_SECRET_*` et la paire `KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS` inventoriés dans `.env.example`. Une API key littérale ou issue d'une provenance non secrète est refusée par l'adapter effectif ; une paire DexScreener issue d'une provenance Secret est également refusée.
Config ne permet pas de fournir `base_url`, `endpoint_url`, `rate_limit` ou `requests_per` aux branches provider. Les origines et cadences sûres restent possédées par `ksp-offchain-transport-lib`. La direction de dépendance reste donc :
```text
ksp-config-lib -> ksp-offchain-transport-lib
```
Off-chain Transport ne lit ni `.env`, ni `KSP_*`, ni les documents Config. Une application telle que la future `ksp-app-solprices-desk` peut recevoir le service déjà composé puis utiliser uniquement `registry()`, `refresh`, `refresh_many` et `refresh_all`.
## 5. Profils et composites
Pour un document standard profilé :

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/offchain_transport.rs
// version: 1
// version: 2
//! Adapter from Config-owned Off-chain Transport documents to the provider-agnostic market-price runtime service.
@@ -322,6 +322,13 @@ fn map_coingecko(
ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode::Keyless => {
ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::keyless(source.enabled)
},
_ => {
return std::result::Result::Err(
effective_error(profile, "Off-chain Transport CoinGecko access mode is not supported by this Config adapter")
.with_context("provider", "coingecko")
.with_context("field", "access_mode"),
);
},
};
return match settings {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
@@ -346,6 +353,13 @@ fn map_coinmarketcap(
ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode::Keyless => {
ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::keyless(source.enabled)
},
_ => {
return std::result::Result::Err(
effective_error(profile, "Off-chain Transport CoinMarketCap access mode is not supported by this Config adapter")
.with_context("provider", "coinmarketcap")
.with_context("field", "access_mode"),
);
},
};
return match settings {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
@@ -386,6 +400,13 @@ fn map_jupiter(
ksp_offchain_transport_lib::MarketPriceJupiterSettings::free(source.enabled, source.api_key)
},
ksp_offchain_transport_lib::MarketPriceJupiterAccessMode::Keyless => ksp_offchain_transport_lib::MarketPriceJupiterSettings::keyless(source.enabled),
_ => {
return std::result::Result::Err(
effective_error(profile, "Off-chain Transport Jupiter access mode is not supported by this Config adapter")
.with_context("provider", "jupiter")
.with_context("field", "access_mode"),
);
},
};
return match settings {
std::result::Result::Ok(value) => std::result::Result::Ok(value),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/offchain_transport.rs
// version: 1
// version: 2
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
@@ -155,6 +155,51 @@ fn dexscreener_pair_environment_must_use_public_namespace_and_disabled_pair_may_
assert!(disabled.is_ok(), "disabled DexScreener runtime settings should accept an absent pair after pre.009 capability reconciliation: {disabled:?}");
}
#[test]
fn provider_url_and_rate_limit_overrides_are_rejected_by_schema_before_runtime_mapping() {
for (provider, field, value) in [
("coingecko", "base_url", serde_json::Value::String("https://example.invalid".to_owned())),
("coinmarketcap", "rate_limit", serde_json::json!({"requests": 999999, "window_seconds": 1})),
] {
let fixture = tempfile::tempdir();
assert!(fixture.is_ok(), "temporary Config root should be creatable: {fixture:?}");
let fixture = match fixture {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let source = committed_document_value();
let mut source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
assert!(profiles.is_some(), "fixture should expose profiles");
if let std::option::Option::Some(profiles) = profiles {
let public_keyless = profiles
.iter_mut()
.find(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some("public_keyless"));
assert!(public_keyless.is_some(), "fixture should contain public_keyless profile");
if let std::option::Option::Some(public_keyless) = public_keyless {
public_keyless["market_price"][provider][field] = value;
}
}
let engine = fixture_engine_with_document(fixture.path(), &source);
assert!(engine.is_ok(), "override fixture engine should be constructible: {engine:?}");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("public_keyless"), &environment);
assert!(resolved.is_err(), "provider URL/rate-limit override must be rejected before runtime mapping: provider={provider} field={field}");
if let std::result::Result::Err(error) = resolved {
assert_eq!(error.code(), crate::ERROR_CODE_SCHEMA_VALIDATION_FAILED);
let debug = format!("{error:?}");
assert!(!debug.contains("https://example.invalid"));
}
}
}
fn assert_provider_availability(
registry: &ksp_offchain_transport_lib::MarketPriceProviderRegistry,
provider_id: &str,

View File

@@ -0,0 +1,138 @@
<!-- file: crates/ksp-offchain-transport-lib/README.md -->
<!-- version: 1 -->
# ksp-offchain-transport-lib
`ksp-offchain-transport-lib` est le propriétaire des transports et adaptations de données **off-chain** utilisés par KSP. La release `0.2.11` matérialise sa première famille fonctionnelle, `market_price`, limitée à des observations SOL/USD multi-provider.
La crate n'est pas une crate « prix uniquement ». Les responsabilités durables sont séparées par famille :
```text
http_* mécanique HTTP partagée réellement commune
market_price_* prix de marché / spot normalisés
swap_quote_* future famille de quotes montant/route
<future_capability>_* ajoutée uniquement lorsqu'un scope réel l'exige
```
## Contrat `market_price` V1
La façade publique fournit :
- `MarketPriceDecimal`, représentation décimale exacte positive sans vérité canonique `f64` ;
- `MarketPriceObservation`, avec paire, prix, sémantique, timestamps KSP/provider et provenance sûre ;
- `MarketPriceProviderDescriptor` et `MarketPriceProviderState`, pour décrire capacités et availability sans logique provider côté consumer ;
- `MarketPriceProviderRegistry`, inventaire déterministe des providers configurés ;
- `MarketPriceService`, façade provider-agnostic pour `refresh`, `refresh_many` et `refresh_all` ;
- `MarketPriceProviderSetup`, frontière de composition initiale provider-specific qui ne doit pas devenir la surface runtime de la HID.
La paire publique V1 est exclusivement :
```text
SOL/USD
```
Les enums publiques susceptibles d'évoluer sont `#[non_exhaustive]`. Un consumer externe doit donc conserver une branche future-safe et ne pas supposer que les paires, sémantiques, états ou providers resteront définitivement fermés à ceux de `0.2.11`.
## Providers V1
L'inventaire fonctionnel comporte exactement huit adapters :
```text
birdeye
coinbase_exchange
coingecko
coinmarketcap
coinpaprika
dexscreener
jupiter
kraken
```
Ils ne prétendent pas produire la même vérité de marché. `MarketPriceSemantics` conserve notamment la différence entre agrégateur, dernier trade d'exchange, heuristique Solana, spot Solana et paire DEX.
Les origines HTTPS, chemins, headers d'authentification, identités d'asset et limites provider restent possédés par les adapters. V1 n'expose aucune URL provider arbitraire ni aucun SDK fournisseur.
DexScreener reste un cas volontairement strict : une paire Solana explicite est fournie à la composition lorsqu'il est activé ; l'adapter appelle uniquement la paire configurée et ne découvre, ne classe ni n'agrège automatiquement des pools.
## HTTP et résilience
Les primitives `http_*` sont crate-private. Elles appliquent notamment :
```text
reqwest uniquement
HTTPS provider fixe
redirects désactivés
Referer automatique désactivé
proxy système implicite désactivé
retries reqwest implicites désactivés
connect/request timeouts bornés
body borné pendant la lecture
JSON validé avant mapping typed
URL retirée des erreurs reqwest
aucun body distant brut dans KspError
429 et Retry-After classés
```
Le rate limiting est provider-owned et non bloquant. Un provider non éligible est projeté en availability/cooldown ; le service ne dort pas pour attendre sa prochaine fenêtre.
## Refresh individuel et multiple
`MarketPriceService` est la surface runtime générique.
La V1 garde un comportement multiple **séquentiel et déterministe** :
```text
refresh(provider_id) un provider opaque
refresh_many(provider_ids) ordre demandé conservé
refresh_all() ordre stable du registry
```
Un provider en cooldown ou en erreur n'empêche pas la projection des autres outcomes. Le service ne fait aucun fallback, aucun consensus et aucune agrégation de prix entre providers.
Le séquentiel de V1 est un contrat volontaire de simplicité et de déterminisme, pas une obligation architecturale éternelle. Une évolution vers une orchestration concurrente demanderait un contrat explicite sur l'ordre, les limites et les effets observables.
## Config et secrets
Cette crate **ne lit jamais** directement `KSP_*`, `KSPB_*`, `.env` ou les documents Config.
La direction autorisée est :
```text
ksp-config-lib
-> ksp-offchain-transport-lib
```
`ksp-config-lib` résout les credentials, vérifie leur provenance et construit `MarketPriceProviderSetup` / `MarketPriceService`. La dépendance inverse est interdite.
Les API keys ne sont pas exposées par les projections publiques usuelles et leurs `Debug` sont redacted. Les erreurs/logs n'embarquent ni credential, ni URL sensible, ni payload distant brut.
## Numeric safety et provenance
`MarketPriceDecimal` accepte les formes décimales/scientifiques bornées nécessaires aux wire providers, puis normalise vers un coefficient `u128` et une scale limitée. Sont rejetés notamment : zéro pour une observation réussie, négatifs, valeurs non numériques, overflow, scale excessive et exposants pathologiques.
Les timestamps provider ne sont présents que lorsqu'un provider fournit réellement une information temporelle correspondant au prix. Un block id, une date de création d'asset ou une donnée de récence non temporelle n'est jamais convertie en faux timestamp.
La provenance textuelle est bornée et contrôlée afin de rester sûre pour les projections/logs.
## Hors scope de `0.2.11`
```text
SOL/EUR
fallback automatique
consensus ou moyenne multi-provider
découverte automatique de pool DexScreener
scheduler périodique
historique persistant
swap routing / Jupiter quote
soumission ou signature de transaction Solana
SDK provider
URL provider configurable
```
## Documentation
- [`USAGE.md`](USAGE.md) — construction programmatique et utilisation de la façade générique ;
- [`../../docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md`](../../docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md) — plan de release ;
- [`../../docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md`](../../docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md) — matrice de validation.
Ce README est un **brouillon technique de `pre.010`**. La réconciliation documentaire finale reste la responsabilité de `pre.012`, après le gate technique/live de `pre.011`.

View File

@@ -0,0 +1,152 @@
<!-- file: crates/ksp-offchain-transport-lib/USAGE.md -->
<!-- version: 1 -->
# Usage de ksp-offchain-transport-lib
Cette page montre la surface technique disponible à `0.2.11-pre.010`. Les credentials et documents runtime doivent normalement être résolus par `ksp-config-lib`; les constructions directes ci-dessous servent surtout aux tests, outils bas niveau et compositions programmatiques maîtrisées.
## 1. Construire un service programmatique
Un service reçoit une liste de `MarketPriceProviderSetup`. Le setup est provider-specific uniquement au moment de la composition :
```rust
let coingecko = ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::keyless(true)?;
let kraken = ksp_offchain_transport_lib::MarketPriceKrakenSettings::new(true)?;
let service = ksp_offchain_transport_lib::MarketPriceService::new(std::vec![
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinGecko(coingecko),
ksp_offchain_transport_lib::MarketPriceProviderSetup::Kraken(kraken),
])?;
```
Une application normale n'a pas à reproduire le mapping des huit providers. `ksp-config-lib::ResolvedOffchainTransportConfig` construit cette composition depuis `cfg.std.offchain_transport`.
## 2. Découvrir le registry générique
Après construction, le consumer travaille sur le registry sans matcher les variants provider :
```rust
for entry in service.registry().entries() {
let descriptor = entry.descriptor();
let state = entry.state();
println!(
"{} {:?} {:?}",
descriptor.display_name(),
descriptor.semantics(),
state.availability(),
);
}
```
Le `provider_id` est opaque. Il sert d'identité stable pour rappeler le service, pas de signal autorisant le consumer à reconstruire un endpoint ou une règle provider.
## 3. Rafraîchir un provider
```rust
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new("coingecko")?;
let outcome = service.refresh(&provider_id).await?;
```
L'outcome expose génériquement l'observation éventuelle et l'état provider. Une erreur provider normalisée n'oblige pas le consumer à parser CoinGecko, Kraken ou Jupiter.
Avant un refresh, l'état peut être consulté via le registry. `MarketPriceProviderAvailability::retry_at()` expose le prochain instant connu lorsqu'il existe réellement.
## 4. Rafraîchir plusieurs providers
```rust
let ids = service
.registry()
.entries()
.iter()
.map(|entry| return entry.descriptor().id().clone())
.collect::<std::vec::Vec<_>>();
let outcomes = service.refresh_many(ids.as_slice()).await?;
```
La V1 exécute ce batch **séquentiellement**, dans l'ordre demandé. Elle valide les IDs avant le premier dispatch, rejette les doublons, ne dort pas pour un cooldown et produit un outcome générique par provider lorsque l'opération est valide.
`refresh_all()` applique le même contrat dans l'ordre stable du registry :
```rust
let outcomes = service.refresh_all().await?;
```
Il n'existe pas de fallback, consensus ou moyenne implicite. Une application voulant comparer les observations doit conserver leurs sémantiques et provenances ; elle ne doit pas présenter leurs différences comme une erreur de KSP.
## 5. Construire depuis Config
La voie runtime normale est :
```rust
let resolved = engine.load_resolved_offchain_transport_config(
std::option::Option::None,
&environment,
)?;
let service = resolved.service();
let registry = service.registry();
```
Le profil `public_keyless` du document standard peut être résolu sans credentials. Le profil `all_free` attend les secrets/public fields inventoriés dans `.env.example` et validés par `ksp-config-lib`.
Off-chain Transport ne lit pas l'environnement lui-même. Ne passez pas un credential via une URL, une query arbitraire ou une surface UI libre pour contourner Config.
## 6. DexScreener
DexScreener doit recevoir la paire SOL/USD Solana explicitement approuvée par la composition lorsqu'il est activé :
```rust
let pair = ksp_core_lib::Pubkey::parse("<PAIR_ADDRESS_APPROUVEE>")?;
let settings = ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(
true,
std::option::Option::Some(pair),
)?;
```
La paire n'est pas un secret. Aucun helper V1 ne découvre automatiquement une autre pool, ne trie par liquidité ou ne remplace la paire configurée.
## 7. Exactitude numérique
Ne convertissez pas l'observation canonique en `f64` pour la stocker ou la comparer comme vérité KSP. `MarketPriceDecimal` conserve une forme décimale exacte et sérialise une représentation canonique.
Pour l'affichage, un consumer peut utiliser sa représentation textuelle publique. Toute conversion approximative éventuelle appartient à une couche de présentation qui accepte explicitement cette perte ; elle ne doit pas remplacer le type canonique dans le transport.
## 8. Forward compatibility
Les enums publiques susceptibles d'évoluer sont `#[non_exhaustive]`. Hors de la crate, les matches doivent donc prévoir un fallback :
```rust
match entry.state().availability() {
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready => {},
ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled => {},
_ => {},
}
```
La branche `_` est intentionnelle : de nouveaux providers, états, sémantiques ou paires pourront être ajoutés sans imposer une rupture source aux consumers bien écrits.
## 9. Diagnostics sûrs
Les diagnostics applicatifs peuvent journaliser :
```text
provider_id validé
code d'erreur KSP
classe d'availability
retry_at borné lorsqu'il existe
durée/opération générique
```
Ils ne doivent pas journaliser :
```text
API key
URL complète sensible
header provider secret
body distant brut
payload de Config secret
```
`ksp-offchain-transport-lib` utilise `ksp-logging-lib` et son `TRACING_TARGET` propriétaire ; une application ne doit pas ajouter un bypass direct `tracing` pour obtenir les payloads rejetés.
## 10. Statut documentaire
Ce document est un **brouillon technique de `pre.010`**. Il ne vaut ni smoke live, ni garantie commerciale sur les plans gratuits des providers. `pre.011` porte les contrôles techniques/live finaux ; `pre.012` porte la réconciliation documentaire finale.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/lib.rs
// version: 10
// version: 11
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -7,10 +7,11 @@
//! KSP-owned off-chain transport foundation.
//!
//! `0.2.11-pre.008` adds the generic SOL/USD refresh service over the complete eight-provider adapter inventory. The service owns provider dispatch,
//! non-blocking rate-limit/cooldown handling, provider-neutral availability transitions and individual/multiple/all refresh operations. Provider-specific setup
//! remains confined to composition while runtime consumers operate only on opaque provider identifiers, registry projections and normalized outcomes. Config
//! integration remains reserved for the next tranche.
//! `0.2.11-pre.010` hardens the complete SOL/USD V1 surface over eight providers. The crate owns exact decimal normalization, provider adapters, hardened
//! HTTP, non-blocking rate-limit/cooldown handling, provider-neutral registry/availability and individual/multiple/all refresh operations. Provider-specific
//! setup remains confined to composition while runtime consumers can operate on opaque provider identifiers, registry projections and normalized outcomes.
//! Config integration is implemented externally by `ksp-config-lib`; this crate remains independent from Config and never reads KSP environment variables
//! directly.
mod constants;
mod error;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_adapter.rs
// version: 1
// version: 2
//! Shared market-price adapter mechanics layered over crate-wide HTTP primitives.
@@ -153,3 +153,6 @@ fn retry_after_from_error(error: &ksp_core_lib::Error) -> std::option::Option<st
}
return std::option::Option::None;
}
#[cfg(test)]
#[path = "../unit_tests/market_price_adapter.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_coingecko.rs
// version: 2
// version: 3
//! CoinGecko SOL/USD market-price adapter using the official REST API directly through `reqwest`.
@@ -8,6 +8,7 @@ const COINGECKO_PROVIDER_ID: &str = "coingecko";
const COINGECKO_SIMPLE_PRICE_URL: &str = "https://api.coingecko.com/api/v3/simple/price";
/// CoinGecko V1 access mode supported by Off-chain Transport.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceCoinGeckoAccessMode {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_coinmarketcap.rs
// version: 2
// version: 3
//! CoinMarketCap SOL/USD market-price adapter using the current Simple Price V2 REST surface.
@@ -10,6 +10,7 @@ const COINMARKETCAP_PROVIDER_ID: &str = "coinmarketcap";
const COINMARKETCAP_SOL_ID: u64 = 5_426;
/// CoinMarketCap V1 access mode supported by Off-chain Transport.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceCoinMarketCapAccessMode {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_jupiter.rs
// version: 1
// version: 2
//! Jupiter Price V3 SOL/USD adapter using the current Developer Platform REST surface.
@@ -10,6 +10,7 @@ const JUPITER_SOL_DECIMALS: u8 = 9;
const JUPITER_SOL_MINT: &str = "So11111111111111111111111111111111111111112";
/// Jupiter V1 access mode supported by Off-chain Transport.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceJupiterAccessMode {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_provider.rs
// version: 5
// version: 6
/// Maximum UTF-8 byte length of one provider display name.
pub const MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96;
@@ -7,6 +7,7 @@ pub const MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96;
pub const MARKET_PRICE_PROVIDER_ID_MAX_BYTES: usize = 64;
/// Only price pair exposed by the `0.2.11` V1 public contract.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPricePair {
@@ -25,6 +26,7 @@ impl MarketPricePair {
}
/// Market-price semantics retained so normalized observations do not imply cross-provider equivalence.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceSemantics {
@@ -41,6 +43,7 @@ pub enum MarketPriceSemantics {
}
/// Generic authentication capability exposed by one configured provider.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceProviderAuthMode {
@@ -53,6 +56,7 @@ pub enum MarketPriceProviderAuthMode {
}
/// Scope to which a provider documents a request limit.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceProviderRateLimitScope {
@@ -67,6 +71,7 @@ pub enum MarketPriceProviderRateLimitScope {
}
/// Shape of one generic provider request-limit capability.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceProviderRateLimitKind {
@@ -150,6 +155,7 @@ impl MarketPriceProviderRateLimit {
}
/// Period used by one documented long-term provider quota.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceProviderQuotaPeriod {
@@ -160,6 +166,7 @@ pub enum MarketPriceProviderQuotaPeriod {
}
/// Unit used by one documented long-term provider quota.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MarketPriceProviderQuotaUnit {
@@ -386,6 +393,7 @@ impl MarketPriceProviderDescriptor {
}
/// Generic runtime availability state exposed without provider-specific error parsing.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum MarketPriceProviderAvailability {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_service.rs
// version: 2
// version: 3
//! Generic market-price refresh service owning provider dispatch and availability transitions.
@@ -9,6 +9,7 @@ const MARKET_PRICE_REFRESH_MAX_PROVIDERS: usize = 64;
/// Provider-specific runtime setup consumed once by [`crate::MarketPriceService`].
///
/// This enum is intended for composition layers such as Config. Runtime consumers use the generic service methods and never need to branch on provider kinds.
#[non_exhaustive]
pub enum MarketPriceProviderSetup {
/// Birdeye Standard setup.
Birdeye(crate::MarketPriceBirdeyeSettings),

View File

@@ -0,0 +1,161 @@
// file: crates/ksp-offchain-transport-lib/tests/release_completeness.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Release-level completeness canaries for the eight-provider SOL/USD V1 contract.
#[test]
fn pre_010_exact_eight_provider_inventory_and_semantics_are_stable() -> ksp_core_lib::Result<()> {
let setups = match all_disabled_setups() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let service = match ksp_offchain_transport_lib::MarketPriceService::new(setups) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = service.registry();
assert_eq!(registry.len(), 8);
let expected = [
(
"birdeye",
ksp_offchain_transport_lib::MarketPriceSemantics::SolanaSpot,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::RequiredApiKey,
),
(
"coinbase_exchange",
ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None,
),
(
"coingecko",
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None,
),
(
"coinmarketcap",
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None,
),
(
"coinpaprika",
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None,
),
("dexscreener", ksp_offchain_transport_lib::MarketPriceSemantics::DexPairUsd, ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None),
("jupiter", ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic, ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None),
("kraken", ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade, ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None),
];
for (entry, (provider_id, semantics, auth_mode)) in registry.entries().iter().zip(expected) {
assert_eq!(entry.descriptor().id().as_str(), provider_id);
assert_eq!(entry.descriptor().semantics(), semantics);
assert_eq!(entry.descriptor().auth_mode(), auth_mode);
assert!(entry.descriptor().supports_sol_usd());
assert_eq!(entry.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
}
return std::result::Result::Ok(());
}
#[test]
fn pre_010_evolutive_public_enums_are_non_exhaustive() -> std::io::Result<()> {
let root = crate_root();
let provider = match std::fs::read_to_string(root.join("src/market_price_provider.rs")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for enum_name in [
"MarketPricePair",
"MarketPriceSemantics",
"MarketPriceProviderAuthMode",
"MarketPriceProviderRateLimitScope",
"MarketPriceProviderRateLimitKind",
"MarketPriceProviderQuotaPeriod",
"MarketPriceProviderQuotaUnit",
"MarketPriceProviderAvailability",
] {
assert_non_exhaustive(provider.as_str(), enum_name);
}
let coingecko = match std::fs::read_to_string(root.join("src/market_price_coingecko.rs")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_non_exhaustive(coingecko.as_str(), "MarketPriceCoinGeckoAccessMode");
let coinmarketcap = match std::fs::read_to_string(root.join("src/market_price_coinmarketcap.rs")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_non_exhaustive(coinmarketcap.as_str(), "MarketPriceCoinMarketCapAccessMode");
let jupiter = match std::fs::read_to_string(root.join("src/market_price_jupiter.rs")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_non_exhaustive(jupiter.as_str(), "MarketPriceJupiterAccessMode");
let service = match std::fs::read_to_string(root.join("src/market_price_service.rs")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_non_exhaustive(service.as_str(), "MarketPriceProviderSetup");
return std::result::Result::Ok(());
}
fn all_disabled_setups() -> ksp_core_lib::Result<std::vec::Vec<ksp_offchain_transport_lib::MarketPriceProviderSetup>> {
let birdeye = match ksp_offchain_transport_lib::MarketPriceBirdeyeSettings::new(false, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinbase = match ksp_offchain_transport_lib::MarketPriceCoinbaseExchangeSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coingecko = match ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinmarketcap = match ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinpaprika = match ksp_offchain_transport_lib::MarketPriceCoinPaprikaSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dexscreener = match ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(false, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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 kraken = match ksp_offchain_transport_lib::MarketPriceKrakenSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![
ksp_offchain_transport_lib::MarketPriceProviderSetup::Birdeye(birdeye),
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinbaseExchange(coinbase),
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinGecko(coingecko),
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinMarketCap(coinmarketcap),
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinPaprika(coinpaprika),
ksp_offchain_transport_lib::MarketPriceProviderSetup::DexScreener(dexscreener),
ksp_offchain_transport_lib::MarketPriceProviderSetup::Jupiter(jupiter),
ksp_offchain_transport_lib::MarketPriceProviderSetup::Kraken(kraken),
]);
}
fn assert_non_exhaustive(source: &str, enum_name: &str) {
let marker = std::format!("pub enum {enum_name}");
let position = source.find(marker.as_str());
assert!(position.is_some(), "public enum must remain present: {enum_name}");
if let std::option::Option::Some(position) = position {
let start = position.saturating_sub(256);
assert!(source[start..position].contains("#[non_exhaustive]"), "evolutive public enum must be non_exhaustive: {enum_name}");
}
}
fn crate_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}

View File

@@ -0,0 +1,90 @@
// file: crates/ksp-offchain-transport-lib/tests/security_hardening.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Adversarial security canaries for Off-chain Transport diagnostics, credentials and ownership boundaries.
#[test]
fn pre_010_keyed_settings_and_service_debug_never_expose_credentials() -> ksp_core_lib::Result<()> {
let secret = "pre010-api-key-secret-canary";
let birdeye = match ksp_offchain_transport_lib::MarketPriceBirdeyeSettings::new(true, std::option::Option::Some(secret.to_owned())) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coingecko = match ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::demo(true, std::option::Option::Some(secret.to_owned())) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinmarketcap = match ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::basic(true, std::option::Option::Some(secret.to_owned())) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let jupiter = match ksp_offchain_transport_lib::MarketPriceJupiterSettings::free(true, std::option::Option::Some(secret.to_owned())) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for debug in [std::format!("{birdeye:?}"), std::format!("{coingecko:?}"), std::format!("{coinmarketcap:?}"), std::format!("{jupiter:?}")] {
assert!(!debug.contains(secret));
assert!(debug.contains("api_key_present"));
}
let service = match ksp_offchain_transport_lib::MarketPriceService::new(std::vec![
ksp_offchain_transport_lib::MarketPriceProviderSetup::Birdeye(birdeye),
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinGecko(coingecko),
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinMarketCap(coinmarketcap),
ksp_offchain_transport_lib::MarketPriceProviderSetup::Jupiter(jupiter),
]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!std::format!("{service:?}").contains(secret));
return std::result::Result::Ok(());
}
#[test]
fn pre_010_market_price_sources_do_not_read_environment_or_use_float_price_truth() -> std::io::Result<()> {
let src = crate_root().join("src");
let entries = match std::fs::read_dir(src) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("market_price_") || !name.ends_with(".rs") {
continue;
}
let source = match std::fs::read_to_string(entry.path()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!source.contains("std::env"), "market-price source must not bypass Config environment ownership: {name}");
assert!(!source.contains("as_f64("), "market-price source must not use serde_json f64 as canonical price truth: {name}");
assert!(!source.contains("tracing::"), "market-price source must use ksp-logging-lib rather than tracing directly: {name}");
}
return std::result::Result::Ok(());
}
#[test]
fn pre_010_config_schema_exposes_no_provider_url_or_rate_limit_override() -> std::io::Result<()> {
let root = crate_root().join("../..");
let schema = match std::fs::read_to_string(root.join("config/schemas/std.offchain_transport.schema.json")) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!schema.contains("base_url"));
assert!(!schema.contains("endpoint_url"));
assert!(!schema.contains("rate_limit"));
assert!(!schema.contains("requests_per"));
return std::result::Result::Ok(());
}
fn crate_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}

View File

@@ -0,0 +1,24 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_adapter.rs
// version: 1
#[test]
fn timestamp_helpers_preserve_valid_values_and_reject_malformed_pre_epoch_and_overflow() {
let epoch_fraction = crate::market_price_timestamp_from_rfc3339("1970-01-01T00:00:00.123Z");
assert_eq!(epoch_fraction.map(|value| return value.unix_millis()), std::option::Option::Some(123));
assert!(crate::market_price_timestamp_from_rfc3339("1969-12-31T23:59:59Z").is_none());
assert!(crate::market_price_timestamp_from_rfc3339("not-a-timestamp").is_none());
assert_eq!(crate::market_price_timestamp_from_unix_seconds(1).map(|value| return value.unix_millis()), std::option::Option::Some(1_000));
assert!(crate::market_price_timestamp_from_unix_seconds(u64::MAX).is_none());
}
#[test]
fn invalid_provider_response_error_contains_only_safe_provider_and_field_context() {
let error = crate::invalid_provider_response("coingecko", "price");
assert_eq!(error.code(), crate::ERROR_CODE_MARKET_PRICE_PROVIDER_RESPONSE_INVALID);
let context = error.context();
assert_eq!(context.len(), 2);
assert_eq!(context[0].key(), "provider");
assert_eq!(context[0].value(), "coingecko");
assert_eq!(context[1].key(), "field");
assert_eq!(context[1].value(), "price");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_decimal.rs
// version: 4
// version: 5
#[test]
fn decimal_normalizes_fractional_and_scientific_forms_without_f64() -> ksp_core_lib::Result<()> {
@@ -84,3 +84,19 @@ fn decimal_parses_raw_json_number_and_string_without_f64_round_trip() -> ksp_cor
assert_eq!(parsed.to_canonical_string(), "151.23");
return std::result::Result::Ok(());
}
#[test]
fn decimal_raw_json_rejects_non_numeric_and_pathological_values() {
let invalid = ["null", "true", "false", "{}", "[]", r#""0""#, r#""-1""#, r#""1e-19""#, r#""1e129""#];
for source in invalid {
let raw = serde_json::from_str::<std::boxed::Box<serde_json::value::RawValue>>(source);
assert!(raw.is_ok(), "adversarial raw JSON fixture must itself be syntactically valid: {source}");
if let std::result::Result::Ok(raw) = raw {
let parsed = crate::MarketPriceDecimal::parse_json_raw(raw.as_ref());
assert!(parsed.is_err(), "non-price raw JSON must not become a successful decimal: {source}");
if let std::result::Result::Err(error) = parsed {
assert_eq!(error.code(), crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID);
}
}
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_observation.rs
// version: 3
// version: 4
#[test]
fn observation_preserves_pair_exact_price_semantics_timestamps_and_safe_provenance() -> ksp_core_lib::Result<()> {
@@ -67,3 +67,18 @@ fn observation_rejects_reversed_ksp_timestamps_and_unsafe_provenance() -> ksp_co
assert!(result.is_err());
return std::result::Result::Ok(());
}
#[test]
fn provenance_accepts_exact_boundary_and_rejects_trim_control_and_oversize() -> ksp_core_lib::Result<()> {
let exact = "a".repeat(crate::MARKET_PRICE_PROVENANCE_MAX_BYTES);
let provenance = crate::MarketPriceProvenance::new(exact.clone());
assert!(provenance.is_ok());
if let std::result::Result::Ok(provenance) = provenance {
assert_eq!(provenance.as_str(), exact);
}
assert!(crate::MarketPriceProvenance::new(format!("{exact}a")).is_err());
assert!(crate::MarketPriceProvenance::new(" leading").is_err());
assert!(crate::MarketPriceProvenance::new("trailing ").is_err());
assert!(crate::MarketPriceProvenance::new("tab\tvalue").is_err());
return std::result::Result::Ok(());
}

309
deltas/0.2.11/pre.010.md Normal file
View File

@@ -0,0 +1,309 @@
<!-- file: deltas/0.2.11/pre.010.md -->
<!-- version: 1 -->
# Delta `0.2.11-pre.010` — Hardening et complétude technique Off-chain Transport
## 1. Base requise
Cette tranche s'applique exclusivement après l'état validé :
```text
v0.2.10
+ 0.2.11-pre.001
+ 0.2.11-pre.001-fix.001
+ 0.2.11-pre.002
+ 0.2.11-pre.002-fix.001
+ 0.2.11-pre.002-fix.002
+ 0.2.11-pre.002-fix.003
+ 0.2.11-pre.003
+ 0.2.11-pre.003-fix.001
+ 0.2.11-pre.004
+ 0.2.11-pre.004-fix.001
+ 0.2.11-pre.005
+ 0.2.11-pre.006
+ 0.2.11-pre.007
+ 0.2.11-pre.008
+ 0.2.11-pre.008-fix.001
+ 0.2.11-pre.009
```
Le gate opérateur de `pre.009` est intégralement PASS : audits Rust/Markdown, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests ciblés `ksp-offchain-transport-lib`, tests Config et `cargo test --workspace`. Seuls les smokes live et probes de timing explicitement `ignored` restent hors du gate déterministe.
La version workspace devient :
```text
0.2.11-pre.10
```
## 2. Mission
`pre.010` ne crée aucun provider ni endpoint supplémentaire. La tranche durcit l'état fonctionnel complet obtenu par `pre.009` avant le gate technique/live de `pre.011`.
Le scope est limité à :
```text
forward compatibility de la façade publique
complétude exacte des huit providers V1
hardening numeric/timestamps/provenance
redaction et ownership de diagnostics/secrets
interdiction durable des URL/rate-limit provider libres dans Config
brouillons techniques README/USAGE
correction de la documentation technique du refresh multiple V1
```
La réconciliation documentaire finale reste explicitement réservée à `pre.012`.
## 3. Forward compatibility de la façade publique
Les enums publiques dont l'espace de valeurs doit pouvoir évoluer sont désormais `#[non_exhaustive]` :
```text
MarketPricePair
MarketPriceSemantics
MarketPriceProviderAuthMode
MarketPriceProviderRateLimitScope
MarketPriceProviderRateLimitKind
MarketPriceProviderQuotaPeriod
MarketPriceProviderQuotaUnit
MarketPriceProviderAvailability
MarketPriceCoinGeckoAccessMode
MarketPriceCoinMarketCapAccessMode
MarketPriceJupiterAccessMode
MarketPriceProviderSetup
```
Cette décision évite qu'une future paire, sémantique, classe de quota, disponibilité, access mode ou provider impose une rupture source aux consumers qui respectent la façade évolutive.
Les structs à champs privés restent extensibles sans attribut supplémentaire.
`ksp-config-lib` étant une crate downstream, ses matches CoinGecko/CoinMarketCap/Jupiter absorbent maintenant explicitement les futures variantes inconnues via une erreur Config effective sûre. La Config ne suppose donc plus que ces enums resteront exhaustifs pour toujours.
## 4. Complétude release-level des huit providers
Un nouveau canari `tests/release_completeness.rs` construit les huit setups désactivés et vérifie l'inventaire exact, ordonné et provider-neutral :
```text
birdeye
coinbase_exchange
coingecko
coinmarketcap
coinpaprika
dexscreener
jupiter
kraken
```
Il verrouille pour chaque entrée :
```text
provider_id
MarketPriceSemantics
MarketPriceProviderAuthMode du mode testé
support SOL/USD
availability Disabled
```
Le même canari vérifie statiquement que les enums évolutives restent `#[non_exhaustive]`.
Aucun neuvième provider, autre paire ou abstraction wire publique n'est ajouté.
## 5. Hardening adversarial numeric, timestamps et provenance
Les tests déterministes sont complétés pour couvrir :
### 5.1 `MarketPriceDecimal`
Rejet explicite de formes JSON qui ne doivent jamais devenir un prix réussi :
```text
null
booléens
objet
tableau
zéro string
négatif string
scale/exposant pathologique
overflow d'exposant
```
Le chemin canonique reste sans `serde_json::Value::as_f64` dans les modules `market_price_*`.
### 5.2 Timestamps provider
Les helpers communs couvrent désormais :
```text
RFC3339 valide avec millisecondes fractionnelles
pré-epoch rejeté
RFC3339 invalide rejeté
Unix seconds valide
multiplication Unix seconds overflow rejetée
```
Aucun timestamp provider n'est inventé lorsqu'une source ne fournit pas une vraie information temporelle du prix.
### 5.3 Provenance
`MarketPriceProvenance` est canarisée à sa borne maximale exacte et rejette :
```text
dépassement de borne
leading/trailing whitespace
contrôle tabulation
```
Les erreurs provider invalides conservent seulement le provider et le champ sûrs dans leur contexte.
## 6. Hardening secrets, diagnostics et ownership
Le nouveau canari `tests/security_hardening.rs` vérifie notamment :
```text
Debug Birdeye/CoinGecko/CoinMarketCap/Jupiter ne contient pas la clé
Debug MarketPriceService ne contient pas la clé
aucun module market_price_* ne lit std::env
aucun module market_price_* n'utilise as_f64 comme vérité prix
aucun module market_price_* n'utilise tracing:: directement
schema Config sans base_url/endpoint_url/rate_limit/requests_per provider
```
Les origines HTTPS, headers et cadences restent donc possédés par Off-chain Transport.
## 7. Hardening Config
Un test Config adversarial injecte dans le document standard des champs non autorisés représentant :
```text
base_url provider arbitraire
rate_limit provider arbitraire
```
Le schema strict doit les rejeter avant le mapping runtime. Cette protection complète les canaries statiques et empêche qu'une future modification du document transforme silencieusement Config en bypass SSRF/credential-redirection ou en moyen d'augmenter une cadence provider.
Les access modes `#[non_exhaustive]` reçoivent aussi une branche downstream future-safe dans l'adapter Config ; aucune variante inconnue n'est interprétée arbitrairement.
## 8. Refresh multiple V1 explicitement séquentiel
Le plan d'origine conservait encore une phrase préparatoire affirmant que les providers éligibles seraient lancés concurremment. Cette phrase était devenue fausse depuis `pre.008`, qui a délibérément matérialisé V1 comme séquentielle et déterministe.
`pre.010` réconcilie le **contrat technique courant**, sans attendre la documentation finale :
```text
refresh_many valide tous les IDs avant dispatch
ordre demandé conservé
refresh_all suit l'ordre stable du registry
aucun sleep pour attendre un cooldown
provider non éligible projeté immédiatement
aucun fallback
aucun consensus
aucune agrégation
```
La concurrence n'est pas interdite pour toujours, mais elle nécessitera un contrat futur explicite sur ordre observable, admission et rate limiting.
## 9. Brouillons techniques README/USAGE
La nouvelle crate reçoit ses premiers documents techniques :
```text
crates/ksp-offchain-transport-lib/README.md
crates/ksp-offchain-transport-lib/USAGE.md
```
Ils décrivent la taxonomie hétérogène, la façade `MarketPrice*`, les huit providers, les frontières Config/HTTP, numeric safety, refresh séquentiel V1 et les règles de diagnostic.
`ksp-config-lib/README.md` et `USAGE.md` sont complétés pour documenter `cfg.std.offchain_transport`, les nouvelles ressources du registry et la direction :
```text
ksp-config-lib -> ksp-offchain-transport-lib
```
Ces textes sont explicitement qualifiés de brouillons techniques. Ils ne remplacent pas la réconciliation documentaire finale de `pre.012` et ne revendiquent aucun résultat live de `pre.011`.
## 10. Scope inchangé
La tranche n'ajoute pas :
```text
provider supplémentaire
SOL/EUR ou autre paire
endpoint nouveau
SDK provider
fallback
consensus/moyenne/médiane
scheduler périodique
concurrence de refresh multiple
cache/historique
smoke live
swap_quote
application Price Desk
```
## 11. Validations sandbox et gate opérateur demandé
Les audits exécutables dans le sandbox sur l'état final donnent :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (124 tables / 118 files)
Rust > 160 colonnes dans les crates touchées: 0
```
Cargo n'est pas disponible dans le sandbox de production du delta ; aucune compilation locale n'est donc revendiquée.
Après application :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.11
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-offchain-transport-lib
cargo test -p ksp-config-lib
cargo test --workspace
```
Le gate attendu doit rester sans warning. Les smokes live demeurent réservés à `pre.011`.
## 12. Fichiers ajoutés
```text
crates/ksp-offchain-transport-lib/README.md
crates/ksp-offchain-transport-lib/USAGE.md
crates/ksp-offchain-transport-lib/tests/release_completeness.rs
crates/ksp-offchain-transport-lib/tests/security_hardening.rs
crates/ksp-offchain-transport-lib/unit_tests/market_price_adapter.rs
deltas/0.2.11/pre.010.md
```
## 13. Fichiers modifiés
```text
Cargo.toml
crates/ksp-config-lib/README.md
crates/ksp-config-lib/USAGE.md
crates/ksp-config-lib/src/offchain_transport.rs
crates/ksp-config-lib/unit_tests/offchain_transport.rs
crates/ksp-offchain-transport-lib/src/lib.rs
crates/ksp-offchain-transport-lib/src/market_price_adapter.rs
crates/ksp-offchain-transport-lib/src/market_price_coingecko.rs
crates/ksp-offchain-transport-lib/src/market_price_coinmarketcap.rs
crates/ksp-offchain-transport-lib/src/market_price_jupiter.rs
crates/ksp-offchain-transport-lib/src/market_price_provider.rs
crates/ksp-offchain-transport-lib/src/market_price_service.rs
crates/ksp-offchain-transport-lib/unit_tests/market_price_decimal.rs
crates/ksp-offchain-transport-lib/unit_tests/market_price_observation.rs
docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md
docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md
```
## 14. Fichiers supprimés
Aucun.
## 15. Autorité du delta
Le présent delta décrit uniquement `0.2.11-pre.010`. Les résultats Cargo du gate opérateur demandé après application devront être reportés dans la validation courante ou dans un éventuel fix si un défaut réel est détecté. `pre.011` reste propriétaire du gate technique/live final ; `pre.012` de la réconciliation documentaire finale ; `pre.013` de la préparation minimale de publication.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md -->
<!-- version: 16 -->
<!-- version: 17 -->
# Plan `0.2.11` — Off-chain price transport SOL/USD multi-provider
**Statut courant : `0.2.11-pre.009` matérialise la frontière Config -> Off-chain Transport. `cfg.std.offchain_transport` et `schema.std.offchain_transport` possèdent désormais un domaine `market_price` avec les huit providers V1, des profils `public_keyless` et `all_free`, les credentials résolus exclusivement depuis une provenance Config classée Secret, et une adresse de paire DexScreener publique lorsqu'elle provient de l'environnement. `ResolvedOffchainTransportConfig` construit `MarketPriceService` sans créer de dépendance inverse depuis Off-chain Transport. Les limites provider restent possédées par la crate runtime et ne deviennent pas des cadences libres de Config.**
**Statut courant : `0.2.11-pre.010` durcit la façade et les invariants avant le gate live. Les enums publiques évolutives de `market_price` sont désormais `#[non_exhaustive]`, des canaries release-level verrouillent l'inventaire exact des huit providers, la redaction et les frontières Config/HTTP, et des tests adversariaux complètent numeric safety, timestamps et provenance. `README.md` / `USAGE.md` de la nouvelle crate sont introduits comme brouillons techniques uniquement ; la réconciliation documentaire finale reste réservée à `pre.012`. Le gate opérateur de `pre.009` est intégralement PASS.**
## 1. Base et autorité
@@ -473,18 +473,19 @@ ne contourne jamais le cooldown
### Refresh multiple/all
Un refresh multiple :
Un refresh multiple V1 :
```text
valide la sélection complète avant le premier dispatch
évalue chaque provider indépendamment
lance concurremment les providers éligibles
exécute séquentiellement dans un ordre déterministe
ne bloque pas Coinbase parce que Jupiter est en cooldown
ne dort pas pour attendre tous les providers non éligibles
ne dort pas pour attendre les providers non éligibles
retourne un résultat générique par provider
expose le prochain instant admissible pour les providers différés
```
La future UI peut donc désactiver/annoter un bouton à partir de l'état retourné sans connaître la règle provider.
Le choix séquentiel de V1 privilégie la simplicité, la reproductibilité des effets et l'absence de scheduler implicite. Il n'interdit pas une évolution concurrente future, mais une telle évolution devra définir explicitement ordre observable, admission et interactions de rate limiting au lieu d'être supposée par la HID. La future UI peut donc désactiver/annoter un bouton à partir de l'état retourné sans connaître la règle provider.
## 14. HTTP commun et résilience
@@ -769,15 +770,15 @@ Implémentation de Birdeye Standard avec `X-API-KEY`, endpoint Price Single Sola
### `pre.009` — Config Off-chain Transport
**Statut : réalisé ; gate Cargo opérateur à rejouer.**
**Statut : réalisé ; gate opérateur complet PASS.**
Ajout de `cfg.std.offchain_transport` / `schema.std.offchain_transport`, du document standard V1, de son schema strict, d'un exemple et de la fixture Config. Le domaine `market_price` configure les huit providers sans rendre leurs cadences modifiables. `public_keyless` fonctionne sans secret ; `all_free` référence quatre credentials `KSP_SECRET_*` et une paire DexScreener `KSP_PUBLIC_*`. `ResolvedOffchainTransportConfig` résout l'environnement, contrôle la provenance des credentials/public fields et construit `MarketPriceService` via `MarketPriceProviderSetup`, sans dépendance inverse. Les deux applications desktop embarquent les nouvelles ressources Config. La tranche réconcilie aussi DexScreener pour qu'une paire soit requise uniquement quand le provider est activé.
### `pre.010` — Hardening et complétude technique
**Statut : planifié.**
**Statut : réalisé côté delta ; gate Cargo opérateur à rejouer.**
Hardening de l'API publique, tests adversariaux/completeness et brouillons techniques README/USAGE sans réconciliation documentaire finale.
Durcissement de la façade publique avec `#[non_exhaustive]` sur les enums évolutives, absorption future-safe de ces enums par l'adapter Config, canaries de complétude exacte des huit providers, tests adversariaux numeric/timestamps/provenance/redaction et vérification de l'absence d'URL/rate-limit provider configurables. Les premiers `README.md` / `USAGE.md` d'Off-chain Transport et les ajouts Config associés sont des brouillons techniques ; `pre.012` reste seule propriétaire de la réconciliation documentaire finale. Le contrat `refresh_many`/`refresh_all` V1 est explicitement réaffirmé séquentiel et déterministe.
### `pre.011` — Gate technique et live final

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# Validation `0.2.11` — Off-chain price transport SOL/USD
@@ -288,30 +288,54 @@ Les tests déterministes de `pre.008` vérifient le registry de service sur les
## 3.12 Gate `0.2.11-pre.009`
| Critère | Statut | Preuve |
|----------------------------------------------------------------------|---------|----------------------------------------------------------------|
| version workspace `0.2.11-pre.9` | PASS | `Cargo.toml` racine |
| document `cfg.std.offchain_transport` enregistré | PASS | registry Config + `std.offchain_transport.json` |
| schema `schema.std.offchain_transport` enregistré | PASS | registry Config + schema Draft 2020-12 |
| domaine actuel imbriqué sous `market_price` | PASS | document/schema V1 |
| profils `public_keyless` et `all_free` versionnés | PASS | document standard + exemple |
| huit branches provider Config obligatoires | PASS | schema + fixture |
| credentials conditionnés par mode/provider | PASS | `if/then` du schema |
| credentials effectifs exigent une provenance Config Secret | PASS | adapter `offchain_transport.rs` + tests |
| littéral credential rejeté par l'adapter effectif | PASS | test `literal_or_nonsecret_provider_credentials...` |
| DexScreener pair requis seulement lorsque le provider est activé | PASS | schema + `MarketPriceDexScreenerSettings` |
| Dex pair via environnement exige une provenance Public | PASS | adapter Config + test |
| `.env.example` inventorie quatre secrets et la paire publique | PASS | `.env.example` |
| Config construit `MarketPriceService` via `MarketPriceProviderSetup` | PASS | `ResolvedOffchainTransportConfig` |
| Off-chain Transport ne dépend pas de Config | PASS | dependency ownership + manifest |
| aucune cadence provider libre ajoutée à Config | PASS | schema/document sans rate-limit override |
| Config Debug/safe view ne divulgue pas les API keys | PASS | tests adapter |
| applications desktop embarquent document + schema | PASS | deux `tauri.conf.json` + canaries desktop |
| audit Rust sandbox | PASS | clean, 0 candidate export |
| audit Markdown sandbox | PASS | clean, 123 tables / 115 fichiers |
| `cargo check` / Clippy / tests Config et workspace | PLANNED | gate opérateur requis après application de l'archive `pre.009` |
| Critère | Statut | Preuve |
|----------------------------------------------------------------------|--------|-----------------------------------------------------|
| version workspace `0.2.11-pre.9` | PASS | `Cargo.toml` racine |
| document `cfg.std.offchain_transport` enregistré | PASS | registry Config + `std.offchain_transport.json` |
| schema `schema.std.offchain_transport` enregistré | PASS | registry Config + schema Draft 2020-12 |
| domaine actuel imbriqué sous `market_price` | PASS | document/schema V1 |
| profils `public_keyless` et `all_free` versionnés | PASS | document standard + exemple |
| huit branches provider Config obligatoires | PASS | schema + fixture |
| credentials conditionnés par mode/provider | PASS | `if/then` du schema |
| credentials effectifs exigent une provenance Config Secret | PASS | adapter `offchain_transport.rs` + tests |
| littéral credential rejeté par l'adapter effectif | PASS | test `literal_or_nonsecret_provider_credentials...` |
| DexScreener pair requis seulement lorsque le provider est activé | PASS | schema + `MarketPriceDexScreenerSettings` |
| Dex pair via environnement exige une provenance Public | PASS | adapter Config + test |
| `.env.example` inventorie quatre secrets et la paire publique | PASS | `.env.example` |
| Config construit `MarketPriceService` via `MarketPriceProviderSetup` | PASS | `ResolvedOffchainTransportConfig` |
| Off-chain Transport ne dépend pas de Config | PASS | dependency ownership + manifest |
| aucune cadence provider libre ajoutée à Config | PASS | schema/document sans rate-limit override |
| Config Debug/safe view ne divulgue pas les API keys | PASS | tests adapter |
| applications desktop embarquent document + schema | PASS | deux `tauri.conf.json` + canaries desktop |
| audit Rust sandbox | PASS | clean, 0 candidate export |
| audit Markdown sandbox | PASS | clean, 123 tables / 115 fichiers |
| `cargo check` / Clippy / tests Config et workspace | PASS | gate opérateur complet du 2026-08-26 |
La tranche n'ajoute aucune URL provider configurable, aucun SDK provider et aucune lecture directe de l'environnement dans `ksp-offchain-transport-lib`. Les secrets restent résolus par `ksp-config-lib`; le service runtime reçoit uniquement les settings typés nécessaires. La modification DexScreener est capability-aware : une paire absente est valide uniquement lorsque l'adapter est désactivé, ce qui permet de conserver son entrée dans l'inventaire générique sans fabriquer de fausse configuration.
La tranche n'ajoute aucune URL provider configurable, aucun SDK provider et aucune lecture directe de l'environnement dans `ksp-offchain-transport-lib`. Les secrets restent résolus par `ksp-config-lib`; le service runtime reçoit uniquement les settings typés nécessaires. La modification DexScreener est capability-aware : une paire absente est valide uniquement lorsque l'adapter est désactivé, ce qui permet de conserver son entrée dans l'inventaire générique sans fabriquer de fausse configuration. Le gate opérateur de `pre.009` est intégralement PASS : audits Rust/Markdown, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests ciblés Off-chain, 119 tests unitaires Config et `cargo test --workspace`. Seuls les smokes live et probes de timing explicitement `ignored` restent non exécutés par le gate déterministe.
## 3.13 Gate `0.2.11-pre.010`
| Critère | Statut | Preuve |
|------------------------------------------------------------------------|---------|------------------------------------------------------|
| version workspace `0.2.11-pre.10` | PASS | `Cargo.toml` racine |
| enums publiques évolutives marquées `#[non_exhaustive]` | PASS | provider/access modes/setup + canari completeness |
| adapter Config future-safe face aux access modes non exhaustifs | PASS | branches wildcard vers erreur effective sûre |
| inventaire exact des huit providers et sémantiques verrouillé | PASS | `tests/release_completeness.rs` |
| aucun environnement lu par les modules `market_price_*` | PASS | `tests/security_hardening.rs` |
| aucun `as_f64` utilisé comme vérité de prix `market_price` | PASS | `tests/security_hardening.rs` |
| aucun `tracing::` direct dans `market_price_*` | PASS | `tests/security_hardening.rs` |
| Debug keyed settings/service ne divulgue pas les API keys | PASS | canari secret déterministe |
| Config rejette URL provider et override de rate limit | PASS | schema strict + test Config adversarial |
| timestamps RFC3339/pre-epoch/overflow durcis | PASS | unit test `market_price_adapter` |
| JSON numeric pathologique/null/non-numérique rejeté | PASS | unit test `market_price_decimal` |
| provenance accepte la borne et rejette trim/control/oversize | PASS | unit test `market_price_observation` |
| `refresh_many` / `refresh_all` V1 documentés séquentiels/déterministes | PASS | plan + `README.md` / `USAGE.md` Off-chain |
| README/USAGE techniques créés sans usurper la réconciliation finale | PASS | crate Off-chain + compléments Config |
| audit Rust sandbox | PLANNED | à exécuter sur l'état final de l'archive |
| audit Markdown sandbox | PLANNED | à exécuter sur l'état final de l'archive |
| `cargo check` / Clippy / tests ciblés et workspace | PLANNED | gate opérateur requis après application de `pre.010` |
`pre.010` ne fait aucun smoke live, ne change aucun endpoint provider et n'introduit ni fallback, ni consensus, ni scheduler. Les documents crate créés ici sont des brouillons techniques courants ; `pre.012` reste seule propriétaire de la réconciliation documentaire finale après le gate technique/live de `pre.011`.
## 4. Matrice provider prévue