Pyth Networkでオンチェインデータフィードを取得する

最近、Solana基盤のOracleであるPythNetworkを使って、$1あたりのwei(Eth)を計算することをしました。Pyth Network自体のマニュアルがよくできているため、ハマりどころは少なく、そのためもあってかまだ技術者ブログなどのコミュニティ記事も少なめでした。日本語での紹介はほとんどなかったので、今回簡単にまとめてみました。
PythNetworkとは
Pyth Networkは、Solanaブロックチェーン基盤の分散型のクロスチェーンデータオラクルです。Web3の開発者や、仮想通貨トレーダーなどにリアルタイムで正確なマーケットデータを提供することを目的としています。主に暗号通貨の価格データに特化していて、30以上のブロックチェーンにデータ配信をしています。
他の分散型データオラクルと比べて、価格の更新間隔が短いため、より正確な価格をオンチェインでも取得することができます。これを実現するために、特に面白いと思ったのが”Pull Oracle”というコンセプトです。
https://docs.pyth.network/documentation/pythnet-price-feeds/on-demand
通常のオラクルが、価格更新のトランザクションをロボティックに行っていることに対して、Pyth Networkでは、オンチェインで価格を取得したい人(アプリケーション)が、自らオフチェインのAPIから値を取得し、オンチェインで更新することで(自分が書き込んだ値も含めて)最新の値が取得される様になっています。
この工夫によって、ネットワークが負担するガス代がなくなりますので、価格フィードやネットワークをどんどん拡張できる様になっています(実際に多い)。大変スマートで良いですね。
Pull Oracleの手順
Pull Oracleの方法は具体的には次の様になります。
- オフチェインのAPI(Hermes)から最新のデータフィードを取得しておく。
- オンチェインのAPIから更新価格を取得しておく。
- 価格を参照したいコントラクトでは、まずArgで渡されたデータフィードを使って、オンチェインのAPIで価格を更新します。
- 更新にガス代とは別に手数料がかかりますので、価格を参照したいトランザクションはpayableである必要があり、かつ2で取得した更新価格以上の送金が必要です。
- 価格更新の後に、価格取得を行います。
$1あたりWEIを取得するサンプル
Hermesからデータフィードを取得してくる関数(TypeScript)。こちらはaxiosでHermesのデータフィードを取ってくるだけのものです。Web2アプリで通貨ペアの価格をただ淡々と表示したいというニーズなら、このHermesをポーリングすれば簡単ですね。とはいえ、このAPIがパーミッションレスなので、クラウドホスティングなどを使っていると、巻き込みで制限されたりしないか心配になりますね。
調べてみると、SolanaのRPCプロバイダなどを使って、Hermesのノードを自分でホスティングすることや、特定のPRCノードプロバイダが運用しているNodeを利用することも可能な様です。プロダクションで使う場合は専用ノードを用意するか、データフィードの使用度が少ないなら複数のpublicエンドポイントでフォールバックがかかる様にしておいた方が良さそうです。
import axios, { AxiosRequestConfig } from "axios"
const HERMES_BASE_URL = process.env.HERMES_BASE_URL || "https://hermes.pyth.network"
type PythPrice = {
id: string
price: {
price: string
conf: string
expo: number
publish_time: number
},
metadata?: {
slot: number
emitter_chain: number
price_service_receive_time: number
prev_publish_time: number
}
ema_price: {
price: string
conf: string
expo: number
publish_time: number
}
vaa?: string
}
export const getPriceData = async (priceFeedID: string) => {
let config: AxiosRequestConfig = {
method: "get",
maxBodyLength: Infinity,
url: `${HERMES_BASE_URL}/api/latest_price_feeds?ids[]=${priceFeedID}&binary=true`,
headers: {},
}
try {
const result = await axios.request<PythPrice[]>(config)
if (result.status === 200) {
return result.data
}
} catch (e) {
console.log(e)
}
}
以下はマルチチェーンのアプリケーションで、Pythのプライスフィード(updateData)とフィーをそれぞれ取得するサンプルです。chainIDには対応するネットワークのIDを入れてgetPythData関数は、そのチェーンでのPythコントラクトのアドレスと、ネイティブトークン/USDペアを返す様になっています。
対応EVMはこちらにリストアップされています:
https://docs.pyth.network/documentation/pythnet-price-feeds/evm
PriceFeedのIDはこちらで検索できます:
https://pyth.network/developers/price-feed-ids#pyth-evm-mainnet
上で説明した関数から返ってきた値を使って、オンチェーンのAPIからupdateFeeを取得し、そのセットを返します。
この例では、web3ライブラリとしてはviemを使っています。ポイントとしてはupdateDataがbase64のバイナリーとして返ってきますが、それをtoHexでhex文字列にして、viemのインターフェースではargsに[vaa_hex]の様に配列として渡すところでしょうか。
import { Address, toHex } from 'viem'
import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json"
const getPythDataAndFee = async(chainId)=>{
const { address, feedId } = getPythData(chainId)
const priceDataArray = await getPriceData(feedId)
if (priceDataArray === undefined || priceDataArray.length === 0 || !priceDataArray[0].vaa) throw new Error("failed to get Pyth Price Data")
const vaa_base64 = priceDataArray[0].vaa
const vaa = Buffer.from(vaa_base64, 'base64')
const vaa_hex = toHex(vaa)
const client = getNetworkProvider(chainId)
const fee = await client.readContract({
address: address as Address,
abi: PythAbi,
functionName: "getUpdateFee",
args: [[vaa_hex]]
}) as BigInt
if (fee === undefined) throw new Error("failed to get Pyth Update Fee")
return {fee, vaa_hex}
}
$1あたりのwei(Ethにおける最小単位)をオンチェインで取得する部分のスマートコントラクト(Solidity)です。ポイントとしてpayableになっていますので、この関数はコントラクト内のupdateFeeに対応するだけの金額のペイトランザクションから呼ばれる必要があります。
引数のputhUpdateDataがオフチェインで取得された価格フィードデータです。
価格が、PythStructという構造体で取得されますが、この構造体内でpriceとexpoという値が、それぞれint64とint32で帰ってきます。これが指数表記になっており、Eth/USDペアで本日の価格ではprice:187948929431,expo:-8の様に返ってきます。次の例では、これを$1あたりのWEI価格になおす処理を行なっています。
constant uint MINIMUM_UNIT_EXPO=18;
function getUnitPricePerUSD(
bytes[] memory pythUpdateData
) public payable returns (uint256 price_per_usd) {
uint256 updateFee = pyth.getUpdateFee(pythUpdateData);
require(msg.value - updateFee >= 0, "insufficient fee");
pyth.updatePriceFeeds{value: updateFee}(pythUpdateData);
PythStructs.Price memory tokenPriceInUSD = pyth.getPrice(priceId);
require(tokenPriceInUSD.price > 0, "price should be greater than 0");
uint expo;
if (tokenPriceInUSD.expo < 0) {
expo = MINIMUM_UNIT_EXPO + uint(uint32(-tokenPriceInUSD.expo));
} else {
if(MINIMUM_UNIT_EXPO < tokenPriceInUSD.expo){
expo = 0;
}else{
expo = MINIMUM_UNIT_EXPO - uint(uint32(tokenPriceInUSD.expo));
}
}
uint unitPricePerUSD = 10 ** expo / uint(uint64(tokenPriceInUSD.price));
return unitPricePerUSD;
}
まとめ
Pyth Networkを使って、Pull Oracle(= On-Demand Update)によって、EVM上で現在のEth/USDペアを取得するサンプルをご紹介しました。
注意点として、プロダクションで利用する際は、オフチェインでのAPIの取得があるため、専用(dedicated)のHermesノードを用意したり、フォールバックの仕組みを整える必要がありそうです。またPush型のOracleと異なり、オフチェインでのデータ取得があるなど開発では少し手数が多いです。しかし、フィードの種類の多さと更新頻度、対応チェーンの多さなどとても有用性の高いプラットフォームに思います。
ファブリカコミュニケーションズで働いてみませんか?
あったらいいな、をカタチに。人々を幸せにする革新的なサービスを、私たちと一緒に創っていくメンバーを募集しています。
ファブリカコミュニケーションズの社員は「全員がクリエイター」。アイデアの発信に社歴や部署の垣根はありません。
“自分から発信できる人に、どんどんチャンスが与えられる“そんな環境で活躍してみませんか?ご興味のある方は、以下の採用ページをご覧ください。