pyth

Par elophanto · elophanto

Guide complet pour Pyth Network - oracle décentralisé fournissant des flux de prix en temps réel pour la DeFi. Couvre l'intégration des flux de prix, les intervalles de confiance, les prix EMA, le CPI on-chain, la récupération off-chain et les mises à jour en streaming pour les applications Solana.

npx skills add https://github.com/elophanto/elophanto --skill pyth

Guide de développement Pyth Network

Pyth Network est un oracle décentralisé fournissant des flux de prix en temps réel pour les cryptomonnaies, actions, forex et matières premières. Ce guide couvre l'intégration des flux de prix Pyth dans les applications Solana.

Vue d'ensemble

Pyth Network fournit :

  • Flux de prix en temps réel - Fréquence de mise à jour de 400ms avec modèle oracle pull
  • Intervalles de confiance - Bornes d'incertitude statistique pour chaque prix
  • Prix EMA - Prix moyennes mobiles exponentielles (~fenêtre d'1 heure)
  • Support multi-actifs - Crypto, actions, FX, matières premières, indices
  • Intégration on-chain - CPI pour les programmes Solana
  • Intégration off-chain - APIs HTTP et WebSocket via Hermes

IDs de programme

Programme Adresse Description
Solana Receiver rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ Poste les mises à jour de prix sur Solana
Price Feed pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT Stocke les données de flux de prix

Déployé sur : Solana Mainnet, Devnet, Eclipse Mainnet/Testnet, réseaux Sonic

IDs de flux de prix populaires

Actif Hex Feed ID
BTC/USD 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43
ETH/USD 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
SOL/USD 0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d
USDC/USD 0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a
USDT/USD 0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b

Liste complète : https://pyth.network/developers/price-feed-ids

Démarrage rapide

Installation

# TypeScript/JavaScript
npm install @pythnetwork/hermes-client @pythnetwork/pyth-solana-receiver

# Rust (ajouter à Cargo.toml)
# pyth-solana-receiver-sdk = "0.3.0"

Récupérer un prix (Off-Chain)

import { HermesClient } from "@pythnetwork/hermes-client";

const client = new HermesClient("https://hermes.pyth.network");

const priceIds = [
  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC/USD
];

const priceUpdates = await client.getLatestPriceUpdates(priceIds);

for (const update of priceUpdates.parsed) {
  const price = update.price;
  const displayPrice = Number(price.price) * Math.pow(10, price.expo);
  console.log(`Price: $${displayPrice.toFixed(2)}`);
  console.log(`Confidence: ±${Number(price.conf) * Math.pow(10, price.expo)}`);
}

Utiliser le prix On-Chain (Rust/Anchor)

use anchor_lang::prelude::*;
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;

#[derive(Accounts)]
pub struct UsePrice<'info> {
    pub price_update: Account<'info, PriceUpdateV2>,
}

pub fn use_price(ctx: Context<UsePrice>) -> Result<()> {
    let price_update = &ctx.accounts.price_update;
    let clock = Clock::get()?;

    // Obtenir un prix pas plus vieux que 60 secondes
    let price = price_update.get_price_no_older_than(
        &clock,
        60, // max age in seconds
    )?;

    msg!("Price: {} × 10^{}", price.price, price.exponent);
    msg!("Confidence: ±{}", price.conf);

    Ok(())
}

Concepts clés

Structure du prix

Chaque prix Pyth contient :

Champ Type Description
price i64 Valeur du prix au format virgule fixe
conf u64 Intervalle de confiance (écart-type)
expo i32 Exposant pour la mise à l'échelle (p. ex. -8 signifie diviser par 10^8)
publish_time i64 Timestamp Unix du prix

Conversion en prix d'affichage :

const displayPrice = price * Math.pow(10, expo);
// Exemple : price=19405100, expo=-2 → $194 051,00

Intervalles de confiance

Les intervalles de confiance représentent l'incertitude dans le prix signalé :

// Un prix de 50 000 $ ± 50 $ signifie :
// - 68 % de chance que le vrai prix soit entre 49 950 $ - 50 050 $
// - Utiliser la confiance pour la gestion des risques

const price = 50000;
const confidence = 50;

// Limite inférieure sûre (conservatrice)
const safeLowerBound = price - confidence;

// Limite supérieure sûre (conservatrice)
const safeUpperBound = price + confidence;

Bonne pratique : Rejeter les prix avec confiance > 2% du prix :

const maxConfidenceRatio = 0.02; // 2%
const confidenceRatio = confidence / Math.abs(price);

if (confidenceRatio > maxConfidenceRatio) {
  throw new Error("Price confidence too wide");
}

Prix EMA

Les prix Exponential Moving Average lissent la volatilité à court terme :

  • ~fenêtre de lissage d'1 heure (5921 slots Solana)
  • Pondérés par confiance inverse (confiance serré = plus de poids)
  • Bon pour : liquidations, valorisation des garanties
  • Disponibles en tant que ema_price et ema_conf
// Utiliser EMA pour les applications moins volatiles
const emaPrice = priceUpdate.emaPrice;
const emaConf = priceUpdate.emaConf;

Intégration Off-Chain

Client Hermes

Hermes est la manière recommandée de récupérer les prix Pyth off-chain.

Endpoint public : https://hermes.pyth.network

Pour la production, obtenez un endpoint dédié auprès d'un fournisseur de données Pyth.

Récupération des derniers prix

import { HermesClient } from "@pythnetwork/hermes-client";

const client = new HermesClient("https://hermes.pyth.network");

// Prix unique
const btcPrice = await client.getLatestPriceUpdates([
  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
]);

// Plusieurs prix en une seule requête
const prices = await client.getLatestPriceUpdates([
  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC
  "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", // ETH
  "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", // SOL
]);

Streaming des mises à jour en temps réel

import { HermesClient } from "@pythnetwork/hermes-client";

const client = new HermesClient("https://hermes.pyth.network");

const priceIds = [
  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
];

// S'abonner aux mises à jour en temps réel via SSE
const eventSource = await client.getPriceUpdatesStream(priceIds, {
  parsed: true,
});

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log("Price update:", data);
};

eventSource.onerror = (error) => {
  console.error("Stream error:", error);
  eventSource.close();
};

// Fermer quand terminé
// eventSource.close();

Publication des prix vers Solana

import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import { HermesClient } from "@pythnetwork/hermes-client";
import { Connection, Keypair } from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = Keypair.fromSecretKey(/* your key */);

const hermesClient = new HermesClient("https://hermes.pyth.network");
const pythReceiver = new PythSolanaReceiver({ connection, wallet });

// Récupérer les données de mise à jour de prix
const priceUpdateData = await hermesClient.getLatestPriceUpdates([
  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
]);

// Construire la transaction pour publier le prix
const transactionBuilder = pythReceiver.newTransactionBuilder();
await transactionBuilder.addPostPriceUpdates(priceUpdateData.binary.data);

// Ajouter votre instruction de programme qui utilise le prix
// transactionBuilder.addInstruction(yourInstruction);

// Envoyer la transaction
const transactions = await transactionBuilder.buildVersionedTransactions({
  computeUnitPriceMicroLamports: 50000,
});

for (const tx of transactions) {
  const sig = await connection.sendTransaction(tx);
  console.log("Transaction:", sig);
}

Intégration On-Chain (Rust)

Configuration

Ajouter à Cargo.toml :

[dependencies]
pyth-solana-receiver-sdk = "0.3.0"
anchor-lang = "0.30.1"

Lecture du prix dans un programme Anchor

use anchor_lang::prelude::*;
use pyth_solana_receiver_sdk::price_update::{PriceUpdateV2, get_feed_id_from_hex};

declare_id!("YourProgramId...");

// ID du flux de prix BTC/USD
const BTC_USD_FEED_ID: &str = "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";

#[program]
pub mod my_program {
    use super::*;

    pub fn check_price(ctx: Context<CheckPrice>) -> Result<()> {
        let price_update = &ctx.accounts.price_update;
        let clock = Clock::get()?;

        // Vérifier qu'il s'agit du bon flux
        let feed_id = get_feed_id_from_hex(BTC_USD_FEED_ID)?;

        // Obtenir un prix pas plus vieux que 60 secondes
        let price = price_update.get_price_no_older_than_with_custom_verification(
            &clock,
            60,
            &feed_id,
            ctx.accounts.price_update.to_account_info().owner,
        )?;

        msg!("BTC/USD Price: {} × 10^{}", price.price, price.exponent);
        msg!("Confidence: ±{}", price.conf);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct CheckPrice<'info> {
    #[account(
        constraint = price_update.to_account_info().owner == &pyth_solana_receiver_sdk::ID
    )]
    pub price_update: Account<'info, PriceUpdateV2>,
}

Utilisation du prix pour les calculs

pub fn swap_with_oracle(
    ctx: Context<SwapWithOracle>,
    amount_in: u64,
) -> Result<()> {
    let price_update = &ctx.accounts.price_update;
    let clock = Clock::get()?;

    // Obtenir le prix avec vérification d'ancienneté
    let price = price_update.get_price_no_older_than(&clock, 30)?;

    // Valider la confiance (max 1% du prix)
    let conf_ratio = (price.conf as u128 * 10000) / (price.price.unsigned_abs() as u128);
    require!(conf_ratio <= 100, ErrorCode::ConfidenceTooWide);

    // Convertir le prix au format utilisable
    // price.price est au format virgule fixe avec price.exponent
    let price_scaled = if price.exponent >= 0 {
        (price.price as u128) * 10_u128.pow(price.exponent as u32)
    } else {
        (price.price as u128) / 10_u128.pow((-price.exponent) as u32)
    };

    // Calculer le montant de sortie en utilisant le prix oracle
    let amount_out = (amount_in as u128)
        .checked_mul(price_scaled)
        .ok_or(ErrorCode::MathOverflow)?
        / 1_000_000; // Ajuster pour les décimales

    msg!("Swap {} -> {} using price {}", amount_in, amount_out, price_scaled);

    Ok(())
}

#[error_code]
pub enum ErrorCode {
    #[msg("Price confidence interval too wide")]
    ConfidenceTooWide,
    #[msg("Math overflow")]
    MathOverflow,
}

Flux de prix multiples

#[derive(Accounts)]
pub struct Liquidation<'info> {
    #[account(
        constraint = collateral_price.to_account_info().owner == &pyth_solana_receiver_sdk::ID
    )]
    pub collateral_price: Account<'info, PriceUpdateV2>,

    #[account(
        constraint = debt_price.to_account_info().owner == &pyth_solana_receiver_sdk::ID
    )]
    pub debt_price: Account<'info, PriceUpdateV2>,
}

pub fn check_liquidation(ctx: Context<Liquidation>) -> Result<bool> {
    let clock = Clock::get()?;

    let collateral = ctx.accounts.collateral_price
        .get_price_no_older_than(&clock, 60)?;
    let debt = ctx.accounts.debt_price
        .get_price_no_older_than(&clock, 60)?;

    // Normaliser au même exposant pour la comparaison
    let collateral_value = normalize_price(collateral.price, collateral.exponent);
    let debt_value = normalize_price(debt.price, debt.exponent);

    // Vérifier si sous-collatéralisé
    let is_liquidatable = collateral_value < debt_value * 150 / 100; // ratio 150%

    Ok(is_liquidatable)
}

fn normalize_price(price: i64, expo: i32) -> i128 {
    let target_expo = -8; // Normaliser à 8 décimales
    let adjustment = expo - target_expo;

    if adjustment >= 0 {
        (price as i128) * 10_i128.pow(adjustment as u32)
    } else {
        (price as i128) / 10_i128.pow((-adjustment) as u32)
    }
}

Bonnes pratiques

1. Toujours vérifier l'ancienneté

// Ne pas utiliser les anciens prix - définir un âge max approprié
let max_age_seconds = 60;
let price = price_update.get_price_no_older_than(&clock, max_age_seconds)?;

2. Valider les intervalles de confiance

// Rejeter les prix avec confiance large (incertitude élevée)
const MAX_CONF_BPS: u64 = 200; // 2%

let conf_bps = (price.conf as u128 * 10000) / (price.price.unsigned_abs() as u128);
require!(conf_bps <= MAX_CONF_BPS as u128, ErrorCode::ConfidenceTooWide);

3. Vérifier la propriété du compte

// Toujours vérifier que le compte de prix est possédé par Pyth
#[account(
    constraint = price_update.to_account_info().owner == &pyth_solana_receiver_sdk::ID
)]
pub price_update: Account<'info, PriceUpdateV2>,

4. Utiliser EMA pour les opérations sensibles

// Pour les liquidations, utiliser EMA pour éviter la manipulation
let ema_price = price_update.get_ema_price_no_older_than(&clock, 60)?;

5. Gérer l'indisponibilité des prix

try {
  const price = await client.getLatestPriceUpdates([feedId]);
  // Utiliser le prix
} catch (error) {
  // Comportement de secours ou rejeter la transaction
  console.error("Price unavailable:", error);
}

6. Considérer le frontrunning

  • Les adversaires peuvent voir les mises à jour de prix avant votre transaction
  • Ne pas concevoir de logique qui court après les mises à jour de prix
  • Utiliser des tolérances de slippage appropriées

Types de flux de prix

Comptes de flux de prix fixes

  • Maintenus en continu par Pyth
  • Adresse fixe par flux
  • Toujours le prix le plus récent
  • Partagés par tous les utilisateurs (congestion potentielle)

Comptes de mise à jour de prix éphémères

  • Créés par transaction
  • Peut spécifier l'ID de shard pour la parallélisation
  • Le loyer peut être récupéré après utilisation
  • Meilleur pour les applications à haut débit
// Utiliser l'ID de shard pour éviter la congestion
const transactionBuilder = pythReceiver.newTransactionBuilder({
  shardId: Math.floor(Math.random() * 65536), // Shard aléatoire
});

Ressources

Documentation officielle

Référentiels GitHub

Packages NPM

Crates Rust


Structure des compétences

pyth/
├── SKILL.md                          # Ce fichier
├── resources/
│   ├── program-addresses.md          # Tous les IDs de programme et IDs de flux
│   └── api-reference.md              # Référence API du SDK
├── examples/
│   ├── price-feeds/
│   │   ├── fetch-price.ts            # Récupération basique de prix
│   │   └── multiple-prices.ts        # Flux de prix multiples
│   ├── on-chain/
│   │   ├── anchor-integration.rs     # Exemple de programme Anchor
│   │   └── price-validation.rs       # Patterns de validation de prix
│   └── streaming/
│       └── real-time-updates.ts      # Streaming WebSocket
├── templates/
│   ├── pyth-client.ts                # Template client TypeScript
│   └── anchor-oracle.rs              # Template programme Anchor
└── docs/
    └── troubleshooting.md            # Problèmes courants et solutions

Vérifier

  • Un appel RPC/SDK réel a été émis (mainnet, devnet, ou validateur local) et la charge utile de réponse est capturée dans la transcription, pas seulement paraphrasée
  • Chaque transaction a été simulée (simulateTransaction ou équivalent) avant toute étape de signature/envoi ; les journaux de simulation sont attachés
  • Pour toute transaction signée/envoyée, la signature résultante est enregistrée et confirmée on-chain (statut retourné par getSignatureStatuses ou une URL d'explorateur)
  • Le slippage, les frais de priorité et les limites d'unités de calcul ont été définis explicitement avec des valeurs numériques concrètes, pas laissés aux valeurs par défaut de la bibliothèque
  • Les adresses de compte, les mints et les IDs de programme utilisés dans l'exécution correspondent aux adresses pyth-oracle documentées pour le cluster ciblé (pas de mélange mainnet/devnet)
  • Le chemin d'erreur a été exercé au moins une fois (solde insuffisant, oracle obsolète, blockhash expiré, etc.) et la gestion des erreurs de l'agent a produit un message lisible

Skills similaires