开始之前
先设置一次许可证密钥。三个 SDK 都会从环境变量中读取它,因此密钥永远不必出现在代码里。export ORBITFLARE_LICENSE_KEY=ORBIT-XXXXXX-NNNNNN-NNNNNN
fra)。请把主机名改为离您的服务器最近的区域。如果您的许可证处于 API Key Only 模式,SDK 也会把密钥发送给 Jetstream 和 Yellowstone。在 IP Whitelist 模式下,运行流式示例之前,请先在控制台中把服务器的 IP 添加到许可证。
- TypeScript
- Rust
- Go
npm init -y && npm pkg set type=module
npm install @orbitflare/sdk ws @grpc/grpc-js yaml bs58
ws 和 @grpc/grpc-js 是 SDK 的可选 peer 依赖。缺少它们时,WebSocket 和 Jetstream 客户端会报错 Cannot find package 'ws' 或 Cannot find package '@grpc/grpc-js'。使用 npx tsx file.ts 运行文件。Cargo.toml
[package]
name = "recipes"
version = "0.1.0"
edition = "2024"
[dependencies]
orbitflare-sdk = { version = "0.4", features = ["ws", "jetstream"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
bs58 = "0.5"
examples/ 下,并用 cargo run --example name 运行。go mod init recipes
go get github.com/orbitflare/orbitflare-sdk-go@latest github.com/mr-tron/base58
main.go,并用 go run ./folder 运行。检查密钥是否可用
这是排除配置问题最快的方法。如果它打印出一个 slot 编号,说明您的密钥、区域和网络路径都没有问题。curl -s https://fra.rpc.orbitflare.com \
-H "x-api-key: $ORBITFLARE_LICENSE_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getSlot"}'
import { RpcClientBuilder } from '@orbitflare/sdk';
// The key comes from ORBITFLARE_LICENSE_KEY, so it never has to appear in code.
const rpc = new RpcClientBuilder().url('https://fra.rpc.orbitflare.com').build();
// A slot number back means the key, region and network path all work.
console.log('slot:', await rpc.getSlot());
use orbitflare_sdk::RpcClientBuilder;
#[tokio::main]
async fn main() -> orbitflare_sdk::Result<()> {
// The key comes from ORBITFLARE_LICENSE_KEY, so it never has to appear in code.
let rpc = RpcClientBuilder::new()
.url("https://fra.rpc.orbitflare.com")
.build()?;
// A slot number back means the key, region and network path all work.
println!("slot: {}", rpc.get_slot().await?);
Ok(())
}
package main
import (
"context"
"fmt"
"log"
"github.com/orbitflare/orbitflare-sdk-go/rpc"
)
func main() {
// The key comes from ORBITFLARE_LICENSE_KEY, so it never has to appear in code.
client, err := rpc.NewBuilder().URL("https://fra.rpc.orbitflare.com").Build()
if err != nil {
log.Fatal(err)
}
// A slot number back means the key, region and network path all work.
slot, err := client.GetSlot(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("slot:", slot)
}
| 响应 | 含义 |
|---|---|
-32401 missing API key(HTTP 401) | 没有发送密钥。请检查 x-api-key 请求头或 ?api_key= 参数,或确认 ORBITFLARE_LICENSE_KEY 已在同一个 shell 中导出 |
-32402 invalid API key(HTTP 403) | 密钥错误或已被重新生成。请从控制台重新复制 |
-32404 IP not whitelisted(HTTP 403) | 您的许可证不允许这台机器的 IP 或这个区域。请在控制台中添加该 IP,或尝试许可证包含的区域 |
监听账户变化
通过 WebSocket 订阅并打印每一次更新。示例监听 Clock sysvar,因为它每个 slot 都会变化,您可以立即看到输出。可以换成任意账户地址。wscat -c "wss://fra.rpc.orbitflare.com?api_key=$ORBITFLARE_LICENSE_KEY"
> {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["SysvarC1ock11111111111111111111111111111111",{"encoding":"base64","commitment":"confirmed"}]}
import { WsClientBuilder } from '@orbitflare/sdk/ws';
// The SDK pings every 10 s and resubscribes on its own after a reconnect.
const ws = await new WsClientBuilder().url('wss://fra.rpc.orbitflare.com').build();
// Notifies only when the account changes. The Clock sysvar changes every slot.
const sub = await ws.accountSubscribe('SysvarC1ock11111111111111111111111111111111', 'confirmed');
for (let account = await sub.next(); account !== undefined; account = await sub.next()) {
// Clock layout: the first 8 bytes are the current slot as a little-endian u64.
const data = Buffer.from(account.data[0], 'base64');
console.log(`clock slot ${data.readBigUInt64LE(0)}, ${account.lamports} lamports`);
}
use orbitflare_sdk::WsClientBuilder;
#[tokio::main]
async fn main() -> orbitflare_sdk::Result<()> {
// The SDK pings every 10 s and resubscribes on its own after a reconnect.
let ws = WsClientBuilder::new()
.url("wss://fra.rpc.orbitflare.com")
.build()
.await?;
// Notifies only when the account changes. The Clock sysvar changes every slot.
let mut sub = ws
.account_subscribe("SysvarC1ock11111111111111111111111111111111", "confirmed")
.await?;
// Each update is the account's full new state: lamports, owner and base64 data.
while let Some(account) = sub.next().await {
println!(
"{} lamports, data {}",
account["lamports"], account["data"][0]
);
}
Ok(())
}
package main
import (
"context"
"fmt"
"log"
"github.com/orbitflare/orbitflare-sdk-go/ws"
)
func main() {
ctx := context.Background()
// The SDK pings every 10 s and resubscribes on its own after a reconnect.
client, err := ws.NewBuilder().URL("wss://fra.rpc.orbitflare.com").Build(ctx)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Notifies only when the account changes. The Clock sysvar changes every slot.
sub, err := client.AccountSubscribe(ctx, "SysvarC1ock11111111111111111111111111111111", "confirmed")
if err != nil {
log.Fatal(err)
}
defer sub.Unsubscribe()
// Each update is the account's full new state: lamports, owner and base64 data.
for {
account, ok := sub.Next(ctx)
if !ok {
return
}
fmt.Println(string(account))
}
}
等待交易确认
在命令行中传入签名。程序会轮询getSignatureStatuses,直到交易已确认、失败,或者明显不会再上链为止。
curl -s https://fra.rpc.orbitflare.com \
-H "x-api-key: $ORBITFLARE_LICENSE_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getSignatureStatuses","params":[["SIGNATURE"],{"searchTransactionHistory":true}]}'
import { RpcClientBuilder } from '@orbitflare/sdk';
const signature = process.argv[2];
const rpc = new RpcClientBuilder().url('https://fra.rpc.orbitflare.com').build();
// 120 polls 500 ms apart is 60 s, well past the ~30 s a blockhash stays valid.
for (let attempt = 0; attempt < 120; attempt++) {
// searchTransactionHistory also finds signatures older than the node's recent status cache.
const { value } = await rpc.request('getSignatureStatuses', [
[signature],
{ searchTransactionHistory: true },
]);
const status = value[0];
// err set means it landed and failed. The fee was still charged.
if (status?.err) {
console.log('landed but failed:', JSON.stringify(status.err));
process.exit(1);
}
if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') {
console.log(`${status.confirmationStatus} in slot ${status.slot}`);
process.exit(0);
}
// null means the cluster has not seen it yet, so keep polling.
await new Promise((r) => setTimeout(r, 500));
}
console.log('not found after 60s: the transaction was dropped or its blockhash expired');
use std::time::Duration;
use orbitflare_sdk::RpcClientBuilder;
use serde_json::json;
#[tokio::main]
async fn main() -> orbitflare_sdk::Result<()> {
let signature = std::env::args()
.nth(1)
.expect("usage: wait_for_signature <SIGNATURE>");
let rpc = RpcClientBuilder::new()
.url("https://fra.rpc.orbitflare.com")
.build()?;
// 120 polls 500 ms apart is 60 s, well past the ~30 s a blockhash stays valid.
for _ in 0..120 {
// searchTransactionHistory also finds signatures older than the node's recent status cache.
let res = rpc
.request(
"getSignatureStatuses",
json!([[signature], {"searchTransactionHistory": true}]),
)
.await?;
let status = &res["value"][0];
// err set means it landed and failed. The fee was still charged.
if !status["err"].is_null() {
println!("landed but failed: {}", status["err"]);
std::process::exit(1);
}
if let Some(level @ ("confirmed" | "finalized")) = status["confirmationStatus"].as_str() {
println!("{level} in slot {}", status["slot"]);
return Ok(());
}
// null means the cluster has not seen it yet, so keep polling.
tokio::time::sleep(Duration::from_millis(500)).await;
}
println!("not found after 60s: the transaction was dropped or its blockhash expired");
Ok(())
}
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"github.com/orbitflare/orbitflare-sdk-go/rpc"
)
type statuses struct {
Value []*struct {
Slot uint64 `json:"slot"`
Err json.RawMessage `json:"err"`
ConfirmationStatus string `json:"confirmationStatus"`
} `json:"value"`
}
func main() {
signature := os.Args[1]
client, err := rpc.NewBuilder().URL("https://fra.rpc.orbitflare.com").Build()
if err != nil {
log.Fatal(err)
}
// 120 polls 500 ms apart is 60 s, well past the ~30 s a blockhash stays valid.
for range 120 {
// searchTransactionHistory also finds signatures older than the node's recent status cache.
raw, err := client.Request(context.Background(), "getSignatureStatuses",
[]any{[]string{signature}, map[string]bool{"searchTransactionHistory": true}})
if err != nil {
log.Fatal(err)
}
var res statuses
if err := json.Unmarshal(raw, &res); err != nil {
log.Fatal(err)
}
if s := res.Value[0]; s != nil {
// err set means it landed and failed. The fee was still charged.
if string(s.Err) != "null" && len(s.Err) > 0 {
fmt.Println("landed but failed:", string(s.Err))
os.Exit(1)
}
if s.ConfirmationStatus == "confirmed" || s.ConfirmationStatus == "finalized" {
fmt.Printf("%s in slot %d\n", s.ConfirmationStatus, s.Slot)
return
}
}
// null means the cluster has not seen it yet, so keep polling.
time.Sleep(500 * time.Millisecond)
}
fmt.Println("not found after 60s: the transaction was dropped or its blockhash expired")
}
searchTransactionHistory 轮询适用于任何签名,无论新旧。signatureSubscribe 只知道最近的交易,因此订阅一笔几分钟前就已确认的交易永远不会收到通知。状态为 null 表示集群还没有见到这笔交易。交易调试介绍了遇到这种情况该怎么办。
订阅程序的交易流
使用 Jetstream v2 打印每一笔涉及 pump.fun 的交易,并通过增强字段输出手续费支付者和计算单元价格。grpcurl -plaintext -import-path . -proto jetstream_v2.proto \
-d '{"add_filters":{"filters":[{"filter_id":"pump","filter":{"account_include":["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],"include_enrichment":true}}]}}' \
fra.jetstream.orbitflare.com:80 jetstream.v2.JetstreamV2/SubscribeTransactions
import bs58 from 'bs58';
import { JetstreamClientBuilder, TransactionFilter } from '@orbitflare/sdk/jetstream/v2';
const PUMP_FUN = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P';
// In API Key Only mode the SDK sends ORBITFLARE_LICENSE_KEY as x-token.
const jetstream = new JetstreamClientBuilder().url('http://fra.jetstream.orbitflare.com').build();
// Matches every transaction that mentions pump.fun. Enrichment adds the fee payer and CU price.
const filter = new TransactionFilter().accountInclude([PUMP_FUN]).includeEnrichment(true).withId('pump');
for await (const update of jetstream.subscribeTransactions([filter])) {
// Other updates are filter validations, heartbeats and pongs.
const tx = update.transaction?.transaction;
if (!tx) continue;
// compute_unit_price is micro-lamports per CU, not the total priority fee.
console.log(
`${tx.slot} ${bs58.encode(tx.signature)} payer=${bs58.encode(tx.feePayer)} cu_price=${tx.computeUnitPrice}`,
);
}
use orbitflare_sdk::jetstream::v2::{JetstreamClientBuilder, TransactionFilter};
use orbitflare_sdk::proto::jetstream::v2::subscribe_transactions_response::Payload;
const PUMP_FUN: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
#[tokio::main]
async fn main() -> orbitflare_sdk::Result<()> {
// In API Key Only mode the SDK sends ORBITFLARE_LICENSE_KEY as x-token.
let jetstream = JetstreamClientBuilder::new()
.url("http://fra.jetstream.orbitflare.com")
.build()?;
// Matches every transaction that mentions pump.fun. Enrichment adds the fee payer and CU price.
let filter = TransactionFilter::new()
.account_include([PUMP_FUN])
.include_enrichment(true)
.with_id("pump");
let mut stream = jetstream.subscribe_transactions(vec![filter]);
while let Some(update) = stream.next().await {
// Other updates are filter validations, heartbeats and pongs.
if let Some(Payload::Transaction(ft)) = update?.payload
&& let Some(tx) = ft.transaction
{
// compute_unit_price is micro-lamports per CU, not the total priority fee.
println!(
"{} {} payer={} cu_price={}",
tx.slot,
bs58::encode(&tx.signature).into_string(),
bs58::encode(&tx.fee_payer).into_string(),
tx.compute_unit_price
);
}
}
Ok(())
}
package main
import (
"context"
"fmt"
"log"
"github.com/mr-tron/base58"
jetstreamv2 "github.com/orbitflare/orbitflare-sdk-go/jetstream/v2"
)
const pumpFun = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
func main() {
// In API Key Only mode the SDK sends ORBITFLARE_LICENSE_KEY as x-token.
client, err := jetstreamv2.NewBuilder().URL("http://fra.jetstream.orbitflare.com").Build()
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Matches every transaction that mentions pump.fun. Enrichment adds the fee payer and CU price.
filter := jetstreamv2.NewTransactionFilter().
AccountInclude(pumpFun).
IncludeEnrichment(true).
WithID("pump")
stream := client.SubscribeTransactions(filter)
defer stream.Close()
for {
update, err := stream.Next(context.Background())
if err != nil {
log.Fatal(err)
}
// Other updates are filter validations, heartbeats and pongs.
tx := update.GetTransaction().GetTransaction()
if tx == nil {
continue
}
// compute_unit_price is micro-lamports per CU, not the total priority fee.
fmt.Printf("%d %s payer=%s cu_price=%d\n",
tx.GetSlot(),
base58.Encode(tx.GetSignature()),
base58.Encode(tx.GetFeePayer()),
tx.GetComputeUnitPrice())
}
}
grpcurl 版本需要当前文件夹中有来自 SDK 仓库的 jetstream_v2.proto。Jetstream 只过滤交易。如需监听账户状态,请使用 Yellowstone。
读取钱包的最近交易
使用getTransactionsForAddress 返回某个地址最近的 10 笔交易。
curl -s https://fra.rpc.orbitflare.com \
-H "x-api-key: $ORBITFLARE_LICENSE_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getTransactionsForAddress","params":["WALLET",{"limit":10}]}'
import { RpcClientBuilder } from '@orbitflare/sdk';
type Entry = { signature: string; slot: number; blockTime: number; err: unknown };
const wallet = process.argv[2];
const rpc = new RpcClientBuilder().url('https://fra.rpc.orbitflare.com').build();
// Returns signatures by default. Ask for "full" with maxSupportedTransactionVersion 1 to get bodies.
const { data } = await rpc.getTransactionsForAddress(wallet, { limit: 10 });
// Newest first. Pass paginationToken from the response to get the next page.
for (const tx of data as Entry[]) {
const when = new Date(tx.blockTime * 1000).toISOString();
console.log(when, tx.slot, tx.signature, tx.err ? 'failed' : 'ok');
}
use orbitflare_sdk::{GetTransactionsOptions, RpcClientBuilder};
#[tokio::main]
async fn main() -> orbitflare_sdk::Result<()> {
let wallet = std::env::args().nth(1).expect("usage: history <WALLET>");
let rpc = RpcClientBuilder::new()
.url("https://fra.rpc.orbitflare.com")
.build()?;
let options = GetTransactionsOptions {
limit: Some(10),
..Default::default()
};
// Returns signatures by default. Ask for "full" with maxSupportedTransactionVersion 1 to get bodies.
let page = rpc.get_transactions_for_address(&wallet, options).await?;
// Newest first. Pass paginationToken from the response to get the next page.
for tx in page.data {
let status = if tx["err"].is_null() { "ok" } else { "failed" };
println!(
"{} {} {}",
tx["slot"],
tx["signature"].as_str().unwrap_or(""),
status
);
}
Ok(())
}
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/orbitflare/orbitflare-sdk-go/rpc"
)
type entry struct {
Signature string `json:"signature"`
Slot uint64 `json:"slot"`
Err json.RawMessage `json:"err"`
}
func main() {
wallet := os.Args[1]
client, err := rpc.NewBuilder().URL("https://fra.rpc.orbitflare.com").Build()
if err != nil {
log.Fatal(err)
}
limit := uint32(10)
// Returns signatures by default. Ask for "full" with maxSupportedTransactionVersion 1 to get bodies.
page, err := client.GetTransactionsForAddress(context.Background(), wallet, &rpc.GetTransactionsOptions{Limit: &limit})
if err != nil {
log.Fatal(err)
}
// Newest first. Pass paginationToken from the response to get the next page.
for _, raw := range page.Data {
var tx entry
if err := json.Unmarshal(raw, &tx); err != nil {
log.Fatal(err)
}
status := "ok"
if string(tx.Err) != "null" {
status = "failed"
}
fmt.Println(tx.Slot, tx.Signature, status)
}
}
signature、slot、blockTime、err 和 confirmationStatus。传入返回的 paginationToken 即可获取下一页。如需完整交易,请将 transactionDetails 设为 "full",并同时设置 "maxSupportedTransactionVersion": 1。缺少后者时,版本 1 交易会被悄悄略过。过滤和排序选项请参阅 getTransactionsForAddress。
更多示例
错误参考
OrbitFlare 返回的每一种错误以及相应的处理方法。
交易调试
交易为何失败、被丢弃或始终没有上链。
通过 Apex 发送
使用 OrbitFlare Apex 让交易更快上链。
SDK 参考
SDK 中的每个客户端、构建器选项和方法。