# 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.** ## 1. Base et autorité Base stable autoritaire : ```text v0.2.10 ``` État vérifié dans l'archive Gitea fournie : ```text workspace.package.version = 0.2.10 deltas/0.2.10/rel.001.md présent prompts/016-V0_2_11_START_PROMPT.md présent ksp-offchain-transport-lib absente metadata .git absente de l'archive Gitea ``` Cette tranche ouvre : ```text workspace.package.version = 0.2.11-pre.1 commit attendu = v0.2.11-pre.001 aucun tag prerelease ``` Ordre d'autorité pendant `0.2.11` : ```text règles normatives KSP archive stable v0.2.10 et code réellement livré décisions opérateur consignées dans le présent plan validation 014 et deltas immuables de 0.2.11 prompt 016 pour les contraintes non supersédées sources provider officielles actuelles pour les faits externes ``` ## 2. Supersessions explicites du prompt d'ouverture Le brainstorming opérateur du 2026-08-25 remplace plusieurs hypothèses préparatoires du prompt `016`. ```text minimum V1 SOL/USD uniquement nombre de providers un ou plusieurs, sans limite artificielle à un provider initial providers ciblés tous les candidats gratuits retenus par le gate pre.001 transport provider HTTP REST avec reqwest uniquement SDK provider interdit dans 0.2.11 SOL/EUR hors scope de cette première release fallback automatique hors scope consensus/agrégation KSP hors scope future application ksp-app-solprices-desk en 0.2.12 rôle de l'application HID pure, aucune connaissance d'un provider DexScreener V1 un client SOL/USD sur une paire configurée et validée ``` Le retrait de SOL/EUR n'interdit pas son ajout ultérieur. Il évite de forcer dès V1 une conversion fiat externe ou de privilégier artificiellement les providers multi-fiat. ## 3. Mission opérationnelle `0.2.11` doit créer `ksp-offchain-transport-lib` comme propriétaire de la récupération de prix off-chain SOL/USD et des différences opérationnelles entre providers. La surface commune doit permettre à un consumer de : ```text lister les providers de prix connus du service observer leur description et leur sémantique générique observer leur disponibilité runtime observer leur capacité de refresh et le prochain instant admissible rafraîchir un provider par identifiant opaque rafraîchir plusieurs providers sans violer leurs limites respectives obtenir une observation SOL/USD typée et sa provenance obtenir une absence/erreur/cooldown sans connaissance du fournisseur ``` La crate possède les exceptions provider. Un consumer ne doit pas coder : ```text URL provider header d'auth provider ID CoinGecko ou CoinMarketCap mint Jupiter/Birdeye paire Kraken/Coinbase pair address DexScreener rate limit provider forme JSON wire provider mapping de status HTTP provider ``` ### 3.1 Taxonomie interne durable de `ksp-offchain-transport-lib` `ksp-offchain-transport-lib` est une crate de transport **off-chain hétérogène**. `0.2.11` n'en définit que la première famille fonctionnelle ; la crate elle-même ne doit pas être structurée comme si toutes ses responsabilités futures étaient des prix. La convention retenue à partir de `pre.002-fix.001` est : ```text constants.rs préoccupations partagées à toute la crate error.rs erreurs partagées / codes crate-wide http_* mécanique HTTP réutilisable entre familles lorsqu'elle sera matérialisée market_price_* prix de marché / spot et observations normalisées swap_quote_* réservé aux futures quotes montant/route, uniquement lorsqu'elles seront réellement implémentées _* nouvelle famille uniquement lorsqu'un scope réel l'exige ``` Les fichiers génériques `decimal.rs`, `observation.rs`, `provider.rs` et `settings.rs` introduits initialement par `pre.002` sont donc corrigés avant toute extension runtime : la fonctionnalité actuelle est préfixée `market_price_*`. Les futurs domaines ne doivent pas être ajoutés dans ces modules uniquement parce qu'ils utilisent eux aussi un provider HTTP. La même frontière s'applique à la **façade publique**. À partir de `pre.002-fix.003`, tout symbole public spécifique à cette famille porte un nom explicitement `MarketPrice*` ou `MARKET_PRICE_*` : `MarketPriceDecimal`, `MarketPriceObservation`, `MarketPriceProviderId`, `MarketPriceProviderDescriptor`, `MarketPriceProviderRateLimit`, etc. Aucun alias public `Price*` n'est conservé pendant cette prerelease, car il figerait précisément l'ambiguïté que le correctif supprime. Une future famille `swap_quote_*` devra employer son propre préfixe public `SwapQuote*`; une abstraction sans préfixe de famille ne pourra être créée que lorsqu'un invariant réellement partagé aura été démontré. La règle est **responsabilité avant taille** : un module commun n'est promu au niveau crate-wide que lorsqu'au moins deux familles réelles partagent le même invariant. Ainsi, le HTTP, le scheduling ou une identité provider pourront être factorisés plus tard si leur partage est effectivement démontré ; aucune abstraction générique n'est créée aujourd'hui uniquement pour anticiper Jupiter Quotes. ### 3.2 Lecture, écriture et verbes HTTP La capacité `market_price` de `0.2.11` est strictement read-only. La crate n'est toutefois pas déclarée architecturalement read-only pour toutes ses versions futures. L'ownership dépend de la sémantique métier, pas du verbe HTTP : ```text GET/POST qui récupère ou calcule une donnée off-chain peut appartenir à Off-chain Transport quote de swap dépendante d'un montant et d'une route future famille swap_quote, pas market_price construction distante d'un payload sans soumission à auditer selon le contrat futur soumission/signature d'une transaction Solana reste hors d'Off-chain Transport écriture distante ayant un effet métier externe nécessite un scope/contrat explicite avant ajout ``` Un futur endpoint Jupiter utilisant `POST` ne devient donc pas automatiquement une « écriture » KSP. Inversement, aucune opération mutante distante n'est autorisée implicitement sous prétexte qu'elle passe par `reqwest`. ## 4. Frontière avec la future `ksp-app-solprices-desk` La release suivante doit créer : ```text ksp-app-solprices-desk ``` Son rôle est **purement HID**. Elle dépend de la façade publique de `ksp-offchain-transport-lib` et ne connaît aucun provider individuellement. Son premier tableau pourra afficher génériquement : ```text nom d'affichage provider prix SOL/USD fraîcheur disponible état runtime prochain refresh possible bouton refresh ``` Un bouton de refresh multiple/all appelle une opération générique du service. L'application ne fait aucun `sleep`, aucun token bucket, aucun retry provider et aucun dispatch par nom de provider. La composition Rust de l'application peut utiliser Config pour obtenir les settings puis construire le service Off-chain Transport, mais elle ne matche pas les variantes provider pour exécuter une requête. ## 5. Baseline technique interne L'inventaire de `v0.2.10` confirme que les primitives génériques nécessaires existent déjà au workspace : ```text reqwest ^0.13 serde ^1.0 serde_json ^1.0 tokio ^1.53 http ^1.5 chrono ^0.4 si parsing timestamp réellement nécessaire ``` Décision de dépendances V1 : ```text aucun SDK CoinGecko aucun SDK CoinMarketCap aucun SDK CoinPaprika aucun SDK Kraken aucun SDK Coinbase aucun SDK Jupiter aucun SDK Birdeye aucun SDK DexScreener aucune bibliothèque decimal provider/externe ajoutée par facilité ``` Les adapters utilisent `reqwest` et des DTOs wire privés KSP. `0.2.11-pre.001` n'ajoute encore aucune crate ni dépendance. ## 6. Audit provider actuel du 2026-08-25 Les huit providers ci-dessous satisfont le critère fonctionnel retenu : accès gratuit actuel, HTTP REST simple et possibilité de produire SOL/USD sans SDK provider. | Provider | Accès gratuit V1 | Auth V1 | Limite ou quota à modéliser | Sémantique SOL/USD | Verdict | |-------------------|-----------------------|-----------------------|----------------------------------------------------|--------------------------------------------|---------| | CoinGecko | keyless ou Demo | aucune ou Demo key | keyless dynamique IP, Demo 100 par min et 10k mois | agrégateur marché CoinGecko | IN | | CoinMarketCap | keyless ou Basic | aucune ou API key | keyless dynamique IP, Basic 50 par min et 15k mois | agrégateur marché CoinMarketCap | IN | | CoinPaprika | Free | aucune | 10 par sec IP et 20k mois, refresh moyen 5 min | agrégateur marché CoinPaprika | IN | | Kraken | public Spot REST | aucune | cadence sûre documentée à 1 par sec ou moins | dernier trade du marché Kraken SOL/USD | IN | | Coinbase Exchange | public Exchange REST | aucune | 10 par sec IP, burst 15 | dernier trade du produit Coinbase SOL-USD | IN | | Jupiter Price V3 | keyless ou Free | aucune ou API key | keyless 0.5 RPS, Free 1 RPS | heuristique de prix USD orientée Solana | IN | | Birdeye | Standard | X-API-KEY obligatoire | 1 RPS compte, 30k CU, Price Single 3 CU | spot USD token Solana Birdeye | IN | | DexScreener | API publique gratuite | aucune | 300 par min pour pairs/tokens | priceUsd de la paire DEX Solana configurée | IN | Sources officielles principales auditées : ```text https://docs.coingecko.com/docs/keyless-public-api https://docs.coingecko.com/reference/simple-price https://www.coingecko.com/en/api/pricing https://coinmarketcap.com/api/documentation/pro-api-reference/keyless-public-api https://coinmarketcap.com/api/documentation/pro-api-reference/cryptocurrency https://coinmarketcap.com/api/documentation/pro-api-reference/cryptocurrency/simple-price-v2 https://coinmarketcap.com/api/documentation/pro-api-reference/cryptocurrency/simple-price-v1 https://coinmarketcap.com/api/pricing https://docs.coinpaprika.com/api-reference/rest-api/introduction https://docs.coinpaprika.com/api-reference/tickers/get-ticker-for-a-specific-coin https://docs.coinpaprika.com/api-plans https://support.kraken.com/articles/206548367-what-are-the-api-rate-limits- https://support.kraken.com/hc/articles/360000919986-public-endpoint-examples-you-can-try-them-directly-in-a-web-browser- https://docs.kraken.com/api/docs/rest-api/get-ticker-information https://docs.cdp.coinbase.com/exchange/rest-api/rate-limits https://docs.cdp.coinbase.com/api-reference/exchange-api/rest-api/products/get-product-ticker https://docs.cdp.coinbase.com/api-reference/exchange-api/rest-api/products/get-single-product https://developers.jup.ag/docs/portal/setup https://developers.jup.ag/docs/portal/plans https://developers.jup.ag/docs/portal/migration https://developers.jup.ag/docs/api-reference/price https://developers.jup.ag/docs/price https://docs.birdeye.so/docs/pricing https://docs.birdeye.so/docs/rate-limiting https://docs.birdeye.so/reference/get-defi-price https://docs.birdeye.so/docs/compute-unit-cost https://docs.dexscreener.com/api/reference https://docs.dexscreener.com/ ``` Les chiffres externes sont une photographie d'audit, pas une constante éternelle. Les adapters doivent rester capables de classer `429` et les informations serveur sans supposer que la documentation ne changera jamais. Réaudit `pre.004` : CoinMarketCap Simple Price est implémenté sur la surface **V2** actuelle. La surface Simple Price V1 est documentée comme deprecated et n'est pas utilisée par KSP. Les deux modes V1 retenus restent distincts : endpoint public keyless V2 sans credential, ou endpoint Pro V2 Basic avec `X-CMC_PRO_API_KEY`. Réaudit `pre.006` : le Developer Platform Jupiter courant utilise `https://api.jup.ag` pour les accès keyless et API-key. Le keyless est documenté à 0,5 RPS et le plan Free à 1 RPS. Price V3 expose `usdPrice` comme prix USD unique et `blockId` comme information de récence ; `createdAt` décrit la création du token et ne doit pas être transformé en timestamp du prix. DexScreener documente l'endpoint direct `/latest/dex/pairs/{chainId}/{pairId}` à 300 requêtes par minute. KSP sélectionne uniquement `chainId = solana` et interdit dans V1 les endpoints de search ou de découverte de pools. Réaudit `pre.007` : Birdeye Standard reste gratuit à 30 000 compute units inclus et 1 RPS au niveau du compte. L'endpoint Price Single `GET /defi/price` consomme actuellement 3 compute units, exige `X-API-KEY`, accepte `x-chain: solana` et le mint canonique WSOL, et fournit `value` plus `updateUnixTime`. Le quota et le coût par requête sont des metadata informatives ; KSP ne décrémente jamais localement le quota comme une vérité authoritative. ### 6.1 Gratuité technique et conditions d'usage Le verdict `IN` signifie ici qu'un accès HTTP gratuit permet techniquement de développer et live-tester le provider dans `0.2.11`. Il ne constitue pas une affirmation de gratuité commerciale permanente. L'audit du 2026-08-25 relève notamment : ```text CoinGecko keyless est présenté pour faible volume, expérimentation/prototypage et usages assimilés ; le plan Demo porte ses propres conditions et attribution CoinMarketCap keyless est présenté pour évaluation/prototypage ; un plan authentifié reste la voie documentée pour un usage avec allowance dédiée CoinPaprika Free est annoncé pour usage personnel ; l'usage commercial relève des offres payantes actuelles ``` Conséquence KSP : ```text les adapters gratuits restent implémentables en V1 aucune documentation stable KSP ne promet une gratuité commerciale durable les conditions d'usage et plans gratuits sont réaudités au gate live final avant publication stable un changement de plan/licence peut rendre un provider administrativement indisponible sans casser le contrat provider-neutral ``` ## 7. Providers écartés de V1 ### Pyth Hermes Pyth reste intéressant comme oracle mais la transition Pyth Core annoncée pour le 2026-08-26 rend son modèle d'accès immédiatement mouvant et ne garantit pas un accès gratuit durable après la période d'essai. Décision : ```text Pyth = OUT de 0.2.11 V1 ``` Un audit ultérieur pourra l'ajouter comme sémantique oracle distincte. ### Binance Aucune assimilation implicite de SOL/USDT à SOL/USD n'est admise. Décision : ```text Binance = OUT tant qu'un produit SOL/USD direct et pertinent n'est pas retenu explicitement ``` ## 8. Identité et sémantique de la paire V1 Le contrat public V1 ne devient pas un moteur générique de symboles. La seule paire fonctionnelle exposée est : ```text SOL/USD ``` Les identifiants propriétaires restent privés aux adapters ou aux settings provider : ```text CoinGecko coin id solana CoinMarketCap asset id 5426 CoinPaprika coin id sol-solana Kraken pair SOLUSD Coinbase product SOL-USD Jupiter mint SOL canonique attendu par Price V3 Birdeye mint SOL canonique attendu par Price Single DexScreener pair address configurée, base SOL vérifiée ``` Le résultat commun ne prétend pas que les prix sont équivalents. Le descriptor générique expose une classe de sémantique, par exemple : ```text AggregatedMarket ExchangeLastTrade SolanaHeuristic SolanaSpot DexPairUsd ``` Le nom final des variantes appartient à `pre.002`, mais la distinction sémantique est obligatoire. ## 9. Cas DexScreener V1 Décision opérateur : `0.2.11` implémente bien DexScreener, mais uniquement comme client SOL/USD. Le client V1 : ```text utilise un pair address Solana configuré appelle l'endpoint pair direct vérifie chainId = solana vérifie que la paire correspond au SOL attendu lit uniquement priceUsd pour le contrat commun conserve la pair address comme provenance provider sûre ne recherche pas automatiquement la meilleure pool ne sélectionne pas par liquidité ne construit aucun consensus entre pools ``` Le choix d'une paire explicite évite d'introduire silencieusement une politique de sélection DEX. Une future version pourra remplacer cette stratégie sans modifier le contrat HID de `ksp-app-solprices-desk`. ## 10. Représentation numérique Le gate rejette `f64` comme représentation canonique publique du prix. Décision matérialisée en `pre.002` : `MarketPriceDecimal` est le type décimal KSP borné, sérialisable sans perte et construit depuis la représentation textuelle du provider. Contrat V1 exact : ```text coefficient positif u128 scale bornée à 18 décimales entrée textuelle bornée à 96 octets normalisation des zéros terminaux parsing décimal et notation scientifique bornés sans passage par f64 aucun NaN aucun Inf overflow rejeté zéro/négatif rejetés pour un prix réussi serialization canonique décimale en chaîne ``` Le wire d'un provider peut être JSON number ou string. L'adapter le convertit vers le type KSP sans utiliser `as_f64()` comme vérité canonique. `MarketPriceDecimal` sérialise toujours sa valeur canonique sous forme de chaîne décimale non scientifique. Le zéro est invalide pour ce type puisqu'il représente exclusivement un prix réussi ; l'absence de prix reste donc hors de la valeur numérique elle-même. Cette solution évite d'ajouter une crate decimal uniquement pour V1 et prépare le passage frontend sans perte IEEE-754. ## 11. Observation commune et fraîcheur Une observation SOL/USD commune doit distinguer : ```text provider_id opaque prix décimal KSP sémantique de prix instant de départ requête KSP instant de réception KSP instant provider optionnel lorsqu'il existe réellement provenance provider sûre et bornée ``` `pre.002` matérialise ce contrat avec `MarketPriceObservation`, `MarketPriceTimestamp` (millisecondes UTC depuis Unix epoch) et `MarketPriceProvenance` bornée à 256 octets sans caractères de contrôle. Le constructeur d'observation rejette un instant de réception antérieur au départ de requête. Règles : ```text ne jamais inventer un timestamp provider ne jamais interpréter un timestamp de création token comme timestamp de prix ne jamais transformer block id, pair creation time ou autre champ en faux update time absence de prix != prix zéro stale != transport indisponible ``` ## 12. Registry et état runtime `ksp-offchain-transport-lib` possède un registry runtime des providers de prix activés/configurés. Deux surfaces génériques doivent rester distinctes : ```text capabilities statiques auditées état runtime courant ``` Le descriptor statique peut exposer : ```text provider id opaque nom d'affichage sémantique mode d'auth générique limitation générique support SOL/USD ``` `pre.002` fixe `MarketPriceProviderId`, `MarketPriceProviderDescriptor`, `MarketPriceProviderAuthMode`, `MarketPriceProviderRateLimit`, `MarketPriceProviderLongTermQuota` et leurs enums de scope/période/unité. `pre.007` ajoute `MarketPriceProviderRequestCost` et l'unité `ComputeUnits` afin de représenter sans perte le modèle Birdeye. Les limites fixes exigent un budget et une fenêtre non nuls ; les limites dynamiques restent explicitement distinctes. Les quotas longs termes et coûts unitaires sont descriptifs et ne deviennent jamais un compteur local de quota restant. L'état runtime peut représenter au minimum : ```text ready cooling down avec prochain instant admissible temporarily unavailable authentication unavailable quota unavailable misconfigured disabled ``` `MarketPriceProviderAvailability` et `MarketPriceProviderState` matérialisent cette projection générique dès `pre.002`. `pre.007` ajoute `MarketPriceProviderRegistry` et `MarketPriceProviderRegistryEntry` : le registry est borné, trié de façon déterministe par `provider_id`, rejette les doublons et n'expose que `descriptor + state`. Son module ne contient aucun dispatch par nom de provider. `pre.008` ajoute `MarketPriceService`, propriétaire du dispatch réel et des mutations d'availability, tout en conservant le registry comme projection générique détachable. `MarketPriceProviderSetup` contient uniquement la sélection provider-specific nécessaire à la composition initiale ; elle ne traverse pas les opérations runtime destinées à la HID. `MarketPriceProviderAvailability` expose aussi génériquement l'éligibilité immédiate au refresh et un `retry_at` lorsqu'il est réellement connu. `AuthenticationUnavailable`, `QuotaUnavailable`, `Misconfigured`, `Disabled`, `CoolingDown`, `TemporarilyUnavailable` et `Ready` restent distincts sans parsing provider dans les consumers. Les noms Rust de cette fondation sont désormais matérialisés ; aucun consumer ne doit parser des strings d'erreur pour connaître cet état. ## 13. Rate limiting et refresh multiple Le rate limiting appartient entièrement à `ksp-offchain-transport-lib`. Le modèle doit savoir représenter : ```text cadence fixe burst si documenté fenêtre par seconde ou minute quota long terme informatif limite dynamique/server-driven scope IP, compte ou organisation lorsque pertinent ``` Les quotas mensuels provider ne sont pas décrémentés comme une vérité authoritative locale : un autre processus peut consommer le même compte. Ils sont exposés comme capacités documentées ; les réponses provider restent la vérité runtime. ### Refresh individuel Un `refresh(provider_id)` : ```text vérifie l'éligibilité locale exécute si autorisé met à jour observation et état classe 429 et Retry-After ne contourne jamais le cooldown ``` ### Refresh multiple/all Un refresh multiple : ```text évalue chaque provider indépendamment lance concurremment les providers éligibles ne bloque pas Coinbase parce que Jupiter est en cooldown ne dort pas pour attendre tous 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. ## 14. HTTP commun et résilience Les adapters REST réutilisent les patterns sûrs déjà éprouvés dans KSP sans dépendre de `ksp-onchain-transport-lib`. Décisions V1 : ```text reqwest uniquement HTTPS officiel fixe dans les adapters V1 redirect désactivé Referer automatique désactivé proxy système/implicite désactivé retries implicites reqwest désactivés connect timeout borné : défaut 5 s, hard max 30 s request timeout borné : défaut 10 s, hard max 120 s GET uniquement pour les prix V1 body réponse borné avant désérialisation : défaut 1 MiB, hard max 4 MiB JSON syntaxiquement validé avant remise à l'adapter typed 429 classé explicitement Retry-After delta-seconds honoré et borné à 1 h lorsqu'il est exploitable 408 et 5xx classés transitoires 401/403 classés auth/access URL retirée des erreurs reqwest avant contexte KSP aucun body remote brut dans KspError ``` Il n'existe pas d'URL provider arbitraire dans la Config V1. Cela évite SSRF, redirection de credential et confusion de provenance. Les primitives `http_*` sont **crate-private** : elles ne créent pas un client HTTP générique public contournant les capacités métier. Les adapters `market_price_*`, puis de futures familles telles que `swap_quote_*`, les consomment derrière leur propre contrat. Le limiter `pre.003` est non bloquant. Une cadence fixe est matérialisée par un token bucket lissé ; lorsqu'aucun burst n'est documenté, la capacité locale initiale reste volontairement `1`. Lorsqu'un burst est documenté, sa capacité est indépendante du nombre moyen de requêtes de la fenêtre et peut donc lui être supérieure. Une limite dynamique n'invente aucune cadence locale et apprend seulement des réponses provider, notamment `429`. Un `Retry-After` serveur peut prolonger le cooldown mais ne peut pas dépasser une borne défensive d'une heure. `pre.008` projette ces délais dans `MarketPriceProviderAvailability` et ne dort jamais : `refresh`, `refresh_many` et `refresh_all` retournent immédiatement un état non éligible lorsqu'un prochain instant admissible est encore dans le futur. ## 15. Config provider-capability-aware Config reste l'unique propriétaire des documents, de l'environnement et des secrets. `pre.009` matérialise les identifiants durables : ```text file_id document = cfg.std.offchain_transport file_id schema = schema.std.offchain_transport fichier = std.offchain_transport.json schema = std.offchain_transport.schema.json format_version = 1 ``` Le document reste générique Off-chain Transport et place la capacité actuelle sous `market_price`. Cette structure évite de transformer le document durable en Config dédiée uniquement au prix lorsque d'autres familles telles que `swap_quote` apparaîtront. Deux profils versionnés servent de canaries : ```text public_keyless = providers réellement utilisables sans secret ; Birdeye et DexScreener désactivés all_free = modes gratuits à clé activés + paire DexScreener explicite ``` Les branches correspondent aux capacités réelles de chaque provider. | Provider | Champs spécifiques V1 | |-------------------|---------------------------------------------------------------| | CoinGecko | enabled, access_mode keyless ou demo, api_key conditionnelle | | CoinMarketCap | enabled, access_mode keyless ou basic, api_key conditionnelle | | CoinPaprika | enabled | | Kraken | enabled | | Coinbase Exchange | enabled | | Jupiter | enabled, access_mode keyless ou free, api_key conditionnelle | | Birdeye | enabled, api_key requise si activé | | DexScreener | enabled, sol_usd_pair_address requise si activé | Le schema conditionne les credentials au mode choisi et interdit un faux champ `api_key` sur les modes keyless. L'adapter effectif applique en plus la provenance : une API key provider doit provenir d'une variable classée Secret (`KSP_SECRET_*` ou namespace KSPB secret admis par Config), même si un littéral non vide est syntaxiquement valide JSON. Les erreurs et `Debug` ne recopient jamais le credential. La paire DexScreener n'est pas un secret. Un littéral validé comme Pubkey Solana est admis ; si la valeur vient de l'environnement, sa provenance doit rester Public, avec le canari versionné `KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS`. Pour respecter « requise seulement si activé », `MarketPriceDexScreenerSettings` accepte désormais l'absence de paire lorsque le provider est désactivé et la refuse toujours lorsqu'il est activé. `.env.example` inventorie les variables nécessaires sans valeur réelle. Les applications desktop empaquetent aussi le nouveau document et son schema parce que `prepare_packaged_runtime` synchronise l'intégralité du registry Config. Les limites provider ne deviennent pas des valeurs libres permettant à Config/UI d'augmenter la cadence. Une future policy pourra permettre un throttle **plus strict** si un besoin réel apparaît. ## 16. Ownership des erreurs et indisponibilités Une panne d'un provider ne doit pas rendre tous les autres inexploitables. Le service classe par provider : ```text configuration manquante/invalide DNS/connect/timeout TLS HTTP 4xx HTTP 429 HTTP 5xx JSON invalide ou schema drift asset/pair mismatch prix absent prix numérique invalide staleness provider si mesurable ``` Un refresh multiple retourne donc des outcomes partiels. Le service peut conserver un provider dans l'inventaire avec un état indisponible, au lieu de forcer l'application à connaître pourquoi Birdeye ou un autre provider manque. ## 17. Threat model V1 | Risque | Contrôle prévu | |------------------------------------------|-----------------------------------------------------------------| | fuite API key | Config propriétaire, header privé, redaction logs/debug/erreurs | | SSRF ou exfiltration vers URL configurée | origines provider fixes en V1, aucun endpoint arbitraire | | redirect credential | redirects HTTP désactivés | | proxy environnement implicite | policy no-proxy cohérente avec les clients KSP sensibles | | réponse JSON énorme | limite de taille avant désérialisation | | prix NaN, Inf, négatif ou overflow | parser décimal KSP borné | | faux asset par collision de symbole | IDs provider fixes et validations d'identité | | mauvaise paire DexScreener | pair address explicite et identité SOL vérifiée | | 429 répétés | limiter local, Retry-After, cooldown provider | | provider down | état provider séparé, autres providers continuent | | stale data | timestamps réels conservés, aucune fraîcheur inventée | | schema drift | DTOs wire privés stricts et erreur provider classée | | quota partagé hors processus | quota local non présenté comme authoritative | | différences de prix | sémantique/provenance visible, aucun consensus KSP V1 | | log d'une réponse sensible | aucun payload remote brut dans les erreurs/logs | ## 18. Tests et smokes attendus ### Tests déterministes Prévoir : ```text parsing décimal string/JSON number/scientifique limites et overflow mapping de chaque wire provider asset/pair mismatch prix null/absent HTTP status classification 429 et Retry-After rate limiter par policy cooldown états unavailable/auth/quota refresh single refresh all partiel un provider en échec n'empêche pas les autres redaction credential Config branches provider public API sans type wire provider ``` ### Smokes live opt-in Une tranche technique dédiée en fin de release doit tester les adapters réellement retenus avec un accès gratuit courant. Le smoke : ```text ne hardcode aucun secret n'affiche aucun secret interroge uniquement SOL/USD vérifie prix positif et parseable vérifie provider et identité attendue respecte chaque cadence ne compare pas à un prix exact figé n'échoue pas tout le batch parce qu'un provider externe est indisponible ``` Les providers keyed gratuits peuvent nécessiter des credentials opérateur via Config. Les smokes keyless restent séparables pour ne pas obliger à posséder toutes les clés au même instant. ## 19. Hors périmètre `0.2.11` ```text SOL/EUR autres fiat prix d'autres assets ou SPL tokens publics Price Desk Tauri modification Wallet Desk provider SDK WebSocket/SSE price streaming OHLCV/candles historique/backfill prix order books quotes/routing/swap metadata token/IPFS/Arweave consensus multi-provider moyenne/médiane KSP fallback automatique choix automatique de pool DexScreener cache durable nouvelle abstraction réseau universelle Pyth Binance sans SOL/USD direct retenu ``` Le fait que Jupiter/Birdeye/DexScreener soient capables de couvrir d'autres tokens ne les met pas dans la surface publique V1. ## 20. Forecast souple des prereleases Chaque tranche technique vise **environ 15 à 20 minutes de travail effectif** lorsque le sujet s'y prête. Cette durée est une cible de granularité et **pas une durée maximale** : un build lent, un live test, un diagnostic ou une difficulté réelle peut légitimement prolonger une tranche. Le forecast reste volontairement souple. Une tranche peut être scindée, fusionnée ou réordonnée par delta si la réalité technique l'exige, à condition de préserver les responsabilités de clôture. Le nombre de prereleases prévu n'impose ni une session de conversation par tranche, ni une scission de session : plusieurs prereleases peuvent être réalisées dans une même session de travail. Chaque entrée porte désormais un statut directement modifiable. Lorsqu'un fix d'une tranche est nécessaire, il peut être ajouté sous la prerelease concernée avec un titre `####`, sans transformer le forecast en tableau ni casser sa lisibilité. ### `pre.001` — Audit, sizing et cadrage multi-provider **Statut : réalisé.** Audit externe des providers, comparaison des sémantiques, réduction du scope V1 à SOL/USD, architecture HID de la future application, threat model, numeric design, sizing et planification. #### `pre.001-fix.001` — Forecast souple éditable **Statut : réalisé.** Remplacement du forecast tabulaire par des sous-sections éditables, clarification de la cible de 15 à 20 minutes et de l'absence de relation obligatoire entre prerelease et session de conversation. Aucun changement du scope technique de `0.2.11`. ### `pre.002` — Fondation de `ksp-offchain-transport-lib` **Statut : réalisé.** Création de la crate et de sa façade publique initiale : `MarketPriceDecimal`, paire SOL/USD, observation/provenance/timestamps, identifiants/descriptors provider, capacités auth/rate-limit/quota, settings communs minimaux et états d'availability provider-neutral. Aucun HTTP, limiter actif, credential provider ou adapter wire n'est avancé depuis `pre.003+`. #### `pre.002-fix.001` — Rustdoc, Logging et taxonomie Off-chain **Statut : réalisé.** Correction des deux warnings `missing_docs` des tests d'intégration, ajout de `ksp-logging-lib` avec `src/constants.rs` et `TRACING_TARGET = "ksp-offchain-transport-lib"`, instrumentation KSP des validations/constructions comportementales, et renommage des modules actuels en `market_price_*` pour préparer les futures familles hétérogènes telles que `swap_quote_*` sans transformer la crate entière en module de prix. #### `pre.002-fix.002` — Conformité Clippy `implicit_return` **Statut : réalisé.** Correction du canari `dependency_boundary` pour respecter `-D clippy::implicit-return`, sans changement de contrat ni de comportement runtime. La version technique workspace devient `0.2.11-pre.2.fix.2`. #### `pre.002-fix.003` — Nommage public `MarketPrice*` **Statut : réalisé.** Préfixage de toute la surface publique spécifique à la famille `market_price` en `MarketPrice*` / `MARKET_PRICE_*`, y compris observation, décimal, timestamps, provenance, provider settings/descriptors/limits/availability et codes d'erreur. Aucun alias `Price*` n'est conservé : le correctif évite de devoir renommer ces API lorsque d'autres familles telles que `swap_quote_*` seront ajoutées. La version technique workspace devient `0.2.11-pre.2.fix.3`. ### `pre.003` — HTTP REST commun et rate limiting **Statut : réalisé avec correctif.** Client HTTP REST commun crate-private, classification d'erreurs HTTP/reqwest, bornes de body et timeouts, redaction, désactivation explicite des redirects/proxy/retries implicites, JSON syntaxiquement validé, limiter token-bucket générique non bloquant et cooldown provider-driven borné. Aucun adapter provider n'est avancé. #### `pre.003-fix.001` — Façade crate-root, staging et burst documenté **Statut : réalisé ; gate Cargo/Clippy/tests PASS, `RUST-FMT-108` structurel repris par `pre.004`.** Correction de l'ordre alphabétique des constantes d'erreur, utilisation systématique de la façade crate-root pour les items `http_*` partagés (`impl crate::...` et signatures crate-wide), suppression des `#[allow(dead_code)]` préparatoires au profit de `#[cfg(test)]` conformément à `RUST-API-008`, et correction du contrat de burst : un burst documenté peut dépasser la cadence moyenne de la fenêtre, comme le cas Coinbase `10 req/s, burst 15`. La version technique workspace devient `0.2.11-pre.3.fix.1`. ### `pre.004` — CoinGecko, CoinMarketCap et CoinPaprika **Statut : réalisé.** Activation en production du socle `http_*` et implémentation des trois adapters d'agrégateurs avec DTOs wire privés, parsing décimal exact via `RawValue`, timestamps provider lorsque fournis, endpoints officiels fixes, auth correspondant aux capacités réelles et limites provider décrites dans leurs descriptors. CoinGecko supporte keyless ou Demo key, CoinMarketCap keyless ou Basic key sur Simple Price V2, CoinPaprika reste keyless. Aucun registry global, Config ou smoke live n'est avancé. #### `pre.004-fix.001` — Ordre des constantes, Clippy et fixture CoinMarketCap V2 **Statut : réalisé, gate opérateur complet PASS.** Correctif strict de `pre.004` : tri alphabétique complet des constantes d'erreur, suppression des deux warnings `clippy::collapsible_if` dans les builders CoinGecko/CoinMarketCap, et correction de la fixture CoinMarketCap V2 dont le JSON concaténé omettait le guillemet ouvrant de `last_updated`. Aucun contrat provider, endpoint, parsing runtime, rate limit ou scope de release n'est modifié. La version technique workspace devient `0.2.11-pre.4.fix.1`. ### `pre.005` — Kraken et Coinbase Exchange **Statut : réalisé, gate opérateur ciblé PASS ; workspace complet non rejoué.** Implémentation des deux adapters exchange keyless avec DTOs wire privés et endpoints HTTPS fixes. Kraken utilise le ticker public `SOLUSD`, retient le dernier trade `c[0]`, applique une cadence locale conservatrice de 1 requête par seconde et ne fabrique aucun timestamp provider absent du ticker. Coinbase Exchange utilise `/products/SOL-USD/ticker`, conserve `price` et `time`, et modélise le token bucket public documenté à 10 requêtes par seconde par IP avec burst 15. Les deux adapters exposent `MarketPriceSemantics::ExchangeLastTrade`; aucun registry global, Config, SDK provider ou smoke live n'est avancé. ### `pre.006` — Jupiter et DexScreener **Statut : réalisé, gate opérateur ciblé PASS ; workspace complet non rejoué.** Implémentation des adapters Jupiter Price V3 et DexScreener avec DTOs wire privés et endpoints HTTPS officiels fixes. Jupiter distingue keyless à 0,5 requête par seconde et Free key à 1 requête par seconde sur le même `api.jup.ag`, utilise uniquement le mint WSOL et conserve `blockId` comme provenance de récence sans le convertir en timestamp. DexScreener exige un pair address Solana explicite lorsqu'il est activé, appelle uniquement `/latest/dex/pairs/solana/{pairId}`, vérifie `chainId = solana`, le pair address retourné et le mint WSOL comme base avant de lire `priceUsd`. Aucune découverte de pool, aucun tri de liquidité, aucun consensus et aucun registry global ne sont avancés. ### `pre.007` — Birdeye, registry et availability **Statut : réalisé, gate opérateur complet PASS.** Implémentation de Birdeye Standard avec `X-API-KEY`, endpoint Price Single Solana/WSOL, prix exact et timestamp provider réel. Le descriptor modélise 1 RPS par compte, 30 000 compute units de quota informatif et 3 compute units par refresh SOL/USD. `MarketPriceProviderRegistry` finalise l'inventaire runtime provider-neutral des huit adapters sous forme d'entrées `descriptor + state`, avec ordre déterministe et doublons rejetés. Les transitions runtime et le dispatch restent réservés à `pre.008`. Aucun dispatch de refresh, Config ou smoke live n'est avancé. ### `pre.008` — Refresh individuel et multiple **Statut : réalisé ; `pre.008-fix.001` corrige le gate opérateur Clippy/boundary sans changement fonctionnel.** `MarketPriceService` possède les huit adapters configurés, le registry runtime et le dispatch provider-specific interne. La construction accepte `MarketPriceProviderSetup` pour la future couche de composition, puis l'usage devient intégralement provider-agnostic via `registry()`, `refresh(provider_id)`, `refresh_many(...)` et `refresh_all()`. Les échecs provider sont classés en availability générique ; un résultat en erreur n'interrompt pas les autres providers d'un batch. Les états non éligibles sont projetés sans requête réseau, les cooldowns expirés redeviennent tentables, et aucune opération ne dort pour attendre un rate limit. Aucun fallback, consensus, agrégation, Config ou scheduling périodique n'est avancé. `pre.008-fix.001` conserve ce contrat. Il collapse uniquement un `if` signalé par Clippy et rend le canari de boundary précis : les rustdocs peuvent mentionner explicitement l'absence de consensus, tandis que le test interdit les fonctions exécutables `aggregate`/`consensus` et le chemin `fallback_provider`. ### `pre.009` — Config Off-chain Transport **Statut : réalisé ; gate Cargo opérateur à rejouer.** 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é.** Hardening de l'API publique, tests adversariaux/completeness et brouillons techniques README/USAGE sans réconciliation documentaire finale. ### `pre.011` — Gate technique et live final **Statut : planifié.** Smokes gratuits possibles, graphes Cargo, validations techniques finales et caractérisation actuelle des providers. ### `pre.012` — Réconciliation documentaire finale **Statut : planifié.** Réconciliation finale du plan, de la validation, des README/USAGE, de l'architecture et des références durables après les gates techniques. ### `pre.013` — Préparation minimale de publication **Statut : planifié.** Prompt `0.2.12`, `CHANGELOG.md`, `ROADMAP.md` et mécanique de version uniquement, sans reprendre les responsabilités techniques ou documentaires des deux tranches précédentes. ### `rel.001` — Publication stable `v0.2.11` **Statut : planifié.** Publication stable après validation de la dernière prerelease et vérification de la cohérence de release. Les responsabilités de clôture restent strictement séparées : ```text pre.011 = technique/live pre.012 = documentation finale pre.013 = prompt suivant + CHANGELOG + ROADMAP seulement ``` ## 21. Gate de sizing `pre.001` Verdict : **GO sans split de release**. Justification : ```text huit adapters REST sont petits et indépendants aucun SDK provider n'est nécessaire les primitives HTTP/serde/tokio sont déjà au workspace la seule paire publique est SOL/USD le contrat commun reste volontairement petit la complexité réelle est isolée dans registry/rate limiting/availability les adapters peuvent être livrés par groupes de prereleases courtes ``` `0.2.11` reste toutefois une release multi-provider et ne doit pas compresser artificiellement les gates de sécurité/live.