# Delta `0.2.11-pre.004` — CoinGecko, CoinMarketCap et CoinPaprika ## 1. Base requise Cette tranche s'applique exclusivement après : ```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 ``` La version Cargo attendue à l'entrée est : ```text 0.2.11-pre.3.fix.1 ``` La version Cargo de sortie est : ```text 0.2.11-pre.4 ``` ## 2. Gate d'entrée et reprise volontaire de `RUST-FMT-108` L'opérateur a validé `0.2.11-pre.003-fix.001` le `2026-08-25` avec : ```text cargo fmt --all exécuté python3 scripts/audit_rust_workspace_rules.py 1 violation RUST-FMT-108 uniquement python3 scripts/audit_markdown_tables.py clean, 117 tables / 107 files cargo check --workspace PASS cargo clippy --workspace --all-targets PASS cargo test -p ksp-offchain-transport-lib PASS, 22 unit + 2 boundary + 2 public API ``` `RUST-FMT-108` provenait uniquement du staging `#[cfg(test)]` des modules `http_*` imposé en `pre.003-fix.001` tant qu'aucun consumer de production n'existait. L'opérateur a explicitement demandé de ne pas créer un fix artificiel supplémentaire puisque `pre.004` est précisément la tranche qui active ces modules avec leurs premiers consumers runtime. `pre.004` retire donc le staging crate-root de : ```text http_admission http_client http_settings ``` La cause structurelle de `RUST-FMT-108` disparaît sans exception ni `allow`. ## 3. Objectif Implémenter les trois premiers providers SOL/USD de la famille `market_price_*` : ```text CoinGecko CoinMarketCap CoinPaprika ``` Le contrat reste : ```text SOL/USD uniquement reqwest direct aucun SDK provider endpoints officiels fixes DTOs wire privés prix public exact, aucun canon f64 credentials absents des Debug/logs/errors aucun Config dans Off-chain Transport aucun registry global ni refresh multiple avant leurs tranches dédiées ``` ## 4. Pont HTTP -> Market Price `market_price_adapter.rs` matérialise uniquement les mécanismes réellement partagés par les adapters de prix : ```text admission non bloquante capture des timestamps KSP GET JSON borné via le client HTTP commun feedback 429 / Retry-After vers le cooldown local conversion RFC3339 ou Unix seconds vers MarketPriceTimestamp erreurs de schéma provider sûres sans copie du payload distant construction du runtime HTTP + admission depuis le descriptor provider ``` Les primitives `http_*` restent crate-wide et crate-private afin d'être réutilisables plus tard par d'autres familles telles que `swap_quote_*`, sans devenir un client HTTP générique public pour les consumers. ## 5. Credential market-price partagé `market_price_api_key.rs` ajoute un holder privé `MarketPriceApiKey` : ```text longueur maximale 512 octets valeur vide rejetée whitespace externe rejeté caractères de contrôle rejetés Debug toujours redacted aucun logging de la valeur ``` Ce holder ne lit aucun environnement. Config reste l'unique futur propriétaire de la résolution des secrets. ## 6. CoinGecko Le provider public est : ```text MarketPriceCoinGeckoProvider MarketPriceCoinGeckoSettings MarketPriceCoinGeckoAccessMode ``` Modes V1 : ```text Keyless Demo ``` La configuration respecte les capacités réelles : ```text Keyless -> aucun credential accepté Demo activé -> API key obligatoire Demo désactivé -> credential optionnel pour permettre une config inactive ``` Le request contract est fixe : ```text GET https://api.coingecko.com/api/v3/simple/price ids=solana vs_currencies=usd include_last_updated_at=true header x-cg-demo-api-key uniquement en mode Demo ``` Le descriptor conserve : ```text Keyless -> auth none, rate limit dynamique scope IP Demo -> API key requise, 100 requêtes / 60 s scope account, 10k credits / mois semantics -> AggregatedMarket ``` La réponse exige `solana.usd` et `solana.last_updated_at`. Le prix est lu via `RawValue` puis `MarketPriceDecimal`; le timestamp provider Unix est conservé. ## 7. CoinMarketCap Le provider public est : ```text MarketPriceCoinMarketCapProvider MarketPriceCoinMarketCapSettings MarketPriceCoinMarketCapAccessMode ``` Modes V1 : ```text Keyless Basic ``` La configuration suit la même discipline : keyless refuse un credential et Basic activé exige une API key. Le réaudit `pre.004` retient la surface **Simple Price V2 actuelle** et n'introduit pas la surface V1 deprecated. Requests fixes : ```text Keyless -> GET https://pro-api.coinmarketcap.com/public-api/v2/simple/price Basic -> GET https://pro-api.coinmarketcap.com/v2/simple/price ids=5426 convert=USD include_last_updated=true header x-cmc_pro_api_key uniquement en mode Basic ``` Le descriptor conserve : ```text Keyless -> auth none, rate limit dynamique scope IP Basic -> API key requise, 50 requêtes / 60 s scope account, 15k credits / mois semantics -> AggregatedMarket ``` Le parser vérifie : ```text status.error_code = 0 un seul item SOL id 5426 symbol = SOL une seule quote USD price exact via RawValue last_updated RFC3339 valide ``` La provenance normalisée conserve explicitement la génération `v2`. ## 8. CoinPaprika Le provider public est : ```text MarketPriceCoinPaprikaProvider MarketPriceCoinPaprikaSettings ``` Il est keyless uniquement : aucun faux champ credential n'est introduit. Request fixe : ```text GET https://api.coinpaprika.com/v1/tickers/sol-solana quotes=USD ``` Le descriptor conserve : ```text auth none 10 requêtes / seconde scope IP 20k requests / mois semantics -> AggregatedMarket ``` Le parser vérifie `id = sol-solana`, `symbol = SOL`, lit `quotes.USD.price` exactement via `RawValue` et conserve `last_updated` RFC3339. ## 9. Numeric safety `MarketPriceDecimal` gagne un parser crate-private de `serde_json::value::RawValue`. Il accepte : ```text nombre JSON exact ou chaîne JSON contenant un décimal valide ``` Il ne passe jamais par `f64`. Les tests couvrent notamment une mantisse dont les derniers chiffres seraient perdus par une conversion flottante intermédiaire. ## 10. Availability et rate limiting dans cette tranche Chaque provider possède son `HttpAdmissionController` local construit depuis son descriptor. Le chemin runtime : ```text fetch_sol_usd -> enabled check -> try_admit non bloquant -> request timestamp -> GET provider -> si 429 : Retry-After enregistré dans le cooldown -> receive timestamp -> parser typed provider -> MarketPriceObservation ``` Un provider localement non admissible retourne `http_admission_deferred` avec un délai safe ; aucun sleep n'est introduit dans l'adapter. Le registry global et la projection générique d'availability restent réservés à `pre.007`, et l'orchestration multiple à `pre.008`. ## 11. Tests ajoutés ou étendus Les tests déterministes couvrent : ```text modes keyless/keyed et credentials requis/interdits Debug settings sans secret headers sensibles présents uniquement dans le bon mode URLs et query fixes capabilities descriptors et quotas réponses SOL/USD valides prix JSON exact sans f64 provider timestamps identités provider incorrectes rejetées schema/provider status incohérents rejetés endpoint CMC V2 exact absence de SDK provider absence de Config et tracing direct HTTP production actif mais non exporté publiquement ``` Aucun smoke réseau n'est ajouté dans cette tranche. ## 12. Fichiers ajoutés ```text crates/ksp-offchain-transport-lib/src/market_price_adapter.rs crates/ksp-offchain-transport-lib/src/market_price_api_key.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_coinpaprika.rs crates/ksp-offchain-transport-lib/unit_tests/market_price_coingecko.rs crates/ksp-offchain-transport-lib/unit_tests/market_price_coinmarketcap.rs crates/ksp-offchain-transport-lib/unit_tests/market_price_coinpaprika.rs deltas/0.2.11/pre.004.md ``` ## 13. Fichiers modifiés ```text Cargo.toml crates/ksp-offchain-transport-lib/Cargo.toml crates/ksp-offchain-transport-lib/src/error.rs crates/ksp-offchain-transport-lib/src/http_admission.rs crates/ksp-offchain-transport-lib/src/http_client.rs crates/ksp-offchain-transport-lib/src/http_settings.rs crates/ksp-offchain-transport-lib/src/lib.rs crates/ksp-offchain-transport-lib/src/market_price_decimal.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/http_admission.rs crates/ksp-offchain-transport-lib/unit_tests/market_price_decimal.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 ```text aucun ``` ## 15. Fichiers volontairement inchangés ```text CHANGELOG.md ROADMAP.md README.md .env.example config/** crates/ksp-config-lib/** crates/ksp-onchain-transport-lib/** prompts/** ``` `ROADMAP.md` et `CHANGELOG.md` restent hors de cette prerelease conformément à leur ownership de release. ## 16. Validations exécutées dans le sandbox ```text python3 scripts/audit_rust_workspace_rules.py General Rust rule audit: clean Rust export completeness audit: 0 candidate(s) KSP workspace Rust rule audit: clean python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.11 clean, 118 tables / 108 files contrôle lignes Rust > 160 sur ksp-offchain-transport-lib PASS, 0 ligne ``` Le sandbox de génération ne possède pas Cargo/Rust. `cargo fmt`, `cargo check`, `cargo clippy` et les tests Cargo ne sont donc pas déclarés exécutés ici. ## 17. Gate opérateur demandé ```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 ``` Les smokes live restent hors de `pre.004`. ## 18. Scope non avancé ```text Kraken Coinbase Exchange Jupiter DexScreener Birdeye registry provider global availability service global refresh single/multiple générique Config std.offchain_transport .env.example secrets provider ksp-app-solprices-desk fallback automatique consensus de prix SOL/EUR prix SPL générique ```