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_priceetema_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 (
simulateTransactionou é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
getSignatureStatusesou 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