# Delta `0.2.11-pre.008` — Refresh individuel + multiple/all ## 1. Base requise Cette tranche s'applique exclusivement après `0.2.11-pre.007` accepté par l'opérateur. Version Cargo attendue à l'entrée : ```text 0.2.11-pre.7 ``` Version Cargo de sortie : ```text 0.2.11-pre.8 ``` Le gate opérateur du `2026-08-26` est entièrement PASS : `cargo fmt --all`, audit Rust clean avec 0 candidate export, audit Markdown clean à 121 tables / 112 fichiers, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-offchain-transport-lib` avec 49 unitaires + 5 boundary + 6 public API, puis `cargo test --workspace` complet PASS avec uniquement les smokes/diagnostics explicitement ignorés. ## 2. Objet strict de la tranche `pre.008` matérialise l'orchestrateur générique de la famille `market_price` : ```text MarketPriceService MarketPriceProviderSetup MarketPriceRefreshOutcome refresh(provider_id) refresh_many(provider_ids) refresh_all() transitions runtime d'availability ``` La tranche n'ajoute pas : ```text Config Off-chain Transport lecture directe d'environnement scheduler périodique fallback provider consensus / moyenne / agrégation de prix smoke live provider nouvelle famille off-chain ``` ## 3. Frontière composition / runtime `MarketPriceProviderSetup` est l'unique enum provider-specific introduit par l'orchestrateur. Il consomme les settings déjà publics des huit adapters V1 et est destiné à la couche de composition, notamment au futur adapter Config de `pre.009`. Une fois le service construit, les opérations runtime ne prennent plus aucun type provider-specific : ```text registry() refresh(&MarketPriceProviderId) refresh_many(&[MarketPriceProviderId]) refresh_all() ``` La future HID peut donc rester indépendante de CoinGecko, Birdeye, Kraken, Jupiter ou de tout autre provider concret. ## 4. Registry runtime et availability `MarketPriceService` possède les adapters concrets et une copie mutable interne du `MarketPriceProviderRegistry`. Le registry public reste une projection détachable `descriptor + state`. Deux helpers crate-private permettent au service de remplacer l'availability sans modifier l'identité du provider. `MarketPriceProviderAvailability` gagne : ```text is_refresh_eligible_at(now) ``` Cette méthode autorise : ```text Ready CoolingDown dont retry_at est expiré TemporarilyUnavailable sans retry_at TemporarilyUnavailable dont retry_at est expiré ``` et bloque : ```text AuthenticationUnavailable Disabled Misconfigured QuotaUnavailable CoolingDown non expiré TemporarilyUnavailable avec retry_at futur ``` Aucun timer/sleep n'est lancé pour rendre un provider éligible : l'état est simplement réévalué lors du prochain appel explicite. ## 5. Classification générique des erreurs Le service absorbe les erreurs provider/HTTP après dispatch et retourne un `MarketPriceRefreshOutcome` générique. Les classes principales sont : ```text provider disabled -> Disabled 401/403 sur mode avec API key -> AuthenticationUnavailable 401/403 sur mode keyless -> TemporarilyUnavailable admission locale deferred -> CoolingDown HTTP 429 -> CoolingDown HTTP 408/5xx -> TemporarilyUnavailable client/settings/request localement invalides -> Misconfigured autre erreur provider/transport -> TemporarilyUnavailable ``` Un `Retry-After` exploitable ou un délai local de defer est converti en `retry_at` absolu. Aucun message/body provider n'est projeté au consumer ; le code d'erreur est seulement journalisé via `ksp-logging-lib` avec le provider id validé. `QuotaUnavailable` reste une catégorie publique mais n'est pas inventée automatiquement lorsque le provider ne fournit pas une preuve exploitable de quota épuisé. ## 6. Refresh individuel `MarketPriceService::refresh` : ```text résout le provider par identifiant opaque projette immédiatement les états non éligibles n'effectue aucun sleep appelle uniquement l'adapter propriétaire du provider met l'état à Ready après succès classe un échec dans l'availability générique retourne toujours une outcome provider-neutral pour un provider configuré ``` Un provider inconnu retourne `ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND` avant tout réseau. ## 7. Refresh multiple / all `refresh_many` conserve l'ordre fourni par le caller et valide la liste complète avant dispatch : ```text maximum 64 providers doublons interdits provider inconnu interdit ``` Chaque provider configuré retourne son outcome propre. Une erreur réseau/provider normalisée ne court-circuite donc pas les providers suivants. `refresh_all` utilise l'ordre stable des provider ids du service. Les providers non éligibles sont inclus comme projections d'état mais ne déclenchent pas de requête réseau. V1 reste volontairement séquentielle et déterministe. Cette décision ne constitue pas un scheduler : aucun provider n'est attendu par sleep et aucun fallback ou consensus n'est exécuté. ## 8. Façade publique Ajouts publics : ```text MarketPriceProviderSetup MarketPriceRefreshOutcome MarketPriceService ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND ERROR_CODE_MARKET_PRICE_REFRESH_INVALID MarketPriceProviderAvailability::is_refresh_eligible_at ``` `MarketPriceRefreshOutcome` expose uniquement : ```text provider_id() state() observation() refreshed() ``` Le consumer n'a pas accès au dispatcher interne, aux clients HTTP, aux limiters ou aux DTOs wire. ## 9. Tests déterministes ajoutés Les nouveaux canaries couvrent : ```text construction du service avec les huit setups V1 désactivés registry de service trié et provider-neutral refresh_all de huit providers désactivés sans réseau ordre déterministe des outcomes doublon dans refresh_many rejeté avant dispatch provider inconnu rejeté avant dispatch 401/403 keyed vs keyless 429 -> CoolingDown 5xx + retry_after -> TemporarilyUnavailable daté erreur locale de request -> Misconfigured éligibilité avant/après retry_at absence de sleep / fallback / consensus dans le service façade publique générique depuis la crate root ``` ## 10. Documentation réconciliée Mise à jour de : ```text docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md ``` Le gate complet de `pre.007` fourni par l'opérateur est enregistré comme PASS. ## 11. Fichiers ajoutés ```text crates/ksp-offchain-transport-lib/src/market_price_service.rs crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs deltas/0.2.11/pre.008.md ``` ## 12. Fichiers modifiés ```text Cargo.toml crates/ksp-offchain-transport-lib/src/error.rs crates/ksp-offchain-transport-lib/src/lib.rs crates/ksp-offchain-transport-lib/src/market_price_provider.rs crates/ksp-offchain-transport-lib/src/market_price_registry.rs crates/ksp-offchain-transport-lib/tests/dependency_boundary.rs crates/ksp-offchain-transport-lib/tests/public_api.rs crates/ksp-offchain-transport-lib/unit_tests/market_price_provider.rs docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md ``` ## 13. Validation attendue ```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 --workspace ``` `pre.009` ne doit commencer qu'après un gate propre de `pre.008` ou un delta fix explicite.