本教程介绍了如何使用Pyth网络和Eclipse测试网创建一个Typescript命令行应用程序,根据ETH价格判断香蕉是否成熟。教程涵盖了Pyth网络的基本原理、价格数据的获取、Rust智能合约的部署与交互,以及Typescript客户端的开发。
在本指南中,你将学习如何将 Pyth 与 Eclipse 测试网一起使用,来创建一个 TypeScript CLI 应用程序。此应用程序将根据从 Pyth 网络获取的 ETH 价格来确定香蕉是否成熟。
学习如何通过创建一个检查 ETH 价格并确定香蕉成熟度的 TypeScript CLI 应用程序,将 Pyth 与 Eclipse 测试网集成。
QuickNode
QuickNode
在观看
/
订阅我们的 YouTube 频道以获取更多视频! 订阅
Pyth Network 是一个高保真、低延迟的预言机网络,它将链下金融数据与区块链生态系统连接起来。由于区块链是确定性和封闭的系统(对于所有节点上的相同输入产生相同的输出,没有外部影响),因此它们无法本地访问链下信息。 Pyth 通过从各种来源收集实时数据并在链上提供这些数据来解决这个问题,从而使智能合约能够访问加密货币、股票、外汇和其他资产的金融市场数据。这种能力对于 DeFi 应用程序、交易平台以及任何需要准确、实时金融数据的区块链应用程序至关重要。
Pyth Network 通过三个核心组件运行:
超过 120 家信誉良好的第一方数据提供商(包括交易所和做市商)将定价数据发布到链上预言机程序。这些预言机智能合约聚合和处理来自多个来源的数据,从而为每个资产创建一个单一的价格源,该价格源可在 100 多个区块链上使用。每个价格源都包含一个量化数据不确定性的置信区间。由于不同的市场实体报告了同一资产的不同价格,Pyth 会计算置信水平。窄区间表示源之间非常一致(高置信度),而宽区间表示方差较大或流动性较低(低置信度)。此指标可帮助开发人员了解他们所使用的价格数据的可靠性。
## 下载并安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
## 出现提示时,按 1 进行默认安装
## 获取 Rust 环境
source $HOME/.cargo/env
## 安装并设置特定版本
rustup install 1.75.0
rustup default 1.75.0
## 验证安装
rustc --version
## 预期输出:rustc 1.75.0 (82e1608df 2023-12-21)
注意: 我们安装特定版本的某些依赖项,以使它们能够相互协作
## 专门安装 Solana 1.18.22
sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.22/install)"
## 将 Solana 添加到 PATH
export PATH="/home/$USER/.local/share/solana/install/active_release/bin:$PATH"
## 使 PATH 永久生效(添加到你的 shell 配置)
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
## 重新加载 shell 配置
source ~/.bashrc # or source ~/.zshrc
## 验证安装
solana --version
## 预期输出:solana-cli 1.18.22 (src:9efdd74b; feat:4215500110, client:Agave)
## 配置 Solana 以使用 Eclipse 测试网
solana config set --url https://testnet.dev2.eclipsenetwork.xyz/
## 验证配置
solana config get
## 应该显示:RPC URL: https://testnet.dev2.eclipsenetwork.xyz/
## 生成新钱包(保存种子短语!)
solana-keygen new --outfile mywallet.json
## 设置为默认钱包(新生成的钱包文件或预先存在的钱包)
solana config set --keypair ./mywallet.json
## 检查你的钱包地址
solana address
## 示例输出:4saf89xtUYFmqiwZU7BH7RX6udiXejjFiMDyyPAVyeEE
## 检查余额(最初将为 0)
solana balance
## 创建项目目录
mkdir banana-ripeness-checker
cd banana-ripeness-checker
## 初始化为 Rust 库(非二进制文件)
cargo init --lib
## 验证结构
ls -la
## 应该显示:
## .
## ├── Cargo.toml
## └── src/
## └── lib.rs
## 在你的编辑器中打开 Cargo.toml
nano Cargo.toml
## OR
code Cargo.toml
将全部内容替换为:
[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
## 打开 lib.rs
nano src/lib.rs
## OR
code src/lib.rs
将全部内容替换为:
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(())
}
一个 rust 程序,它根据当前的 ETH 价格确定香蕉是否“成熟”。
这是它的作用:
PriceData 结构:price (i64) 和 decimals (i32)。msg! 宏来记录将出现在交易日志中的消息,TypeScript CLI 应用程序随后可以检索这些消息并将其显示给用户。## 构建程序
cargo build-sbf
## 部署并保存程序 ID
solana program deploy target/deploy/banana_ripeness_checker.so
## 你会看到如下输出:
## Program Id: 6VrSSGDWZ1KjWZuzAAJTssKyu2unLCcy7xyqkCDaYQAe
## 重要提示:复制你的程序 ID!
## 使用你的程序 ID 更新 lib.rs
## 使用你的实际程序 ID 编辑 declare_id! 行
nano src/lib.rs
## 使用更新的 ID 重新构建
cargo build-sbf
## 重新部署到同一地址
solana program deploy target/deploy/banana_ripeness_checker.so --program-id YOUR_PROGRAM_ID
## 创建客户端目录
mkdir banana-cli
cd banana-cli
## 初始化 npm 项目
npm init -y
## 创建源目录
mkdir src
## 创建 tsconfig.json
nano tsconfig.json
## OR
code tsconfig.json
添加以下内容:
{
"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
}
}
## 编辑 package.json
nano package.json
## OR
code package.json
更新 scripts 部分:
{
"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",
}
}
## 从 package.json 安装
npm install
## 创建主 TypeScript 文件
nano src/banana-checker.ts
## OR
code src/banana-checker.ts
添加完整的 TypeScript 代码:
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);
}
⚠️ 重要提示:使用你实际部署的程序 ID 更新 YOUR_PROGRAM_ID!
## 将你的钱包复制到 CLI 目录
cp ../banana-ripeness-checker/mywallet.json ./mywallet.json
## 验证它是否存在
ls -la mywallet.json
## 1. 验证你是否在 banana-cli 目录中
pwd
## 应该显示:.../banana-cli
## 2. 检查所有文件是否存在
ls -la
## 应该显示:
## - package.json
## - tsconfig.json
## - mywallet.json
## - src/banana-checker.ts
## - node_modules/
## 3. 验证程序 ID 是否已更新
grep "YOUR_PROGRAM_ID" src/banana-checker.ts
## 不应显示占位符 - 应显示你的实际程序 ID
## 执行应用程序
npm run banana
## 预期输出:
## 1. ASCII 艺术欢迎屏幕
## 2. “按任意键检查你的香蕉...”
## 3. 从 Pyth 获取 ETH 价格
## 4. 将交易发送到 Eclipse
## 5. 显示香蕉状态的智能合约响应
成功实施香蕉成熟度检查器后,请考虑探索:
如果你有任何反馈或对新主题的请求,请告知我们。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/oth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!