Библиотека для разработчиков

@solanapaykz/core — пакет на TypeScript для приёма платежей в USDC/SOL сети Solana с автоматической конвертацией из тенге по текущему курсу. Изоморфный код: работает и в Node.js 20.18+, и в браузере, зависит только от глобального fetch.

Для кого эта библиотека

Для тех, у кого своя платформа — не WordPress и не Tilda. Если магазин на WooCommerce, ему нужен готовый плагин; если на Tilda — готовый сервер. Эта библиотека — для разработчика, который пишет собственный бэкенд или собственную интеграцию и хочет получить только арифметику и проверку платежа, не выбирая за него, где хранить заказы и как показывать QR на странице.

SDK закрывает три шага приёма платежа: котировка, платёжный запрос и проверка. Он не отвечает за хранение заказов, показ QR на странице и обработку вебхуков/поллинга — это задача самой интеграции.

Установка

npm install @solanapaykz/core

Если нужна неопубликованная правка из ветки main, пакет ставится и прямо из репозитория — он соберётся на месте: npm install github:Tatancloud/solanapaykz#<хеш коммита>.

Пакет уже включает точно закреплённые версии @solana/pay и @solana/kit — менять их вручную не нужно (и @solana/kit@8 ставить нельзя: @solana/pay требует ^6.4.0).

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

Полный путь — четыре шага. orderId, saveOrder и markOrderPaid в примере — не часть SDK, это функции вашей собственной интеграции; здесь они только иллюстрируют, куда передать reference из шага 3.

import { SolanaPayKZ } from '@solanapaykz/core';

const sdk = new SolanaPayKZ({
  recipient: '<ВАШ_SOLANA_АДРЕС>', // адрес КОШЕЛЬКА продавца — НЕ адрес монеты (mint).
                                   // Ошибка здесь необратима: тем, кто владеет mint-адресом
                                   // USDC/SOL, а не вашим кошельком, платежи не вернуть.
  rpcUrl: process.env.SOLANA_RPC_URL!, // свой RPC-провайдер, см. раздел ниже
  cluster: 'mainnet',
});

// Шаг 1. Котировка: сумма в тенге → сумма токена. Курс замораживается на
// 15 минут — см. «Риск сдвига курса» ниже.
const quote = await sdk.createQuote({ amountKzt: '10000', token: 'USDC' });
// quote.amountToken — сумма в USDC строкой, например "21.758051"

// Шаг 2. Платёжный запрос: ссылка Solana Pay (solana:...) и QR-код в SVG.
const request = await sdk.createPaymentRequest(quote, {
  label: 'Магазин Example',
  message: `Заказ №${orderId}`,
});

// Шаг 3. Сохранить request.reference вместе с заказом — обязательно, см.
// «Метка платежа (reference)» ниже. Показать покупателю request.qrSvg
// (или ссылку request.url для перехода в кошелёк напрямую).
await saveOrder({
  orderId,
  reference: request.reference,
  quoteId: quote.quoteId,
});

// Шаг 4. Проверка — вызывается позже: по таймеру, при возврате покупателя на
// сайт, из cron-задачи и т. д. Можно вызывать многократно.
const status = await sdk.checkPayment({ reference: request.reference, quote });

switch (status.status) {
  case 'pending':
    // Платёж ещё не пришёл. Повторить проверку позже.
    break;
  case 'expired':
    // Котировка просрочена, платежа так и не было. Нужно выпустить новую
    // котировку и новый платёжный запрос — старую метку переиспользовать
    // нельзя, см. предупреждение про уникальность метки.
    break;
  case 'confirmed':
    // status.signature — подпись транзакции в Solana.
    // status.amountPaid — сумма из котировки, подтверждённая как полученная
    // не меньше (см. «Что означает amountPaid» ниже).
    await markOrderPaid(orderId, status.signature);
    break;
  case 'mismatch':
    // Транзакция с этой меткой найдена, но не прошла проверку (не тот
    // получатель, не тот токен или заниженная сумма). status.reason — текст
    // для логов, не для показа покупателю. Это НЕ значит «денег нет» —
    // деньги могли уже уйти. Посмотрите status.signature вручную, прежде чем
    // считать заказ неоплаченным.
    break;
}

Параметры конструктора SolanaPayKZ

Параметр Тип Обязателен По умолчанию Описание
recipient string да Solana-адрес продавца, куда должны поступать платежи.
rpcUrl string да значения по умолчанию нет Адрес RPC-узла Solana. Публичный узел не подходит, см. ниже.
cluster 'mainnet' \| 'devnet' да Кластер сети: определяет адреса mint-токенов.
markupPercent number нет 0 Наценка продавца в процентах (0–100, шаг 0.01 п.п.), применяется к сумме в тенге до конвертации.
quoteTtlMs number нет 900000 (15 минут) Срок жизни котировки в миллисекундах.

Параметры createPaymentRequest

Второй аргумент — CreatePaymentRequestOptions. recipient в него не входит: адрес получателя уже зафиксирован в конструкторе и не может быть переопределён для отдельного запроса.

Параметр Тип Обязателен По умолчанию Описание
label string нет Название магазина/получателя — показывается кошельком покупателя.
message string нет Пояснение к платежу (например, номер заказа) — показывается кошельком.
memo string нет Записывается в саму транзакцию Solana (инструкция memo) — часть ончейн-истории, а не только UI кошелька.
qrSize number нет 320 Размер QR-кода в пикселях.

Метка платежа (reference) — единственное требование к вашему хранилищу

Каждый платёжный запрос получает reference — случайную метку, по которой поиск в блокчейне находит транзакцию.

Сохраняйте reference вместе с заказом. Без него найти платёж, соответствующий конкретному заказу, невозможно — блокчейн не знает о заказах, только о переводах с меткой. Метка должна быть уникальной для каждой попытки оплаты. Поиск возвращает самую старую транзакцию с данной меткой. Если покупатель ошибся с оплатой и для повторной попытки переиспользовать ту же метку, проверка навсегда найдёт первую, неудачную транзакцию — новый корректный платёж останется невидим для checkPayment. Практическое следствие: на каждую попытку оплаты — новый createPaymentRequest, даже для того же заказа и той же котировки.

Обязательные требования к узлу Solana

rpcUrl не имеет значения по умолчанию и обязателен в конструкторе. Публичный узел (api.mainnet-beta.solana.com и аналогичные) жёстко лимитирован по частоте запросов и не хранит достаточно истории транзакций для поиска по метке — checkPayment на нём будет ненадёжен или недоступен. Для продакшена нужен собственный RPC-провайдер (Helius, QuickNode, Triton и т. п.); для разработки подойдёт публичный devnet-узел.

Формат котировки и риск сдвига курса

Котировка замораживает курс на quoteTtlMs (по умолчанию 15 минут) — ровно столько действует сумма в QR-коде. Пока покупатель не оплатил, курс на рынке может немного отличаться от зафиксированного, и эту разницу несёт продавец.

Замер пары USDT/KZT на Binance (288 пятиминутных свечей за сутки) показывает: типичное движение курса внутри пяти минут — около нуля (медиана 0.000%), 95-й перцентиль — 0.065%, максимум за сутки — 0.92%. Риск за время одной попытки оплаты обычно пренебрежимо мал, но не равен нулю — учитывайте это при выборе quoteTtlMs и наценки.

SDK опрашивает источники курса по порядку и берёт первый успешный ответ:

Приоритет Источник Как считается
Основной Binance USDTKZT × USDCUSDT (или × SOLUSDT для SOL)
Резервный синтетика курс USD/KZT (open.er-api.com) × цена токена в USD (CoinGecko)

CoinGecko не поддерживает тенге вовсе: запрос отвечает HTTP 200 и пустым объектом, то есть отказ происходит молча, а не явной ошибкой. Поэтому тенге для резервного варианта берётся отдельно у поставщика курсов валют, а такой пустой ответ SDK трактует как отказ источника, а не как нулевой курс. Резервный курс обновляется раз в сутки и в момент сбоя Binance может отличаться от биржевого примерно на процент.

Порядок источников фиксирован внутри SDK и не настраивается через публичный API — RateProvider, BinanceRateSource, SyntheticRateSource и тип RateSource намеренно не экспортируются из пакета, чтобы нельзя было собрать провайдер курса с другим порядком источников в обход SolanaPayKZ.

Проверка платежа

checkPayment возвращает один из четырёх статусов:

Статус Поля Когда
pending Транзакция с данной меткой ещё не найдена, котировка не просрочена.
expired Транзакция не найдена, а котировка уже просрочена.
confirmed signature, amountPaid Транзакция найдена и прошла проверку получателя, токена и суммы.
mismatch signature, reason Транзакция найдена, но не прошла проверку — не тот получатель, не тот токен или заниженная сумма.

Подтверждение проверяется на уровне finalized — самом надёжном из доступных в Solana; более быстрые, но менее надёжные уровни (confirmed, processed) SDK не использует.

Платёж, пришедший уже после истечения котировки (expired на момент проверки), всё равно может найтись и подтвердиться при следующем вызове — транзакция в блокчейне необратима. Принимать такой платёж как оплату заказа или нет — решение вашей интеграции, SDK его не принимает.

Статус mismatch — это не то же самое, что «покупатель не заплатил». Транзакция с данной меткой найдена в блокчейне — деньги уже могли уйти со счёта покупателя, просто проверка не сошлась по формальному признаку. Увидев mismatch, не считайте заказ автоматически неоплаченным — посмотрите транзакцию по status.signature вручную и решите, что с ней делать, прежде чем сообщать покупателю об ошибке оплаты или создавать новый платёжный запрос.

Что означает amountPaid

При статусе confirmed поле amountPaid — это сумма из котировки, подтверждённая как полученная не меньше этой суммы, а не точная сумма фактического перевода. Проверка (через validateTransfer из @solana/pay) проверяет условие «переведено не меньше ожидаемого» — переплата тоже проходит успешно. Если важна именно фактически поступившая сумма, смотрите её в самой транзакции по status.signature, а не в amountPaid.

Обработка ошибок

Методы SDK, помимо штатных статусов PaymentStatus, могут выбрасывать исключения. Все они наследуются от SolanaPayKzError.

Метод Что может выбросить Когда
createQuote ConfigError Некорректные входные данные (сумма, token/cluster, TTL) — до сетевого запроса.
createQuote RateUnavailableError Ни один источник курса не ответил.
createPaymentRequest ConfigError Котировка испорчена или некорректен recipient.
createPaymentRequest QuoteExpiredError Котировка уже просрочена — нужно выпустить новую.
checkPayment ConfigError Котировка испорчена, recipient/reference невалидны, либо кластер котировки не совпадает с кластером клиента.
checkPayment ошибки RPC-клиента / сетевые ошибки Пробрасываются наружу без оборачивания. Сбой RPC-узла — это не mismatch и не pending, а исключение: временный сбой сети не должен выглядеть как результат проверки платежа. Оборачивайте вызов в try/catch и предусматривайте повтор.

Конструктор new SolanaPayKZ(...) также синхронно бросает ConfigError на некорректные recipient и rpcUrl — стоит проверить это сразу при создании клиента, а не только при первом вызове.

Приватные ключи

SDK не создаёт, не хранит и не запрашивает приватные ключи ни продавца, ни покупателя. Метка платежа (reference) — это 32 случайных байта, отформатированные как Solana-адрес; пара ключей для неё не генерируется и не существует. Проверка платежа читает только публичные данные блокчейна (RPC); подписывать или отправлять транзакции от чьего-либо имени SDK не умеет и не пытается.

Подробнее о том, что именно проверяется в транзакции и что происходит при несовпадении суммы или сбое сети — на странице «Безопасность».

Совместимость

Node.js 20.18+ и браузер. Единственная внешняя зависимость времени выполнения, требующая сети, — глобальный fetch; в старых окружениях Node без встроенного fetch потребуется полифил.

Генерация QR (createPaymentRequest) запрашивает у библиотеки qrcode только SVG. Сборщики (webpack, Vite/Rollup, esbuild) под браузер по умолчанию учитывают поле "browser" в package.json этой библиотеки и используют её браузерный вариант, где не встречается fs — специальная настройка с вашей стороны не нужна. Проблема теоретически может проявиться только при нестандартной конфигурации сборки, явно отключающей учёт поля browser.

Лицензия

MIT — github.com/Tatancloud/solanapaykz.