> ## 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 на TypeScript

> Отправляйте транзакции, пакеты и бандлы через OrbitFlare Apex из TypeScript с помощью ApexClient в @orbitflare/sdk. Только HTTP JSON-RPC, с резервированием по регионам и хелперами для чаевых.

> [!NOTE]
>
> Клиент Apex на TypeScript входит в [`@orbitflare/sdk`](/ru/sdk/overview) и не является отдельным пакетом. Он работает с Apex **только по HTTP JSON-RPC**. Транспорта QUIC для TypeScript пока нет, потому что в Node нет реализации QUIC, достаточно надёжной для отправки. Для пути с наименьшей задержкой по QUIC используйте [клиент на Rust](/ru/apex/rust-client) или [клиент на Go](/ru/apex/go-client).

## Установка

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
npm install @orbitflare/sdk @solana/kit @solana-program/system @solana-program/compute-budget
```

`@solana/kit` и два пакета программ собирают и подписывают транзакцию. Самому клиенту Apex нужен только `@orbitflare/sdk`.

Задайте свой API-ключ Apex, keypair с балансом и эндпоинт Solana RPC для blockhash и подтверждения. В примере используется OrbitFlare RPC с вашим лицензионным ключом:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
export ORBITFLARE_APEX_API_KEY=apex_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ORBITFLARE_LICENSE_KEY=ORBIT-XXXXXX-NNNNNN-NNNNNN
export KEYPAIR_PATH=payer.json
export SOLANA_RPC_URL="https://fra.rpc.orbitflare.com?api_key=$ORBITFLARE_LICENSE_KEY"
```

## Быстрый старт

Эта программа отправляет 1,000 lamports на выбранный вами адрес (по умолчанию на свой собственный) с минимальными чаевыми, а затем ждёт, пока транзакция попадёт в блок. Сохраните её как `send.ts` и запустите командой `npx tsx send.ts`.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
import { readFileSync } from 'node:fs';

import {
  address,
  appendTransactionMessageInstructions,
  createKeyPairSignerFromBytes,
  createTransactionMessage,
  getSignatureFromTransaction,
  getTransactionEncoder,
  lamports,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
  type Blockhash,
} from '@solana/kit';
import {
  getSetComputeUnitLimitInstruction,
  getSetComputeUnitPriceInstruction,
} from '@solana-program/compute-budget';
import { getTransferSolInstruction } from '@solana-program/system';

import {
  APEX_DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
  APEX_MIN_TIP_LAMPORTS,
  ApexClientBuilder,
  RpcClientBuilder,
  pickApexTipAccount,
  waitForConfirmation,
} from '@orbitflare/sdk';

async function main() {
  // Reads ORBITFLARE_APEX_API_KEY; sends over HTTP to Frankfurt.
  const apex = new ApexClientBuilder().region('fra').build();
  const solana = new RpcClientBuilder()
    .url(process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com')
    .build();

  const keypair = JSON.parse(readFileSync(process.env.KEYPAIR_PATH ?? 'payer.json', 'utf8'));
  const payer = await createKeyPairSignerFromBytes(Uint8Array.from(keypair));
  const to = address(process.env.TO ?? payer.address);

  // A tip to any other account is rejected, so pick from the published list.
  const tipAccount = pickApexTipAccount(await apex.getTipAccounts());
  const { blockhash, lastValidBlockHeight } = await solana.getLatestBlockhash();

  const message = pipe(
    createTransactionMessage({ version: 'legacy' }),
    (m) => setTransactionMessageFeePayerSigner(payer, m),
    (m) =>
      setTransactionMessageLifetimeUsingBlockhash(
        { blockhash: blockhash as Blockhash, lastValidBlockHeight: BigInt(lastValidBlockHeight) },
        m,
      ),
    (m) =>
      appendTransactionMessageInstructions(
        [
          getSetComputeUnitLimitInstruction({ units: 50_000 }),
          getSetComputeUnitPriceInstruction({
            microLamports: APEX_DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
          }),
          getTransferSolInstruction({ source: payer, destination: to, amount: lamports(1_000n) }),
          // Exactly one top-level tip of at least 0.001 SOL.
          getTransferSolInstruction({
            source: payer,
            destination: address(tipAccount),
            amount: lamports(BigInt(APEX_MIN_TIP_LAMPORTS)),
          }),
        ],
        m,
      ),
  );
  const tx = await signTransactionMessageWithSigners(message);
  const wire = getTransactionEncoder().encode(tx);

  const sentAt = Date.now();
  const signature = await apex.sendTransactionBinary(wire);
  console.log(`accepted in ${Date.now() - sentAt} ms: ${signature}`);
  console.log(`matches the signed transaction: ${signature === getSignatureFromTransaction(tx)}`);

  // Accepted is not landed: confirm on a Solana RPC.
  const slot = await waitForConfirmation(solana, signature);
  console.log(
    slot === null
      ? 'not confirmed within 30 s'
      : `landed in slot ${slot} after ${Date.now() - sentAt} ms`,
  );
}

void main();
```

Фрагменты ниже используют `apex`, `solana` и байты подписанной транзакции (`wire`) из этой программы.

## Отправка

Одну транзакцию отправляют два метода. Оба возвращают подпись, как только эндпоинт её принимает.

| Метод | Маршрут | Тело |
| - | - | - |
| `sendTransactionBinary(bytes)` | `POST /send-bin` | Сырые байты транзакции. Самый быстрый путь по HTTP, ничего не нужно кодировать |
| `sendTransaction(bytesOrBase64)` | JSON-RPC `sendTransaction` | Base64 внутри запроса JSON-RPC. Принимает байты или строку base64 |

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const signature = await apex.sendTransaction(wire, { mevProtect: true, maxRetries: 10 });
```

Оба принимают необязательные `mevProtect` (пропускать лидеров из блок-листа Shield, см. [Защита от MEV](/ru/apex/mev-protection)) и `maxRetries` (ограничить, сколько раз Apex повторно отправляет транзакцию), которые переопределяют значения билдера по умолчанию для этого вызова. Транзакция должна занимать от 1 до 4,096 байт (`APEX_MAX_TRANSACTION_SIZE`).

## Пакеты

`sendBatch` отправляет до 16 транзакций (`APEX_MAX_BATCH`) в одном запросе. Каждая транзакция проверяется и отправляется наперегонки отдельно, поэтому одна плохая транзакция не останавливает остальные.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const batch = await apex.sendBatch([wire1, wire2, wire3]);
console.log(`${batch.accepted} of ${batch.attempted} accepted`);
for (const item of batch.results) {
  console.log(item.accepted ? item.signature : `${item.error}: ${item.message}`);
}
```

## Бандлы

`sendBundle` отправляет от 1 до 4 транзакций (`APEX_MAX_BUNDLE`), которые попадают в блок вместе или не попадают вовсе, только через лидеров Jito. Ровно одна транзакция в бандле несёт чаевые.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const { bundleId, signatures } = await apex.sendBundle([wire1, wire2]);
const [status] = await apex.getBundleStatuses([bundleId]);
console.log(status?.state, status?.landedSlot);
```

`state` принимает значения `Pending`, `Landed`, `Failed` или `Invalid`. Статусы бандлов могут отставать от чейна, поэтому подтверждайте `signatures` бандла через `waitForConfirmation`, а не ждите только статуса. Правила и лимиты запросов описаны в разделе [Бандлы](/ru/apex/bundles).

## Чаевые и подтверждение

| Хелпер | Что делает |
| - | - |
| `apex.getTipAccounts()` | Аккаунты для чаевых, которые принимает эндпоинт |
| `pickApexTipAccount(accounts)` | Один из них, выбранный случайным образом, чтобы распределить блокировки на запись |
| `APEX_MIN_TIP_LAMPORTS` | Минимум чаевых стандартного тарифа: 1,000,000 lamports (0.001 SOL) |
| `APEX_DEFAULT_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS` | Разумная приоритетная комиссия по умолчанию: 10,000 micro-lamports за вычислительную единицу |
| `fetchApexVaults(rpc)` | Читает vault-аккаунты программы чаевых из чейна. Это не список для чаевых: он включает неопубликованные vault-аккаунты, которые эндпоинт отклоняет. Используйте `getTipAccounts()` для аккаунтов, которым отправлять чаевые, а это - только чтобы проверить, что конкретный аккаунт является настоящим vault-аккаунтом |
| `waitForConfirmation(rpc, signature, { timeoutMs, pollMs })` | Опрашивает, пока транзакция не будет подтверждена. Возвращает слот, `null` по тайм-ауту (по умолчанию 30 s) и выбрасывает исключение, если транзакция попала в блок, но завершилась с ошибкой |

Правила чаевых описаны на странице [Чаевые](/ru/apex/tips).

## Регионы и резервирование

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
const apex = new ApexClientBuilder()
  .region('fra')
  .fallbackRegion('ams')
  .fallbackRegion('lon')
  .build();
```

Когда запрос к основному эндпоинту не удаётся, клиент по порядку пробует резервные. Коды регионов: `fra`, `ams`, `dub`, `lon`, `nyc`, `slc`, `sgp`, `tyo`, `sqq` и `global` (ближайший исправный эндпоинт). Если регион не задан, клиент использует `ORBITFLARE_APEX_URL`, затем `ORBITFLARE_APEX_REGION`, затем Франкфурт. См. [Эндпоинты](/ru/apex/endpoints).

## Параметры

| Метод билдера | По умолчанию | Что делает |
| - | - | - |
| `.apiKey(key)` | `ORBITFLARE_APEX_API_KEY` | Ваш API-ключ Apex, отправляется в заголовке `x-api-key` |
| `.region(code)` / `.url(url)` | Франкфурт | Основной эндпоинт |
| `.fallbackRegion(code)` / `.fallbackUrl(url)` | Нет | Эндпоинты, которые пробуются, когда основной не отвечает |
| `.mevProtect(enabled)` | `false` | Пропускать лидеров из блок-листа Shield при каждой отправке. См. [Защита от MEV](/ru/apex/mev-protection) |
| `.maxRetries(n)` | Значение Apex по умолчанию | Сколько раз Apex повторно отправляет каждую транзакцию |
| `.retry(policy)` | 1 попытка на эндпоинт | Повторы неудавшегося запроса на стороне клиента к тому же эндпоинту, с задержкой между попытками |
| `.timeoutMs(ms)` | `5000` | Тайм-аут одного запроса |

По умолчанию клиент делает одну попытку на эндпоинт и сразу переходит к следующему резервному вместо повторов. Это сделано намеренно: повторная отправка транзакции, которую Apex уже принял, расходует ваш лимит запросов, а Apex сам продолжает повторно отправлять принятые транзакции.

## Поддержание соединения прогретым

`apex.ping()` вызывает `GET /ping` и завершается, когда эндпоинт отвечает `pong`. Вызов раз в несколько секунд держит HTTP-соединение открытым, поэтому следующая отправка пропускает TCP-рукопожатие.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
setInterval(() => apex.ping().catch(() => {}), 5_000);
```

## Ошибки

Каждый сбой является `OrbitflareError` с полем `kind`:

| `kind` | Значение |
| - | - |
| `config` | Нет API-ключа, неизвестный регион, либо транзакция, пакет или бандл выходит за ограничения размера. Ничего не было отправлено |
| `rpc` | Apex ответил на вызов JSON-RPC ошибкой, например из-за недостаточных чаевых или невалидной транзакции. Код и сообщение находятся в ошибке |
| `apex` | Один из бинарных маршрутов (`/send-bin`, `/send-batch`, `/send-bundle`) отклонил запрос. HTTP-статус, метка и сообщение находятся в ошибке |
| `rateLimited` | Превышен посекундный лимит вашего ключа (HTTP 429). `retryAfterMs` говорит, сколько ждать, если Apex его передаёт |
| `timeout` | Нет ответа в течение `timeoutMs` |
| `transport` | Запрос не удался на всех опробованных эндпоинтах, или эндпоинт вернул ошибку сервера |

Коды ошибок и лимиты запросов Apex перечислены в разделе [Ошибки и лимиты запросов](/ru/apex/errors-and-rate-limits).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.