Библиотека для разработчиков
@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.