> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitflare.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Отправка транзакций через Apex

> Отправляйте транзакции Solana в OrbitFlare Apex через JSON-RPC sendTransaction, бинарные HTTP-маршруты /send-bin и /send-batch или постоянные QUIC-потоки.

## Выберите транспорт

|                     | JSON-RPC                    | `POST /send`            | `POST /send-bin`                    | `POST /send-batch`              | QUIC uni             | QUIC bidi                         |
| ------------------- | --------------------------- | ----------------------- | ----------------------------------- | ------------------------------- | -------------------- | --------------------------------- |
| **Тело**            | base64 в JSON               | base64 в JSON           | сырые байты                         | до 16 сырых фреймов             | один пакет на поток  | один пакет на поток               |
| **Ответ**           | подпись или ошибка JSON-RPC | подпись или ошибка JSON | подпись или ошибка JSON             | результаты по каждому фрейму    | нет                  | код допуска                       |
| **Лучше всего для** | замены в существующем коде  | простых JSON-клиентов   | минимальных накладных расходов HTTP | многих отправок в одном запросе | самого быстрого пути | причины отклонения сразу в ответе |

Все транспорты применяют одно и то же [правило чаевых](/ru/apex/tips), одну и ту же валидацию и одну и ту же маршрутизацию. Транспорт меняет только то, как байты попадают на эндпоинт Apex.

Ограничения на размер транзакции везде одинаковы: транзакции legacy и v0 до 1232 байт, [транзакции v1](/ru/apex/transaction-v1) до 4096 байт.

## Соберите подписанную транзакцию с чаевыми

Примеры на этой странице используют один общий хелпер для каждого языка. Он собирает memo-транзакцию с вычислительным бюджетом и чаевыми, подписывает её и умеет подтверждать подпись. Запустите его напрямую, чтобы вывести подписанную транзакцию в base64: именно её используют примеры с cURL.

Сначала задайте переменные окружения:

```bash theme={null}
export APEX_API_KEY="YOUR_API_KEY"
export APEX_RPC="http://fra.apex.orbitflare.com"
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
export KEYPAIR_PATH="payer.json"
```

<CodeGroup>
  ```javascript apex-tx.mjs theme={null}
  // npm install @solana/web3.js
  // Print a signed, tipped transaction as base64: node apex-tx.mjs
  import {
    ComputeBudgetProgram, Connection, Keypair, PublicKey, SystemProgram,
    TransactionInstruction, TransactionMessage, VersionedTransaction,
  } from "@solana/web3.js";
  import { readFileSync } from "node:fs";
  import { fileURLToPath } from "node:url";

  export const APEX_RPC = process.env.APEX_RPC ?? "http://fra.apex.orbitflare.com";
  export 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");

  export const payer = Keypair.fromSecretKey(
    Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? "payer.json", "utf8"))),
  );
  export const connection = new Connection(SOLANA_RPC, "confirmed");

  export 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;
  }

  let tipAccounts;
  let counter = 0;

  // Returns a signed VersionedTransaction. Pass { tip: false } for a bundle member without a tip.
  export async function buildTx({ tip = true, label = "apex" } = {}) {
    tipAccounts ??= await apexRpc("getTipAccounts", []);
    const tipAccount = new PublicKey(tipAccounts[Math.floor(Math.random() * tipAccounts.length)]);
    const { blockhash } = await connection.getLatestBlockhash("confirmed");
    const 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(`${label} ${Date.now()} ${counter++}`),
      }),
    ];
    if (tip) {
      instructions.push(
        SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: tipAccount, lamports: TIP_LAMPORTS }),
      );
    }
    const message = new TransactionMessage({
      payerKey: payer.publicKey,
      recentBlockhash: blockhash,
      instructions,
    }).compileToV0Message();
    const tx = new VersionedTransaction(message);
    tx.sign([payer]);
    return tx;
  }

  // Poll getSignatureStatuses on the Solana RPC. Returns the slot, or null on timeout.
  export async function confirm(signature, timeoutMs = 30_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const { value } = await connection.getSignatureStatuses([signature]);
      const status = value[0];
      if (status?.err) throw new Error(`failed on chain: ${JSON.stringify(status.err)}`);
      if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
        return status.slot;
      }
      await new Promise((resolve) => setTimeout(resolve, 500));
    }
    return null;
  }

  if (process.argv[1] === fileURLToPath(import.meta.url)) {
    const tx = await buildTx();
    console.log(Buffer.from(tx.serialize()).toString("base64"));
  }
  ```

  ```python apex_tx.py theme={null}
  # pip install solders requests
  # Print a signed, tipped transaction as base64: python apex_tx.py
  import base64
  import itertools
  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.get("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")

  # One session keeps the HTTP connection to the Apex endpoint open between sends.
  session = requests.Session()
  session.headers.update({"x-api-key": API_KEY})

  with open(os.environ.get("KEYPAIR_PATH", "payer.json")) as f:
      payer = Keypair.from_bytes(bytes(json.load(f)))

  _tip_accounts = None
  _counter = itertools.count()


  def apex_rpc(method, params):
      r = session.post(APEX_RPC, 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 build_tx(tip=True, label="apex"):
      """Returns a signed Transaction. Pass tip=False for a bundle member without a tip."""
      global _tip_accounts
      if _tip_accounts is None:
          _tip_accounts = apex_rpc("getTipAccounts", [])
      tip_account = Pubkey.from_string(random.choice(_tip_accounts))
      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"{label} {time.time_ns()} {next(_counter)}".encode(),
                      [AccountMeta(payer.pubkey(), is_signer=True, is_writable=True)]),
      ]
      if tip:
          instructions.append(
              transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=tip_account, lamports=TIP_LAMPORTS)))
      return Transaction([payer], Message.new_with_blockhash(instructions, payer.pubkey(), blockhash), blockhash)


  def confirm(signature, timeout_s=30.0):
      """Poll getSignatureStatuses on the Solana RPC. Returns the slot, or None on timeout."""
      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


  if __name__ == "__main__":
      print(base64.b64encode(bytes(build_tx())).decode())
  ```
</CodeGroup>

Для примеров с cURL подписывайте свежую транзакцию перед каждой отправкой:

```bash theme={null}
TX_BASE64=$(node apex-tx.mjs)      # or: TX_BASE64=$(python apex_tx.py)
```

Транзакция действительна, только пока действителен её blockhash, примерно от 60 до 90 секунд, поэтому подписывайте непосредственно перед отправкой.

## JSON-RPC sendTransaction

Отправьте `POST` в корень эндпоинта Apex. Это стандартный для Solana формат `sendTransaction`, поэтому существующий код переносится заменой URL и добавлением API-ключа.

| Параметр               | Тип                     | Описание                                                                                                   |
| ---------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `params[0]`            | string                  | Подписанная транзакция в кодировке base64                                                                  |
| `params[1].encoding`   | string                  | `"base64"`                                                                                                 |
| `params[1].maxRetries` | number, необязательный  | Бюджет повторов для этой транзакции. Не указывайте его, чтобы использовать значение эндпоинта по умолчанию |
| `params[2]`            | boolean, необязательный | `mevProtect`. `true` пропускает лидеров из блок-листа Shield. См. [Защита от MEV](/ru/apex/mev-protection) |

<CodeGroup>
  ```bash cURL theme={null}
  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\",\"maxRetries\":30},false]}"
  ```

  ```javascript JavaScript theme={null}
  import { apexRpc, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex json-rpc" });
  const signature = await apexRpc("sendTransaction", [
    Buffer.from(tx.serialize()).toString("base64"),
    { encoding: "base64", maxRetries: 30 },
    false, // mevProtect
  ]);
  console.log("accepted:", signature);
  console.log("landed in slot", await confirm(signature));
  ```

  ```python Python theme={null}
  import base64

  from apex_tx import apex_rpc, build_tx, confirm

  tx = build_tx(label="apex json-rpc")
  signature = apex_rpc("sendTransaction", [
      base64.b64encode(bytes(tx)).decode(),
      {"encoding": "base64", "maxRetries": 30},
      False,  # mevProtect
  ])
  print("accepted:", signature)
  print("landed in slot", confirm(signature))
  ```

  ```rust Rust theme={null}
  // `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
  use apex_sender_client::rpc::RpcClient;
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wire = serialize_transaction(&tx)?;
  // mev_protect = false, max_retries = Some(30)
  let signature = rpc.send_transaction(&wire, false, Some(30)).await?;
  println!("accepted: {signature}");
  ```
</CodeGroup>

Ответ:

```json theme={null}
{ "jsonrpc": "2.0", "id": 1, "result": "<signature>" }
```

Apex не выполняет симуляцию и preflight-проверки. `skipPreflight` и `preflightCommitment` ни на что не влияют. Если вам нужна симуляция, вызовите `simulateTransaction` на своём обычном RPC перед отправкой.

## Простые HTTP-маршруты

Эти маршруты находятся рядом с JSON-RPC на том же хосте и порту и обходятся без обёртки JSON-RPC.

| Маршрут             | Тело запроса                                                                                 | Ответ при успехе                                |
| ------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `POST /send`        | JSON: `{"transaction": "<base64>", "mevProtect"?: bool, "maxRetries"?: n}`                   | `{"signature": "..."}`                          |
| `POST /send-bin`    | Сырые байты транзакции, `Content-Type: application/octet-stream`                             | `{"signature": "..."}`                          |
| `POST /send-batch`  | До 16 фреймов, каждый состоит из длины u16 в big-endian и следующих за ней байтов транзакции | `attempted`, `accepted`, `rejected` и `results` |
| `POST /send-bundle` | От 1 до 4 фреймов, тот же формат фреймов. См. [Бандлы](/ru/apex/bundles)                     | `{"bundle_id": "...", "signatures": [...]}`     |
| `GET /ping`         | Нет, и API-ключ не нужен                                                                     | `pong`                                          |

На бинарных маршрутах параметры передаются флагами запроса: `?mev_protect=1` и `&max_retries=N`.

Ошибки возвращаются в JSON с HTTP-статусом: `{"error": "<label>", "message": "..."}`. См. [Ошибки и лимиты запросов](/ru/apex/errors-and-rate-limits#ошибки-простых-http-маршрутов).

### POST /send

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "$APEX_RPC/send" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"transaction\":\"$TX_BASE64\",\"mevProtect\":false,\"maxRetries\":30}"
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex send" });
  const res = await fetch(`${APEX_RPC}/send`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
    body: JSON.stringify({ transaction: Buffer.from(tx.serialize()).toString("base64"), mevProtect: false }),
  });
  const body = await res.json();
  if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("accepted:", body.signature);
  console.log("landed in slot", await confirm(body.signature));
  ```

  ```python Python theme={null}
  import base64

  from apex_tx import APEX_RPC, build_tx, confirm, session

  tx = build_tx(label="apex send")
  r = session.post(f"{APEX_RPC}/send",
                   json={"transaction": base64.b64encode(bytes(tx)).decode(), "mevProtect": False}, timeout=5)
  body = r.json()
  if "signature" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("accepted:", body["signature"])
  print("landed in slot", confirm(body["signature"]))
  ```
</CodeGroup>

### POST /send-bin

Самый дешёвый HTTP-путь: на входе нет ни base64, ни JSON. Тело запроса содержит сериализованную транзакцию, байт в байт.

<CodeGroup>
  ```bash cURL theme={null}
  echo "$TX_BASE64" | base64 --decode | curl -s "$APEX_RPC/send-bin?mev_protect=0" \
    -H "Content-Type: application/octet-stream" \
    -H "x-api-key: $APEX_API_KEY" \
    --data-binary @-
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const tx = await buildTx({ label: "apex send-bin" });
  const res = await fetch(`${APEX_RPC}/send-bin`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: tx.serialize(),
  });
  const body = await res.json();
  if (!body.signature) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("accepted:", body.signature);
  console.log("landed in slot", await confirm(body.signature));
  ```

  ```python Python theme={null}
  from apex_tx import APEX_RPC, build_tx, confirm, session

  tx = build_tx(label="apex send-bin")
  r = session.post(f"{APEX_RPC}/send-bin", headers={"Content-Type": "application/octet-stream"},
                   data=bytes(tx), timeout=5)
  body = r.json()
  if "signature" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("accepted:", body["signature"])
  print("landed in slot", confirm(body["signature"]))
  ```

  ```rust Rust theme={null}
  // `tx` is a signed, tipped VersionedTransaction, built as in the Quickstart.
  use apex_sender_client::rpc::RpcClient;
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wire = serialize_transaction(&tx)?;
  let signature = rpc.send_transaction_binary(&wire, false, None).await?;
  println!("accepted: {signature}");
  ```
</CodeGroup>

### POST /send-batch

Отправьте до 16 независимых транзакций в одном запросе. Тело представляет собой последовательность фреймов:

```text theme={null}
u16 big-endian   length of the transaction bytes
bytes            the serialized transaction
... repeated, up to 16 frames
```

Каждая транзакция принимается отдельно. Пакет **не атомарен**: одни фреймы могут быть приняты, а другие отклонены. Для исполнения по принципу «всё или ничего» используйте [Бандлы](/ru/apex/bundles).

<CodeGroup>
  ```bash cURL theme={null}
  : > batch.bin
  for i in 1 2 3; do
    node apex-tx.mjs | base64 --decode > tx.bin
    printf '%04x' $(($(wc -c < tx.bin))) | xxd -r -p >> batch.bin   # u16 big-endian length
    cat tx.bin >> batch.bin
  done

  curl -s "$APEX_RPC/send-batch" \
    -H "Content-Type: application/octet-stream" \
    -H "x-api-key: $APEX_API_KEY" \
    --data-binary @batch.bin
  ```

  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx, confirm } from "./apex-tx.mjs";

  const txs = [];
  for (let i = 0; i < 3; i++) txs.push(await buildTx({ label: `apex batch ${i}` }));

  // Each frame: u16 big-endian length, then the transaction bytes.
  const frames = txs.map((tx) => {
    const wire = Buffer.from(tx.serialize());
    const length = Buffer.alloc(2);
    length.writeUInt16BE(wire.length);
    return Buffer.concat([length, wire]);
  });

  const res = await fetch(`${APEX_RPC}/send-batch`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: Buffer.concat(frames),
  });
  const body = await res.json();
  if (!body.results) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log(`accepted ${body.accepted} of ${body.attempted}`);
  for (const result of body.results) {
    if (result.signature) console.log(result.signature, "landed in slot", await confirm(result.signature));
    else console.log("rejected:", result.error, result.message);
  }
  ```

  ```python Python theme={null}
  import struct

  from apex_tx import APEX_RPC, build_tx, confirm, session

  txs = [bytes(build_tx(label=f"apex batch {i}")) for i in range(3)]
  # Each frame: u16 big-endian length, then the transaction bytes.
  payload = b"".join(struct.pack(">H", len(wire)) + wire for wire in txs)

  r = session.post(f"{APEX_RPC}/send-batch", headers={"Content-Type": "application/octet-stream"},
                   data=payload, timeout=5)
  body = r.json()
  if "results" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print(f"accepted {body['accepted']} of {body['attempted']}")
  for result in body["results"]:
      if "signature" in result:
          print(result["signature"], "landed in slot", confirm(result["signature"]))
      else:
          print("rejected:", result["error"], result["message"])
  ```

  ```rust Rust theme={null}
  // `txs` is a Vec of signed, tipped VersionedTransaction values.
  use apex_sender_client::rpc::{BatchItem, RpcClient};
  use apex_sender_client::{serialize_transaction, Region};

  let rpc = RpcClient::new(Region::Frankfurt, &api_key);
  let wires: Vec<Vec<u8>> = txs.iter().map(serialize_transaction).collect::<Result<_, _>>()?;
  let refs: Vec<&[u8]> = wires.iter().map(Vec::as_slice).collect();

  let batch = rpc.send_batch(&refs, false, None).await?;
  println!("accepted {} of {}", batch.accepted, batch.attempted);
  for item in &batch.results {
      match item {
          BatchItem::Accepted(signature) => println!("accepted: {signature}"),
          BatchItem::Rejected { error, message } => println!("rejected: {error}: {message}"),
      }
  }
  ```
</CodeGroup>

Если сам запрос корректен, ответ всегда приходит с HTTP 200 и содержит по одному результату на фрейм в порядке фреймов:

```json theme={null}
{
  "attempted": 3,
  "accepted": 2,
  "rejected": 1,
  "results": [
    { "signature": "<signature 1>" },
    { "signature": "<signature 2>" },
    { "error": "below_floor", "message": "..." }
  ]
}
```

### GET /ping

Возвращает `pong`. API-ключ не нужен. Используйте его, чтобы заранее открыть и прогреть HTTP-соединение и чтобы поддерживать простаивающее соединение. См. [Лучшие практики](/ru/apex/best-practices#держите-соединения-прогретыми).

```bash theme={null}
curl -s "$APEX_RPC/ping"
```

### CORS

Каждый HTTP-ответ содержит `Access-Control-Allow-Origin: *`, а на предварительные запросы `OPTIONS` даётся ответ, поэтому все перечисленные выше маршруты работают из браузерного кода. Прежде чем передавать ключ во фронтенд, прочитайте [замечания о браузерах](/ru/apex/authentication#браузеры).

## QUIC

QUIC является самым быстрым способом отправки. Вы держите одно постоянное соединение с эндпоинтом Apex, которое один раз аутентифицируется [клиентским сертификатом, производным от вашего API-ключа](/ru/apex/authentication#quic-клиентский-сертификат), и открываете один поток на каждую транзакцию. В прогретом соединении отправка сводится к одному открытию потока и одной записи, то есть к нескольким микросекундам времени клиента.

Есть два вида потоков:

| Поток                | Что вы получаете в ответ                                      | Для чего использовать                                                   |
| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------- |
| **Однонаправленный** | Ничего. Отправили и забыли                                    | Боевые отправки по самому быстрому пути                                 |
| **Двунаправленный**  | Один ответ о допуске: принято либо код и сообщение отклонения | Интеграция, отладка и инструменты, которым нужна причина сразу в ответе |

### Rust

Крейт [`apex-sender-client`](/ru/apex/rust-client) реализует весь транспорт: сертификат, keep-alive, возобновление 0-RTT и переподключения. Эта программа отправляет по одной транзакции в потоке каждого вида. Она использует тот же `Cargo.toml`, что и [Быстрый старт](/ru/apex/quickstart#зависимости-rust).

```rust theme={null}
use std::str::FromStr;
use std::time::Duration;

use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{tip, tip_instruction, ApexSenderClient, Error, Region, MIN_TIP_LAMPORTS};
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::{read_keypair_file, Keypair};
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
use solana_transaction::Transaction;

async fn tipped_memo(
    payer: &Keypair,
    tip_accounts: &[Pubkey],
    solana: &SolanaRpc,
    label: &str,
) -> Result<VersionedTransaction, Box<dyn std::error::Error>> {
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)?
        .as_nanos();
    let tip_account = tip::pick_tip_account(tip_accounts).ok_or("no tip accounts")?;
    let instructions = [
        ComputeBudgetInstruction::set_compute_unit_limit(100_000),
        ComputeBudgetInstruction::set_compute_unit_price(10_000),
        Instruction::new_with_bytes(
            Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")?,
            format!("{label} {nonce}").as_bytes(),
            vec![AccountMeta::new(payer.pubkey(), true)],
        ),
        tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
    ];
    let blockhash = solana.latest_blockhash().await?;
    Ok(VersionedTransaction::from(Transaction::new_signed_with_payer(
        &instructions,
        Some(&payer.pubkey()),
        &[payer],
        blockhash,
    )))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("APEX_API_KEY")?;
    let solana = SolanaRpc::new(
        std::env::var("SOLANA_RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into()),
    );
    let payer = read_keypair_file(std::env::var("KEYPAIR_PATH").unwrap_or_else(|_| "payer.json".into()))?;
    let region = Region::Frankfurt;

    let client = ApexSenderClient::connect(region, &api_key).await?;
    let tip_accounts = RpcClient::new(region, &api_key).get_tip_accounts().await?;

    // Unidirectional stream: fire and forget. Returns the signature, reads nothing back.
    let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic uni").await?;
    let signature = client.send_transaction(&tx).await?;
    println!("sent: {signature}");
    println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);

    // Bidirectional stream: the endpoint answers accepted or rejected before racing it.
    let tx = tipped_memo(&payer, &tip_accounts, &solana, "apex quic bidi").await?;
    match client.send_transaction_with_response(&tx).await {
        Ok(signature) => {
            println!("accepted: {signature}");
            println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);
        }
        // Fix the transaction, not the client. The message names the floor when the tip is short.
        Err(Error::Rejected { code, message }) => println!("rejected: {code:?}: {message}"),
        Err(e) => return Err(e.into()),
    }
    Ok(())
}
```

### Коды допуска

Двунаправленный поток отвечает одним из этих кодов:

| Код | Имя              | Значение                                                                           |
| --- | ---------------- | ---------------------------------------------------------------------------------- |
| `0` | ok               | Принято. Транзакция отслеживается и отправляется лидерам наперегонки               |
| `1` | unauthorized     | Ключ соединения недействителен                                                     |
| `2` | rate limited     | Ваш ключ превысил свой лимит в секунду                                             |
| `3` | invalid          | Транзакция не прошла проверку корректности. В сообщении указано, что именно не так |
| `4` | no tip           | Перевод на аккаунт для чаевых Apex не найден                                       |
| `5` | below floor      | Чаевые ниже минимума вашего тарифа. В сообщении указан минимум                     |
| `6` | busy             | Эндпоинт сбрасывает нагрузку. Повторите чуть позже                                 |
| `7` | malformed packet | Неверный формат фрейма пакета                                                      |

Отклонение окончательно для этого пакета. Транспортную ошибку до получения ответа можно безопасно повторять, потому что эндпоинт отсеивает дубликаты по подписи.

### QUIC из других языков

Вы можете реализовать транспорт на любом языке, для которого есть библиотека QUIC с поддержкой клиентских сертификатов.

**Соединение**

* QUIC (RFC 9000) с TLS 1.3 и ALPN `solana-tpu`.
* Сертификат сервера представляет собой самоподписанную заглушку. Не проверяйте его.
* Клиент должен предъявить [сертификат, производный от вашего API-ключа](/ru/apex/authentication#quic-клиентский-сертификат).
* Держите одно открытое соединение на каждый эндпоинт Apex. Регулярно отправляйте QUIC PING (клиент на Rust отправляет пинг каждую секунду). Тайм-аут простоя эндпоинта составляет 30 секунд. 0-RTT включён.

**Пакет транзакции**

Откройте поток, запишите ровно один пакет и завершите поток:

```text theme={null}
u64 LE   length of the transaction bytes
bytes    the serialized transaction: legacy or v0 at most 1232 bytes, v1 at most 4096
u8       mev_protect: 1 to skip Shield-blocklisted leaders, else 0
u8       max_retries present: 0 or 1
u16 LE   max_retries, only if present
```

Это в точности bincode от `{ wire_transaction: Vec<u8>, mev_protect: bool, max_retry: Option<u16> }` со стандартными параметрами bincode: целые фиксированной длины в little-endian. Весь пакет должен быть не больше 4160 байт.

**Фрейм допуска** (только для двунаправленных потоков)

```text theme={null}
u8       status: one of the admission codes above
[64]     status 0 only: the transaction's first signature
u16 LE   status != 0: message length
bytes    status != 0: UTF-8 message
```

В Rust эталонной реализацией обоих фреймов служат `wire::encode_packet` и `wire::decode_admission`.
