> ## 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.

# gRPC-клиент (Yellowstone)

> Стриминг Yellowstone Geyser с автоматическим переподключением, обнаружением живости через ping/pong и типизированными фильтрами.

## Установка

```bash theme={null}
go get github.com/orbitflare/orbitflare-sdk-go
```

```go theme={null}
import "github.com/orbitflare/orbitflare-sdk-go/geyser"
```

## Сборка клиента

Клиент со всеми заданными опциями:

```go theme={null}
import (
    "time"

    "github.com/orbitflare/orbitflare-sdk-go/geyser"
    "github.com/orbitflare/orbitflare-sdk-go/orbitflare"
)

client, err := geyser.NewBuilder().
    URL("http://ny.rpc.orbitflare.com:10000").
    FallbackURL("http://fra.rpc.orbitflare.com:10000").
    Retry(orbitflare.RetryPolicy{
        InitialDelay: 100 * time.Millisecond,
        MaxDelay:     30 * time.Second,
        Multiplier:   2.0,
        MaxAttempts:  0,
    }).
    Timeout(30 * time.Second).
    Keepalive(60 * time.Second).
    PingInterval(10 * time.Second).
    MaxMissedPongs(3).
    ChannelCapacity(4096).
    Build()
```

Минимальная настройка:

```go theme={null}
client, err := geyser.NewBuilder().
    URL("http://ny.rpc.orbitflare.com:10000").
    Build()
```

gRPC не требует API-ключа. URL берётся из переменной окружения `ORBITFLARE_GRPC_URL`.

### Методы билдера

**`.URL(url)`** - Основной gRPC-эндпоинт. Резерв: `ORBITFLARE_GRPC_URL`. Используйте `http://` для незашифрованного HTTP/2 и `https://` для TLS.

```go theme={null}
.URL("http://ny.rpc.orbitflare.com:10000")
```

**`.URLs(urls...)`** - Основной и резервные одним вызовом. Первый элемент - основной.

```go theme={null}
.URLs("http://ny.rpc.orbitflare.com:10000", "http://fra.rpc.orbitflare.com:10000")
```

**`.FallbackURL(url)` / `.FallbackURLs(urls...)`** - Добавить резервные эндпоинты. При сбое соединения SDK перебирает их.

```go theme={null}
.FallbackURL("http://fra.rpc.orbitflare.com:10000")
```

**`.Retry(policy)`** - Управляет backoff при переподключении. При обрыве соединения SDK ждёт `InitialDelay`, затем увеличивает задержку на каждой попытке (не выше `MaxDelay`). `MaxAttempts` равное 0 - бесконечные повторы. По умолчанию: 100 мс начальная, 30 с максимум, множитель 2x, бесконечно.

```go theme={null}
.Retry(orbitflare.RetryPolicy{
    InitialDelay: 200 * time.Millisecond,
    MaxDelay:     15 * time.Second,
    Multiplier:   2.0,
    MaxAttempts:  0,
})
```

**`.Timeout(duration)`** - Таймаут подключения/набора. По умолчанию 30 с.

```go theme={null}
.Timeout(15 * time.Second)
```

**`.Keepalive(duration)`** - Интервал keepalive TCP/HTTP2, используется для обнаружения мёртвых соединений на транспортном уровне. По умолчанию 60 с.

```go theme={null}
.Keepalive(30 * time.Second)
```

**`.PingInterval(duration)`** - Как часто SDK отправляет `Ping`-сообщения уровня протокола серверу. Сервер должен отвечать `Pong`. По умолчанию 10 с.

```go theme={null}
.PingInterval(15 * time.Second)
```

**`.MaxMissedPongs(n)`** - Сколько последовательных ping могут остаться без ответа, прежде чем SDK сочтёт соединение мёртвым и переподключится. По умолчанию 3. При значениях по умолчанию мёртвое соединение обнаруживается в течение 30 секунд.

```go theme={null}
.MaxMissedPongs(5)
```

**`.ChannelCapacity(n)`** - Ограниченный канал между фоновой горутиной и вашим кодом. Если ваш код медленно потребляет события, фоновая горутина блокируется при заполнении канала, а не расходует неограниченную память. По умолчанию 4096.

```go theme={null}
.ChannelCapacity(8192)
```

**`.Logger(logger)`** - `*slog.Logger` для событий жизненного цикла соединения. Если не задан, управляется через `ORBITFLARE_LOG`.

## Построение подписки

Фильтры строятся программно с помощью `SubscribeRequestBuilder` и типизированных типов фильтров. Транзакция совпадает, если задействует любой адрес из `AccountInclude`; `AccountExclude` убирает совпадения; `AccountRequired` требует присутствия всех перечисленных адресов.

```go theme={null}
request := geyser.NewSubscribeRequestBuilder().
    Transactions("pumpfun", geyser.NewTransactionFilter().
        Vote(false).
        Failed(false).
        AccountInclude("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")).
    Accounts("usdc-holders", geyser.NewAccountFilter().
        Owner("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").
        Datasize(165).
        Memcmp(geyser.MemcmpBase58(0, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")).
        Lamports(geyser.LamportsGt(0))).
    Slots("slots", geyser.NewSlotFilter().FilterByCommitment(true)).
    Commitment(geyser.Confirmed).
    Build()
```

### Типы фильтров

* **`NewTransactionFilter()`** - `Vote(bool)`, `Failed(bool)`, `Signature(string)`, `AccountInclude(...)`, `AccountExclude(...)`, `AccountRequired(...)`.
* **`NewAccountFilter()`** - `Account(...)`, `Owner(...)`, `Datasize(uint64)`, `TokenAccountState(bool)`, `Memcmp(...)`, `Lamports(...)`, `NonemptyTxnSignature(bool)`.
* **`NewSlotFilter()`** - `FilterByCommitment(bool)`, `InterslotUpdates(bool)`.
* **`NewBlockFilter()`** - `AccountInclude(...)`, `IncludeTransactions(bool)`, `IncludeAccounts(bool)`, `IncludeEntries(bool)`.
* **`NewDeshredTransactionFilter()`** - `Vote(bool)`, `AccountInclude(...)`, `AccountExclude(...)`, `AccountRequired(...)`.

`Memcmp` строится через `geyser.MemcmpBytes(offset, []byte)`, `geyser.MemcmpBase58(offset, string)` или `geyser.MemcmpBase64(offset, string)`. `Lamports` - через `geyser.LamportsEq/Ne/Lt/Gt(uint64)`. `Commitment` - одно из `geyser.Processed`, `geyser.Confirmed`, `geyser.Finalized`.

`Subscribe` также принимает сырой `*geyser.SubscribeRequest` (реэкспорт proto-типа), если вы предпочитаете собрать его вручную.

## Чтение потока

`Subscribe` возвращает `*geyser.Stream`. Вызовите `Next(ctx)`, чтобы получить следующее обновление:

```go theme={null}
stream := client.Subscribe(request)
defer stream.Close()

for {
    update, err := stream.Next(ctx)
    if errors.Is(err, io.EOF) {
        break // поток закрыт
    }
    if err != nil {
        break // фатально: все повторы исчерпаны
    }

    switch {
    case update.GetTransaction() != nil:
        tx := update.GetTransaction()
        // tx.GetSlot(), tx.GetTransaction() (подпись, аккаунты, инструкции, meta)
    case update.GetAccount() != nil:
        acct := update.GetAccount()
        // acct.GetSlot(), acct.GetAccount() (pubkey, lamports, owner, data), acct.GetIsStartup()
    case update.GetSlot() != nil:
        slot := update.GetSlot()
        // slot.GetSlot(), slot.GetStatus()
    case update.GetBlockMeta() != nil:
        meta := update.GetBlockMeta()
        // meta.GetSlot(), meta.GetBlockhash(), meta.GetParentSlot()
    }
}
```

`Next(ctx)` блокируется до прихода события. Возвращает `io.EOF` после завершения потока (в том числе после `Close`); любая другая ненулевая ошибка фатальна для этого потока. Также возвращает `ctx.Err()`, если контекст отменён. Сообщения Pong обрабатываются внутри и никогда не попадают в ваш поток.

### Закрытие

```go theme={null}
stream.Close()
```

Немедленно останавливает фоновую горутину. `client.Close()` останавливает каждый поток, запущенный клиентом.

## Несколько потоков

Один клиент может вести много потоков одновременно. У каждого своё фоновое соединение:

```go theme={null}
pumpfun := client.Subscribe(pumpfunReq)
raydium := client.Subscribe(raydiumReq)
slots := client.Subscribe(slotsReq)
```

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

## Полный пример

Поток, отслеживающий транзакции pump.fun и печатающий сводку по каждой.

```go theme={null}
package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log"

    "github.com/orbitflare/orbitflare-sdk-go/geyser"
)

func main() {
    client, err := geyser.NewBuilder().
        URL("http://ny.rpc.orbitflare.com:10000").
        FallbackURL("http://fra.rpc.orbitflare.com:10000").
        Build()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    request := geyser.NewSubscribeRequestBuilder().
        Transactions("pumpfun", geyser.NewTransactionFilter().
            Vote(false).
            Failed(false).
            AccountInclude("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")).
        Slots("slots", geyser.NewSlotFilter().FilterByCommitment(true)).
        Commitment(geyser.Confirmed).
        Build()

    stream := client.Subscribe(request)
    defer stream.Close()

    ctx := context.Background()
    var count int
    fmt.Println("streaming...")

    for {
        update, err := stream.Next(ctx)
        if errors.Is(err, io.EOF) {
            return
        }
        if err != nil {
            log.Fatal(err)
        }

        switch {
        case update.GetTransaction() != nil:
            count++
            tx := update.GetTransaction()
            fmt.Printf("#%d slot=%d\n", count, tx.GetSlot())
        case update.GetSlot() != nil:
            s := update.GetSlot()
            fmt.Printf("slot %d (%s)\n", s.GetSlot(), s.GetStatus())
        }
    }
}
```
