如何将Pyth与Eclipse SVM一起使用

本教程介绍了如何使用Pyth网络和Eclipse测试网创建一个Typescript命令行应用程序,根据ETH价格判断香蕉是否成熟。教程涵盖了Pyth网络的基本原理、价格数据的获取、Rust智能合约的部署与交互,以及Typescript客户端的开发。

概述

在本指南中,你将学习如何将 PythEclipse 测试网一起使用,来创建一个 TypeScript CLI 应用程序。此应用程序将根据从 Pyth 网络获取的 ETH 价格来确定香蕉是否成熟。

学习如何通过创建一个检查 ETH 价格并确定香蕉成熟度的 TypeScript CLI 应用程序,将 Pyth 与 Eclipse 测试网集成。

QuickNode

如何在 Eclipse SVM 中使用 Pyth

QuickNode

在观看

/

订阅我们的 YouTube 频道以获取更多视频! 订阅

前提条件

  • 与 Solana 虚拟机 (SVM) 兼容的 Eclipse 钱包。 例如,Backpack
  • Eclipse SVM 钱包中的 Testnet ETH(使用 Eclipse 测试网桥
  • 对 TypeScript 和 Rust 编程语言的基本了解

你将学到什么

  • 什么是 Pyth 网络以及它的工作原理
  • 如何从 Pyth 网络获取价格数据
  • 如何部署 Rust 智能合约并与之交互

什么是 Pyth 网络?

Pyth Network 是一个高保真、低延迟的预言机网络,它将链下金融数据与区块链生态系统连接起来。由于区块链是确定性和封闭的系统(对于所有节点上的相同输入产生相同的输出,没有外部影响),因此它们无法本地访问链下信息。 Pyth 通过从各种来源收集实时数据并在链上提供这些数据来解决这个问题,从而使智能合约能够访问加密货币、股票、外汇和其他资产的金融市场数据。这种能力对于 DeFi 应用程序、交易平台以及任何需要准确、实时金融数据的区块链应用程序至关重要。

Pyth 如何运作?

Pyth Network 通过三个核心组件运行:

  • 发布者 (Publishers)
  • 链上程序 (Onchain programs)
  • 消费者 (Consumers)

超过 120 家信誉良好的第一方数据提供商(包括交易所和做市商)将定价数据发布到链上预言机程序。这些预言机智能合约聚合和处理来自多个来源的数据,从而为每个资产创建一个单一的价格源,该价格源可在 100 多个区块链上使用。每个价格源都包含一个量化数据不确定性的置信区间。由于不同的市场实体报告了同一资产的不同价格,Pyth 会计算置信水平。窄区间表示源之间非常一致(高置信度),而宽区间表示方差较大或流动性较低(低置信度)。此指标可帮助开发人员了解他们所使用的价格数据的可靠性。

应用设置

步骤 1:安装系统工具

1.1 安装 Rust
## 下载并安装 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)

注意: 我们安装特定版本的某些依赖项,以使它们能够相互协作

1.2 安装 Solana CLI
## 专门安装 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)

步骤 2:配置 Solana 以用于 Eclipse

2.1 设置 Eclipse RPC URL
## 配置 Solana 以使用 Eclipse 测试网
solana config set --url https://testnet.dev2.eclipsenetwork.xyz/

## 验证配置
solana config get
## 应该显示:RPC URL: https://testnet.dev2.eclipsenetwork.xyz/
2.2 创建和配置钱包
## 生成新钱包(保存种子短语!)
solana-keygen new --outfile mywallet.json

## 设置为默认钱包(新生成的钱包文件或预先存在的钱包)
solana config set --keypair ./mywallet.json

## 检查你的钱包地址
solana address
## 示例输出:4saf89xtUYFmqiwZU7BH7RX6udiXejjFiMDyyPAVyeEE

## 检查余额(最初将为 0)
solana balance

步骤 3:创建智能合约项目

3.1 初始化 Cargo 项目
## 创建项目目录
mkdir banana-ripeness-checker
cd banana-ripeness-checker

## 初始化为 Rust 库(非二进制文件)
cargo init --lib

## 验证结构
ls -la
## 应该显示:
## .
## ├── Cargo.toml
## └── src/
##     └── lib.rs
3.2 配置 Cargo.toml
## 在你的编辑器中打开 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
3.3 编写智能合约 (lib.rs)
## 打开 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 价格确定香蕉是否“成熟”。

这是它的作用:

  1. 接收价格数据:合约接受来自 TypeScript 客户端的序列化价格数据,其中包含 ETH 价格(作为带有小数的整数)和小数位数。
  2. 反序列化数据:它使用 Borsh 反序列化将原始字节转换为具有两个字段的 PriceData 结构:price (i64) 和 decimals (i32)。
  3. 转换为美元:它通过将原始价格除以 10 的小数次幂来计算美元的实际 ETH 价格。例如,如果原始价格为 255816500000,小数为 8,则变为 2558.16 美元。
  4. 检查阈值:它将 ETH 价格与 3000 美元的硬编码阈值进行比较。
  5. 返回结果:如果 ETH >= 3000 美元,它会记录“你的香蕉成熟了!”消息。 如果低于 3000 美元,它会记录“你的香蕉尚未成熟”消息。
  6. 记录所有内容:在整个过程中,它使用 msg! 宏来记录将出现在交易日志中的消息,TypeScript CLI 应用程序随后可以检索这些消息并将其显示给用户。
3.4 构建和部署智能合约
## 构建程序
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

步骤 4:创建 TypeScript 客户端

4.1 初始化 NPM 项目
## 创建客户端目录
mkdir banana-cli
cd banana-cli

## 初始化 npm 项目
npm init -y

## 创建源目录
mkdir src
4.2 创建 TypeScript 配置 (tsconfig.json)
## 创建 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
  }
}
4.3 更新 package.json 脚本
## 编辑 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",
  }
}
4.4 安装 NPM 包
## 从 package.json 安装
npm install
4.5 创建 TypeScript 客户端 (banana-checker.ts)
## 创建主 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

4.6 复制钱包文件
## 将你的钱包复制到 CLI 目录
cp ../banana-ripeness-checker/mywallet.json ./mywallet.json

## 验证它是否存在
ls -la mywallet.json

步骤 5:运行应用程序

5.1 最终检查清单
## 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
5.2 运行香蕉检查器!
## 执行应用程序
npm run banana

## 预期输出:
## 1. ASCII 艺术欢迎屏幕
## 2. “按任意键检查你的香蕉...”
## 3. 从 Pyth 获取 ETH 价格
## 4. 将交易发送到 Eclipse
## 5. 显示香蕉状态的智能合约响应

后续步骤

成功实施香蕉成熟度检查器后,请考虑探索:

  • 将 Pyth Solana 接收器 SDK 直接集成到你的 Rust 程序中。
  • CLI 应用程序的附加功能,例如价格阈值的用户输入。
我们 ❤️ 反馈!

如果你有任何反馈或对新主题的请求,请告知我们。我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/oth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。