前提条件
您需要:- 一个 Apex API 密钥,可在 OrbitFlare 控制台的 Dashboard > Apex 下获取。Apex 目前处于测试阶段,仅限受邀用户使用。
- 一个有余额的 Solana 密钥对文件(由
solana-keygen写入的 JSON 数组格式),其中至少有 0.002 SOL 用于支付小费和费用。 - 任意 Solana RPC URL,用于获取 blockhash 和确认交易,例如您的 OrbitFlare RPC 端点。
export APEX_API_KEY="YOUR_API_KEY"
export APEX_RPC="http://fra.apex.orbitflare.com" # nearest Apex endpoint, see Endpoints and regions
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
export KEYPAIR_PATH="payer.json"
这些示例会在主网上发送一笔真实交易。如果交易上链,将支付 0.001 SOL 小费以及网络费用。如果未上链,则不产生任何费用。
发送您的第一笔交易
1
检查 Apex 端点是否可达
GET /ping 无需密钥,返回 pong:curl -s "$APEX_RPC/ping"
2
获取小费账户
curl -s "$APEX_RPC" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getTipAccounts","params":[]}'
3
构建、签名、发送并确认
每个标签页都是一个完整的程序。它构建一笔带有计算预算和小费的 memo 交易,通过 JSON-RPC
sendTransaction 发送,并等待确认。cURL 无法对交易签名,因此 cURL 标签页使用一个小型 Node.js 脚本来构建并签名交易,然后以 base64 形式输出。之后的所有步骤都是纯 cURL。# 1. Save this as sign.mjs, then: npm install @solana/web3.js
cat > sign.mjs <<'SCRIPT'
import {
ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram,
TransactionInstruction, TransactionMessage, VersionedTransaction,
} from "@solana/web3.js";
import { readFileSync } from "node:fs";
const payer = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? "payer.json", "utf8"))),
);
const connection = new Connection(process.env.SOLANA_RPC_URL, "confirmed");
const { blockhash } = await connection.getLatestBlockhash("confirmed");
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 100_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
new TransactionInstruction({
programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
data: Buffer.from(`apex curl ${Date.now()}`),
}),
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: new PublicKey(process.argv[2]),
lamports: 1_000_000,
}),
],
}).compileToV0Message();
const tx = new VersionedTransaction(message);
tx.sign([payer]);
console.log(Buffer.from(tx.serialize()).toString("base64"));
SCRIPT
# 2. Pick a tip account and sign a transaction that tips it.
TIP_ACCOUNT=$(curl -s "$APEX_RPC" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getTipAccounts","params":[]}' | jq -r '.result[0]')
TX_BASE64=$(node sign.mjs "$TIP_ACCOUNT")
# 3. Send it. params: [base64 transaction, config, mevProtect]
SIGNATURE=$(curl -s "$APEX_RPC" \
-H "Content-Type: application/json" \
-H "x-api-key: $APEX_API_KEY" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"sendTransaction\",\"params\":[\"$TX_BASE64\",{\"encoding\":\"base64\"},false]}" \
| jq -r '.result // .error')
echo "accepted: $SIGNATURE"
# 4. Confirm on any Solana RPC. Repeat until confirmationStatus is "confirmed".
curl -s "$SOLANA_RPC_URL" -H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getSignatureStatuses\",\"params\":[[\"$SIGNATURE\"]]}" | jq '.result.value[0]'
// send.mjs
// npm install @solana/web3.js
// node send.mjs
import {
ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram,
TransactionInstruction, TransactionMessage, VersionedTransaction,
} from "@solana/web3.js";
import { readFileSync } from "node:fs";
const APEX_RPC = process.env.APEX_RPC ?? "http://fra.apex.orbitflare.com";
const API_KEY = process.env.APEX_API_KEY;
const SOLANA_RPC = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
const TIP_LAMPORTS = Number(process.env.TIP_LAMPORTS ?? 1_000_000);
const MEMO_PROGRAM = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
async function apexRpc(method, params) {
const res = await fetch(APEX_RPC, {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
});
const body = await res.json();
if (body.error) throw new Error(`${method} failed ${body.error.code}: ${body.error.message}`);
return body.result;
}
if (!API_KEY) throw new Error("APEX_API_KEY is required");
const payer = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? "payer.json", "utf8"))),
);
const connection = new Connection(SOLANA_RPC, "confirmed");
const tipAccounts = await apexRpc("getTipAccounts", []);
const tipAccount = new PublicKey(tipAccounts[Math.floor(Math.random() * tipAccounts.length)]);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 100_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
new TransactionInstruction({
programId: MEMO_PROGRAM,
keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
data: Buffer.from(`apex javascript ${Date.now()}`),
}),
SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: tipAccount, lamports: TIP_LAMPORTS }),
],
}).compileToV0Message();
const tx = new VersionedTransaction(message);
tx.sign([payer]);
const started = Date.now();
// params: [base64 transaction, { encoding, maxRetries? }, mevProtect]
const signature = await apexRpc("sendTransaction", [
Buffer.from(tx.serialize()).toString("base64"),
{ encoding: "base64" },
false,
]);
console.log(`accepted in ${Date.now() - started} ms: ${signature}`);
const result = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed");
if (result.value.err) throw new Error(`failed on chain: ${JSON.stringify(result.value.err)}`);
console.log(`confirmed after ${Date.now() - started} ms`);
# send.py
# pip install solders requests
# python send.py
import base64
import json
import os
import random
import time
import requests
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.hash import Hash
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
APEX_RPC = os.environ.get("APEX_RPC", "http://fra.apex.orbitflare.com")
API_KEY = os.environ["APEX_API_KEY"]
SOLANA_RPC = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
TIP_LAMPORTS = int(os.environ.get("TIP_LAMPORTS", "1000000"))
MEMO_PROGRAM = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
def apex_rpc(method, params):
r = requests.post(APEX_RPC, headers={"x-api-key": API_KEY},
json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=5)
body = r.json()
if "error" in body:
raise RuntimeError(f"{method} failed {body['error']['code']}: {body['error']['message']}")
return body["result"]
def solana_rpc(method, params):
r = requests.post(SOLANA_RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=10)
return r.json()["result"]
def confirm(signature, timeout_s=30.0):
deadline = time.time() + timeout_s
while time.time() < deadline:
status = solana_rpc("getSignatureStatuses", [[signature]])["value"][0]
if status and status.get("err"):
raise RuntimeError(f"failed on chain: {status['err']}")
if status and status.get("confirmationStatus") in ("confirmed", "finalized"):
return status["slot"]
time.sleep(0.5)
return None
with open(os.environ.get("KEYPAIR_PATH", "payer.json")) as f:
payer = Keypair.from_bytes(bytes(json.load(f)))
tip_account = Pubkey.from_string(random.choice(apex_rpc("getTipAccounts", [])))
blockhash = Hash.from_string(
solana_rpc("getLatestBlockhash", [{"commitment": "confirmed"}])["value"]["blockhash"])
instructions = [
set_compute_unit_limit(100_000),
set_compute_unit_price(10_000),
Instruction(MEMO_PROGRAM, f"apex python {time.time_ns()}".encode(),
[AccountMeta(payer.pubkey(), is_signer=True, is_writable=True)]),
transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=tip_account, lamports=TIP_LAMPORTS)),
]
tx = Transaction([payer], Message.new_with_blockhash(instructions, payer.pubkey(), blockhash), blockhash)
started = time.perf_counter()
# params: [base64 transaction, {encoding, maxRetries?}, mevProtect]
signature = apex_rpc("sendTransaction", [base64.b64encode(bytes(tx)).decode(), {"encoding": "base64"}, False])
print(f"accepted in {(time.perf_counter() - started) * 1000:.1f} ms: {signature}")
print("landed in slot", confirm(signature))
// src/main.rs. Cargo.toml dependencies are listed under "Rust dependencies" below.
// cargo run --release
use std::str::FromStr;
use std::time::{Duration, Instant};
use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{tip, tip_instruction, ApexSenderClient, Region, MIN_TIP_LAMPORTS};
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::read_keypair_file;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction::Transaction;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("APEX_API_KEY")?;
let solana_rpc_url = std::env::var("SOLANA_RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into());
let keypair_path = std::env::var("KEYPAIR_PATH").unwrap_or_else(|_| "payer.json".into());
let payer = read_keypair_file(keypair_path)?;
let region = Region::Frankfurt;
// One persistent QUIC connection, authenticated by a certificate derived from the key.
let client = ApexSenderClient::connect(region, &api_key).await?;
let tip_accounts = RpcClient::new(region, &api_key).get_tip_accounts().await?;
let tip_account = tip::pick_tip_account(&tip_accounts).ok_or("no tip accounts")?;
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos();
let memo = Instruction::new_with_bytes(
Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")?,
format!("apex rust {nonce}").as_bytes(),
vec![AccountMeta::new(payer.pubkey(), true)],
);
let instructions = [
ComputeBudgetInstruction::set_compute_unit_limit(100_000),
ComputeBudgetInstruction::set_compute_unit_price(10_000),
memo,
tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
];
let solana = SolanaRpc::new(solana_rpc_url);
let blockhash = solana.latest_blockhash().await?;
let tx = VersionedTransaction::from(Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&[&payer],
blockhash,
));
let sent_at = Instant::now();
let signature = client.send_transaction(&tx).await?; // unidirectional stream, no acknowledgement
println!("sent in {} us", sent_at.elapsed().as_micros());
match solana.confirm(&signature.to_string(), Duration::from_secs(30)).await? {
Some(slot) => println!("landed in slot {slot} after {} ms: {signature}", sent_at.elapsed().as_millis()),
None => println!("not confirmed within 30 s: {signature}"),
}
Ok(())
}
sendTransaction 会返回交易的签名:
{
"jsonrpc": "2.0",
"id": 1,
"result": "5gwAmfVM..."
}
已接受并不代表已上链。 一旦 Apex 端点持有交易并开始竞速发送,签名就会立即返回。请务必像每个示例那样,在 Solana RPC 上使用
getSignatureStatuses 进行确认。Rust 依赖
Rust 标签页使用 QUIC 客户端,这是最快的路径。该 crate 即将以apex-sender-client 的名称发布到 crates.io。在此之前,请从 OrbitFlare 的 GitHub 仓库安装:
[dependencies]
apex-sender-client = { git = "https://github.com/orbitflare/apex-sender-client", features = ["rpc"] }
tokio = { version = "1", features = ["full"] }
solana-compute-budget-interface = "3"
solana-instruction = "3.4"
solana-keypair = "3.1"
solana-pubkey = "4.2"
solana-signer = "3"
solana-transaction = { version = "=4.1.6", features = ["wincode"] }
已经在调用 sendTransaction?
Apex 使用标准的 SolanasendTransaction 格式,因此迁移现有代码只需替换 URL,再加上两处改动:
- 将您的发送调用指向
http://<code>.apex.orbitflare.com,并添加x-api-key请求头(或?api-key=)。 - 为每笔交易添加小费指令。
下一步
发送交易
二进制 HTTP、批量和 QUIC 流。
小费
完整的小费规则以及小费的用途。
错误与速率限制
每一种拒绝代码及其处理方法。
最佳实践
连接预热、小费设置、确认和重试。