15 min read
Overview
In this guide, you will learn how to use Pyth with the Eclipse testnet to create a TypeScript CLI application. This application will determine if a banana is ripe based on the ETH price, which will be fetched from the Pyth network.
Learn how to integrate Pyth with the Eclipse testnet by creating a TypeScript CLI app that checks ETH prices and determines banana ripeness.
Prerequisites
- An Eclipse wallet compatible with the Solana Virtual Machine (SVM)
- Testnet ETH in your Eclipse SVM wallet
- Basic understanding of TypeScript and Rust programming languages
What You Will Learn
- What is Pyth network and how it works
- How to fetch price data from the Pyth network
- How to deploy and interact with a Rust smart contract
What is Pyth Network?
Pyth Network is a high-fidelity, low-latency oracle network that bridges off-chain financial data with blockchain ecosystems. Since blockchains are deterministic and closed systems (producing the same output for the same input across all nodes without external influence) they cannot natively access off-chain information. Pyth solves this by collecting real-time data from various sources and making it available on-chain, enabling smart contracts to access financial market data for cryptocurrencies, stocks, forex, and other assets. This capability is essential for DeFi applications, trading platforms, and any blockchain application requiring accurate, real-time financial data.
How Does Pyth Work?
Pyth Network operates through three core components: publishers, on-chain programs, and consumers. Over 120 reputable first-party data providers including exchanges and market makers publish pricing data to on-chain oracle programs. These oracle smart contracts aggregate and process data from multiple sources to create a single price feed per asset, available across more than 100 blockchains. Each price feed includes a confidence interval that quantifies data uncertainty. Since different market entities report varying prices for the same asset, Pyth calculates a confidence level. A tight interval indicates sources are agreeing closely (high confidence), while a wider interval suggests more variance or less liquidity (lower confidence). This metric helps developers understand the reliability of the price data they're consuming.
App Setup
Step 1: Install System Tools
1.1 Install Rust
# Download and install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# When prompted, press 1 for default installation
# Source the Rust environment
source $HOME/.cargo/env
# Install and set specific version
rustup install 1.75.0
rustup default 1.75.0
# Verify installation
rustc --version
# Expected output: rustc 1.75.0 (82e1608df 2023-12-21)
1.2 Install Solana CLI
# Install Solana 1.18.22 specifically
sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.22/install)"
# Add Solana to PATH
export PATH="/home/$USER/.local/share/solana/install/active_release/bin:$PATH"
# Make PATH permanent (add to your shell config)
echo 'export PATH="/home/$USER/.local/share/solana/install/active_release/bin:$PATH"' >> ~/.bashrc
# OR for zsh users:
echo 'export PATH="/home/$USER/.local/share/solana/install/active_release/bin:$PATH"' >> ~/.zshrc
# Reload shell configuration
source ~/.bashrc # or source ~/.zshrc
# Verify installation
solana --version
# Expected output: solana-cli 1.18.22 (src:9efdd74b; feat:4215500110, client:Agave)
Step 2: Configure Solana for Eclipse
2.1 Set Eclipse RPC URL
# Configure Solana to use Eclipse testnet
solana config set --url https://testnet.dev2.eclipsenetwork.xyz/
# Verify configuration
solana config get
# Should show: RPC URL: https://testnet.dev2.eclipsenetwork.xyz/
2.2 Create and Configure Wallet
# Generate new wallet (save the seed phrase!)
solana-keygen new --outfile mywallet.json
# Set as default wallet (new generated wallet file or preexisting)
solana config set --keypair ./mywallet.json
# Check your wallet address
solana address
# Example output: 4saf89xtUYFmqiwZU7BH7RX6udiXejjFiMDyyPAVyeEE
# Check balance (will be 0 initially)
solana balance
Step 3: Create Smart Contract Project
3.1 Initialize Cargo Project
# Create project directory
mkdir banana-ripeness-checker
cd banana-ripeness-checker
# Initialize as Rust library (not binary)
cargo init --lib
# Verify structure
ls -la
# Should show:
# .
# ├── Cargo.toml
# └── src/
# └── lib.rs
3.2 Configure Cargo.toml
# Open Cargo.toml in your editor
nano Cargo.toml
# OR
code Cargo.toml
Replace entire contents with:
[package]
name = "banana-ripeness-checker"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"] # Build as dynamic library for Solana
name = "banana_ripeness_checker"
[dependencies]
solana-program = "1.18.22" # Core Solana SDK
borsh = "0.10.3" # Serialization (Binary Object Representation)
bytemuck = "1.14.0" # Zero-copy byte manipulation
3.3 Write Smart Contract (lib.rs)
# Open lib.rs
nano src/lib.rs
# OR
code src/lib.rs
Replace entire contents with:
use solana_program::{
account_info::AccountInfo,
entrypoint, // Macro to define program entry
entrypoint::ProgramResult,
msg, // For logging
program_error::ProgramError,
pubkey::Pubkey,
};
use borsh::{BorshDeserialize, BorshSerialize};
// Temporary ID - will update after deployment
solana_program::declare_id!("11111111111111111111111111111111");
// This macro creates the program entry point
entrypoint!(process_instruction);
// Data structure matching TypeScript client
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct PriceData {
pub price: i64, // ETH price with decimals
pub decimals: i32, // Number of decimal places
}
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
instruction_data: &[u8], // Our serialized price data
) -> ProgramResult {
msg!("🍌 Banana Ripeness Checker starting...");
// Deserialize the price data from bytes
let price_data = PriceData::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
// Convert to USD (e.g., 255816500000 with 8 decimals = $2558.16)
let eth_price_usd = price_data.price / 10_i64.pow(price_data.decimals as u32);
msg!("🍌 ETH Price: ${}", eth_price_usd);
msg!("Raw price: {} with {} decimals", price_data.price, price_data.decimals);
const RIPENESS_THRESHOLD: i64 = 3000;
// Core business logic: Is the banana ripe?
if eth_price_usd >= RIPENESS_THRESHOLD {
msg!("🟢 YOUR BANANA IS RIPE! ETH is above $3000!");
msg!("🎉 Time to make banana bread!");
} else {
msg!("🟡 Your banana is not ripe yet. ETH is below $3000.");
msg!("⏰ Keep waiting, it'll ripen soon!");
}
Ok(())
}
3.4 Build and Deploy Smart Contract
# Build the program
cargo build-sbf
# Deploy and save the Program ID
solana program deploy target/deploy/banana_ripeness_checker.so
# You'll see output like:
# Program Id: 6VrSSGDWZ1KjWZuzAAJTssKyu2unLCcy7xyqkCDaYQAe
# IMPORTANT: Copy your Program ID!
# Update lib.rs with your Program ID
# Edit the declare_id! line with your actual Program ID
nano src/lib.rs
# Rebuild with updated ID
cargo build-sbf
# Redeploy to same address
solana program deploy target/deploy/banana_ripeness_checker.so --program-id YOUR_PROGRAM_ID
Step 4: Create TypeScript Client
4.1 Initialize NPM Project
# Create client directory
mkdir banana-cli
cd banana-cli
# Initialize npm project
npm init -y
# Create source directory
mkdir src
4.2 Create TypeScript Configuration (tsconfig.json)
# Create tsconfig.json
nano tsconfig.json
# OR
code tsconfig.json
Add this content:
{
"compilerOptions": {
"target": "es2020", // Modern JS features
"module": "commonjs", // Required for ts-node
"lib": ["es2020"], // Available JS APIs
"outDir": "./dist", // Compiled output
"rootDir": "./src", // Source location
"strict": true, // Type safety
"esModuleInterop": true, // Import compatibility
"skipLibCheck": true, // Faster builds
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true // Import JSON files
}
}
4.3 Update package.json Scripts
# Edit package.json
nano package.json
# OR
code package.json
Update the scripts section:
{
"name": "banana-cli",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "ts-node --transpile-only src/banana-checker.ts",
"build": "tsc",
"banana": "npm start" // Friendly alias
},
"dependencies": {
"@pythnetwork/hermes-client": "^2.0.0", // Pyth price feeds
"@solana/web3.js": "1.78.0", // Blockchain interaction
"rpc-websockets": "7.10.0", // EXACT version required!
"borsh": "^2.0.0", // Must match Rust side
"chalk": "^4.1.2", // Terminal colors
"figlet": "^1.8.1", // ASCII art
},
"devDependencies": {
"@types/node": "^18.0.0", // TypeScript types
"typescript": "^5.0.0",
"ts-node": "^10.9.2", // Direct TS execution
"@types/figlet": "^1.7.0",
}
}
4.4 Install NPM packages
# Install from package.json
npm install
4.5 Create TypeScript Client (banana-checker.ts)
# Create the main TypeScript file
nano src/banana-checker.ts
# OR
code src/banana-checker.ts
Add the complete TypeScript code:
import 'rpc-websockets/dist/lib/client';
import {
Connection,
PublicKey,
Keypair,
Transaction,
TransactionInstruction
} from '@solana/web3.js';
import { HermesClient } from '@pythnetwork/hermes-client'; // Pyth Network Hermes client for fetching oracle price data
import * as fs from 'fs';
import chalk from 'chalk'; // Terminal color styling
import figlet from 'figlet'; // ASCII art generation
const ECLIPSE_RPC = 'ECLIPSE_TESTNET_RPC_URL'; // Eclipse testnet RPC endpoint, public endpoint: https://testnet.dev2.eclipsenetwork.xyz/
const HERMES_URL = 'HERMES_API_URL'; // Pyth Hermes API URL, public endpoint: https://api.pyth.network/hermes
const ETH_USD_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace'; // Pyth ETH/USD price feed ID, find price feeds here: https://www.pyth.network/developers/price-feed-ids
const YOUR_PROGRAM_ID = 'YOUR_DEPLOYED_PROGRAM_ID_HERE'; // UPDATE THIS!
class PriceData {
price: bigint; // Using bigint for large numbers without precision loss
decimals: number; // Number of decimal places in the price
constructor(fields: { price: bigint; decimals: number }) {
this.price = fields.price;
this.decimals = fields.decimals;
}
}
class BananaRipenessChecker {
private connection: Connection;
private wallet: Keypair;
private hermesClient: HermesClient;
constructor(walletPath: string) {
const walletData = JSON.parse(fs.readFileSync(walletPath, 'utf8')); // Load wallet from JSON file
this.wallet = Keypair.fromSecretKey(new Uint8Array(walletData));
this.connection = new Connection(ECLIPSE_RPC, 'confirmed');
this.hermesClient = new HermesClient(HERMES_URL);
}
async checkBananaRipeness(): Promise<void> {
try {
// Step 1: Fetch ETH price from Pyth Network
console.log(chalk.blue('📡 Fetching ETH price from Pyth Network...'));
const priceUpdates = await this.hermesClient.getLatestPriceUpdates([ETH_USD_FEED]);
// Validate response structure
if (!priceUpdates || !Array.isArray(priceUpdates.parsed)) {
throw new Error('No price updates parsed from Hermes.');
}
const feedId = ETH_USD_FEED.startsWith("0x") ? ETH_USD_FEED.slice(2) : ETH_USD_FEED;
// Find our specific price feed in the response
const priceUpdate = priceUpdates.parsed.find(p => p.id === feedId);
if (!priceUpdate || !priceUpdate.price) {
throw new Error('No valid ETH price update received from Hermes.');
}
// Step 2: Process price data
const { price, expo } = priceUpdate.price;
const priceValue = BigInt(price); // Convert to BigInt for precision
const decimals = Math.abs(expo); // Convert negative exponent to positive decimals
// Calculate human-readable price for display
const displayPrice = Number(priceValue) / Math.pow(10, decimals);
console.log(chalk.cyan(`💰 Current ETH Price: $${displayPrice.toFixed(2)}`));
// Step 3: Prepare data for smart contract
const priceDataForContract = new PriceData({
price: priceValue,
decimals: decimals
});
// Step 4: Manual serialization - CRITICAL for Rust compatibility!
const buffer = Buffer.alloc(12);
buffer.writeBigInt64LE(priceDataForContract.price, 0);
buffer.writeInt32LE(priceDataForContract.decimals, 8);
const serializedData = buffer;
// Step 5: Create blockchain transaction
console.log(chalk.blue('\n🔍 Checking banana status on Eclipse blockchain...\n'));
// Build instruction for our smart contract
const instruction = new TransactionInstruction({
programId: new PublicKey(YOUR_PROGRAM_ID), // Target program
keys: [], // No accounts needed for this simple program
data: serializedData // Our serialized price data
});
// Wrap instruction in a transaction
const transaction = new Transaction().add(instruction);
// Step 6: Send transaction and wait for confirmation
try {
// Send transaction to blockchain
// skipPreflight: false = run simulation first to catch errors
const signature = await this.connection.sendTransaction(
transaction,
[this.wallet], // Array of signers (just our wallet)
{ skipPreflight: false }
);
// Wait for blockchain confirmation
await this.connection.confirmTransaction(signature, 'confirmed');
// Step 7: Retrieve transaction details to read program logs
const txDetails = await this.connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0
});
// Step 8: Parse and display smart contract output
if (txDetails?.meta?.logMessages) {
const logs = txDetails.meta.logMessages;
console.log(chalk.gray('\n📜 Smart Contract Says:\n'));
// Extract only our program's logs (remove system messages)
logs.forEach(log => {
if (log.includes('Program log:')) {
// Remove the "Program log: " prefix to show just our message
const message = log.replace('Program log: ', '');
console.log(message);
}
});
}
} catch (error: any) {
console.error(chalk.red('❌ Error checking ripeness:'), error.message || error);
}
// Step 9: Display wallet information
console.log(chalk.gray(`\n📍 Your wallet: ${this.wallet.publicKey.toBase58()}`));
// Get and display balance (Eclipse uses ETH, not SOL)
const balance = await this.connection.getBalance(this.wallet.publicKey);
console.log(chalk.gray(`💰 Balance: ${balance / 1e9} ETH\n`));
} catch (error: any) {
console.error(chalk.red('❌ Error:'), error.message || error);
}
}
async showWelcome(): Promise<void> {
console.clear();
console.log('\n');
// Generate ASCII art title using figlet
const title = figlet.textSync('Banana Checker', {
font: 'Standard',
horizontalLayout: 'default',
verticalLayout: 'default'
});
// Display title in banana yellow
console.log(chalk.yellow(title));
console.log(
chalk.bold.hex('#7142CF')(' Pyth') +
chalk.white(' × ') +
chalk.bold.hex('#a1fe9f')('Eclipse')
);
// Brief description
console.log(chalk.cyan('\n📡 Live ETH/USD oracle data\n'));
}
}
async function main() {
const checker = new BananaRipenessChecker('./mywallet.json');
await checker.showWelcome();
console.log(chalk.blue('Press any key to check your banana...'));
await new Promise(resolve => process.stdin.once('data', resolve));
await checker.checkBananaRipeness();
console.log(chalk.gray('Press Ctrl+C to exit'));
}
if (require.main === module) {
main().catch(console.error);
}
⚠️ IMPORTANT: Update YOUR_PROGRAM_ID with your actual deployed Program ID!
4.6 Copy Wallet File
# Copy your wallet to the CLI directory
cp ../banana-ripeness-checker/mywallet.json ./mywallet.json
# Verify it exists
ls -la mywallet.json
Step 5: Run the Application
5.1 Final Checklist
# 1. Verify you're in banana-cli directory
pwd
# Should show: .../banana-cli
# 2. Check all files exist
ls -la
# Should show:
# - package.json
# - tsconfig.json
# - mywallet.json
# - src/banana-checker.ts
# - node_modules/
# 3. Verify Program ID is updated
grep "YOUR_PROGRAM_ID" src/banana-checker.ts
# Should NOT show placeholder - should show your actual Program ID
5.2 Run the Banana Checker!
# Execute the application
npm run banana
# Expected output:
# 1. ASCII art welcome screen
# 2. "Press any key to check your banana..."
# 3. Fetching ETH price from Pyth
# 4. Sending transaction to Eclipse
# 5. Smart contract response showing banana status
Next Steps
After successfully implementing the banana ripeness checker, consider exploring:
- Direct integration of the Pyth Solana receiver SDK in your Rust program.
- Additional features for your CLI application, such as user input for price thresholds.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.