API
Каждый узел OpenFiat раскрывает одну конечную точку JSON-RPC 2.0, смоделированную
прямо по собственному JSON-RPC API Solana — имена методов в camelCase
getX/sendX через единственную конечную точку POST, а не иерархию ресурсов
REST. Её реализуют крейты rpc и api в
openfiat-core.
Конечная точка
POST /rpc
Content-Type: application/json
Каждый запрос — стандартный конверт JSON-RPC 2.0:
{ "jsonrpc": "2.0", "id": 1, "method": "getAdvertisement", "params": { "id": "ad-1" } }
и каждый ответ — либо result, либо error, никогда оба:
{ "jsonrpc": "2.0", "id": 1, "result": { "id": "ad-1", "status": "Active", "..." : "..." } }
Публичный devnet-узел находится на https://openfiat.allenhark.com — тот же хост
также публикует multiaddr точки входа для узлов, что является другим адресом
для другой задачи (см.
getting started).
Любой узел, который вы запускаете сами, обслуживает идентичный интерфейс на
:7080.
Ключи, peer id и подписи — это base58
Каждый публичный ключ, идентификатор пира, подпись и идентификатор события — это строка base58:
{
"service_id": "node-ALLENLMtV1zEAHT3xpVryqcbdPCB8c9JhM1Jdbe5XHg5",
"provider": "12D3KooWK9hQ7TwbfvFiaAxUbRFCkdhS7iEpAJDnewNL1anyREQ1",
"provider_public_key": "ALLENLMtV1zEAHT3xpVryqcbdPCB8c9JhM1Jdbe5XHg5"
}
До недавнего времени это были массивы целых чисел. Если вы видите
"provider_public_key": [192, 74, 15, ...], вы говорите с узлом, предшествующим
изменению, — и ничто в этом ответе не отличает опубликованный публичный ключ от
утёкшего приватного, потому что секрет Ed25519 — это тоже тридцать два байта.
Именно эта неоднозначность и стала причиной изменения. Форма base58 к тому же
единственная пригодная: 12D3KooW… — это то, что принимает --entrypoint и что
можно найти в логе.
Это не только изменение отображения. Полезная нагрузка sendX подписывается
по JSON её внутренней структуры, поэтому клиент, который пишет ключ в нагрузку как
массив, создаёт запись, которую узел не воспроизводит. Тогда подпись не проходит
проверку — что проявляется как отклонённая мутация, а не как ошибка парсинга.
Используйте SDK, и это обрабатывается за вас; при ручной сборке
формата провода кодируйте идентификаторы как base58.
Байтовые поля, которые не являются идентификаторами, остаются массивами.
commitment голоса по спору и его secret раскрытия — это непрозрачные
тридцатидвухбайтовые значения, не идентичности, и отправляются как массивы.
Различие — по тому, чем поле является, а не по его длине.
Именование методов
Методы чтения начинаются с get и никогда не меняют состояние:
{ "method": "getReservation", "params": { "id": "res-1" } }
{ "method": "getReservations", "params": {} }
Метод getMyX всё ещё чтение, но отвечает только за кошелёк, владение которым
вызывающий доказывает, — см. чтения с доказательством кошелька.
Мутации начинаются с send и принимают одно поле — data, base64-кодированную,
уже подписанную полезную нагрузку провода, которую собственный кошелёк
вызывающего произвёл локально. Это зеркалит sendTransaction в Solana: узел
никогда не конструирует и не подписывает ничего от имени вызывающего, он лишь
декодирует нагрузку и применяет её по тому же пути проверки подписи, по которому
проходит событие, полученное через gossip.
{ "method": "sendReservationRequest", "params": { "data": "<base64 wire bytes>" } }
Типизированный Client каждого SDK строит и подписывает эту нагрузку
за вас — это рекомендуемая точка интеграции, а не сборка формата провода вручную.
Категории методов
| Домен | Примеры методов |
|---|---|
| Объявления | getAdvertisement, getAdvertisements, sendAdvertisementCreate, sendAdvertisementPriceUpdate, sendAdvertisementDisable |
| Резервирования | getReservation, getReservations, getMyReservations, sendReservationRequest |
| Расчёты | getSettlement, getSettlements, getMySettlements, sendSettlementInitiate, sendPaymentSubmitted, sendSettlementApproved |
| Сделка (соединение только для чтения) | getTrade, getTrades |
| Споры | getDispute, getDisputes, getMyDisputes, sendDisputeOpen, sendArbitratorJoin, sendVoteCommit, sendVoteReveal |
| Доказательства кошелька | getWalletChallenge, getCounterpartiesChallenge, getCounterparties, getProviderEarningsChallenge |
| Объём | getSettledVolume |
| Вложения и контент | getSettlementAttachments, getHeldContent, sendAttachmentPublish |
| Идентичность | getIdentityClaim, getIdentityClaimsByWallet, sendClaimPublish |
| Репутация (только чтение) | getReputation |
| Управление | getProposal, getProposals, sendProposalCreate, sendVoteCast |
| Поставщики услуг | getProvider, getProviders, sendProviderRegister, sendProviderHealthUpdate, getProviderEarnings, sendProviderWithdraw |
| Уведомления | getSubscription, getNotificationDispatch, sendSubscriptionUpdate, sendDeliveryReport |
| Оракулы | getOracleRecord, getExchangeRate, getMedianExchangeRate, sendOraclePublish |
| Анализ рисков | getWalletScreening, sendRiskPublish |
| Вознаграждения | getRewardObservations |
| Снапшоты | getLatestSnapshot, getSnapshots, getCheckpointSlot, sendSnapshotAnnounce |
| Сессии | getSession, sendSessionEstablish, sendSessionRenew, sendSessionRevoke, sendSessionMigrate |
| Мост цепочки (Solana, OFS-4300) | getChainStatus, getLatestBlockhash, sendTransaction |
| Узел | getVersion, getHealth, getPeers |
Чтения, не называющие стороны
getSettlement(s), getReservation(s) и getDispute(s) возвращают записи с
удалённой идентичностью сторон. Это намеренное, продиктованное безопасностью
изменение, а не недосмотр, и у него своя страница:
что возвращает публичное чтение. Сторона читает свои
собственные записи полностью через чтения с доказательством кошелька.
Споры решаются в цепочке, а не отвечающим вам узлом
Ответ getDispute несёт раскрытия, которые узел собрал, и не несёт исход,
выведенный из них. Разрешение появляется только после того, как этот узел
наблюдал подтверждение транзакции исполнения и прочитал, что она решила, — см.
как разрешается спор. Клиент, который сам подсчитывает
раскрытия, вновь ввёл ровно то расхождение, которое узел перестал производить.
Два метода обменного курса, и какой выбрать
getMedianExchangeRate возвращает голое число или null, что является правильной
формой, когда всё, что вам нужно, — это цена или ничего.
getExchangeRate принимает те же { base, quote } и вместо этого отвечает
помеченным статусом, потому что null схлопывает два разных факта:
{ "status": "current", "rate": 129.5, "expiresAt": 1753800000000 }
{ "status": "stale" }
{ "status": "noData" }
Различие не академическое. Stale означает, что провайдер эту пару всё же публикует и каждая запись истекла (OFS-7000 §12: истёкшие данные не текущие, каким бы недавним ни был лапс) — фид, вероятно, вернётся, поэтому ждать разумно. NoData означает, что этот коридор никто не оценивает и ждать бессмысленно. Ни то ни другое не число, и вызывающий не должен показывать ни одно как число.
Используйте getExchangeRate, если нет причины не делать этого.
getMedianExchangeRate остаётся, потому что клиенты от него зависят.
Service id
Узел регистрируется под node-<его публичный ключ base58>, а поставщик снапшотов
под snapshot-<тот же ключ> — префикс и есть то, что позволяет одному узлу
держать несколько записей реестра без их столкновения.
Идентификатор выведен, а не случаен, чтобы перезапускающийся узел обновлял свою существующую запись, а не оставлял мёртвую запись позади. Выводить его из целого ключа важно: более ранняя схема использовала первые восемь байт peer id как hex, что выглядит как шестнадцать цифр идентичности, но их две, поскольку каждый peer id Ed25519 открывается одной и той же шестибайтовой преамбулой. Два узла столкнулись в пределах нескольких сотен регистраций, и второй зарегистрировавшийся вытеснил первого.
Что узел знает о сети
getPeers сообщает о пирах, которые этот узел обнаружил, об адресах, которые он
анонсирует о себе, и о собственном self_peer_id в форме 12D3Koo…, которая
идёт в --entrypoint. См. обнаружение пиров
для взгляда оператора на это.
Ошибки
Стандартные коды ошибок JSON-RPC 2.0 (-32700 ошибка парсинга, -32601 метод не
найден, -32602 неверные параметры, -32603 внутренняя ошибка) покрывают сбои
уровня транспорта. Каждый сбой уровня домена — недостаточная ликвидность,
дублирующее событие, неавторизованный подписант — возвращается как единая ошибка
приложения -32000, с собственным числовым кодом протокола и символьным именем
(из OFS-8000) в data:
{ "error": { "code": -32000, "message": "INSUFFICIENT_AVAILABLE_LIQUIDITY", "data": { "ofsErrorCode": 4004, "ofsErrorName": "INSUFFICIENT_AVAILABLE_LIQUIDITY" } } }
Подписки
GET /ws
транслирует каждую успешную мутацию по мере её появления — {"method": "sendX", "result": ...} — чтобы клиент мог реагировать на активность рынка без опроса. Фильтруйте на стороне клиента по интересующим вас методам.
Снапшоты помечаются slot, а не высотой
getCheckpointSlot возвращает slot Solana, по состоянию на который состояние
последнего импортированного снапшота было текущим, или null на узле, который не
импортировал ни одного.
Раньше это было getCheckpointHeight, и переименование не косметическое. Старое
значение было собственным подсчётом событий gossip производящего узла, что
зависит от производителя: два узла с идентичным состоянием сообщают разные числа,
а узел, присоединившийся на прошлой неделе, сообщает меньшее, чем узел, работающий
с генезиса. Сравнение чисел двух производителей не сравнивало ничего.
Slot — единственные часы, которые каждый участник уже разделяет. Он также делает утверждение проверяемым — узел может сравнить анонсированный slot со своим собственным взглядом на цепочку и отклонить один из неправдоподобного будущего, что невозможно против числа, которое видит только анонсирующий.
То, что slot утверждает, у́же, чем кажется. Он говорит, когда состояние было захвачено, а не что оно содержит: два узла, делающие снапшот на одном slot, могут держать слегка разное состояние gossip, потому что распространение не мгновенно. Считайте его якорем свежести, а не доказательством того, что один снапшот содержит другой, — то же, что́ под этим подразумевают собственные снапшоты Solana.
Узел, который никогда не наблюдал slot, снапшоты не производит и так и говорит. Это не требование запускать RPC-соединение: узел только с gossip узнаёт slot-ы через мост цепочки.
Рассчитанный объём, и почему он по активам
getSettledVolume отвечает одной строкой на актив, никогда итогом:
{
"assets": [
{ "asset_mint": "2bHPi…RRU", "asset_symbol": "USDC", "decimals": 6,
"base_units": 4500000, "settlements": 12 },
{ "asset_mint": "So111…112", "asset_symbol": "wSOL", "decimals": 9,
"base_units": 2000000000, "settlements": 3 }
],
"unattributed_settlements": 1,
"settlements_known": 16,
"scope": "settlements this node has replicated and observed confirmed"
}
Четыре вещи, которые клиент не должен делать с этим:
Не суммируйте между активами. Это разные токены в разных масштабах; объединённая цифра прибавляет SOL к USDC и не значит ничего.
Не угадывайте decimals. Он равен null, наряду с null asset_symbol,
когда у этого узла нет имени для того mint. Показывайте адрес и сырые базовые
единицы. Предположение 6 — это ровно то, отчего wSOL, у которого их девять,
выходит в тысячу раз больше.
Не скрывайте unattributed_settlements. Это реальные подтверждённые расчёты,
объявление которых с тех пор удалено, поэтому их актив невосстановим. Их пропуск
делает итоги полными на вид, тогда как их не хватает ровно на столько.
Не отбрасывайте scope. Он говорит, что это расчёты, которые этот узел
реплицировал и подтвердил, — а не вся история сети. Цифра объёма, поданная без её
охвата, читается как глобальный итог. settlements_known рядом с подсчитанными
строками заставляет остаток читаться как сделки в пути, а не как расхождение.
Интерактивный справочник
Документ OpenRPC 1.2.6 (эквивалент JSON-RPC спецификации
OpenAPI/Swagger) — /api/openrpc.json — плюс
самодостаточная интерактивная страница для просмотра каждого метода. Список
методов генерируется прямо из собственной живой таблицы диспетчеризации
openfiat-rpc (cargo run -p openfiat-api --example dump_openrpc), поэтому он не
может уйти в метод, который реальный узел не запускает; он публикуется здесь как
статический снапшот, поскольку у этого сайта документации нет своего узла, чтобы
подавать его вживую. Направьте панель «Try it» справочной страницы на узел,
который запускаете сами (по умолчанию http://localhost:7080), чтобы вызвать
метод по-настоящему.
Схемы по методам в этом документе — намеренно упрощённое, основанное на
соглашениях приближение — каждый getX(id) принимает {id}, каждый sendX
принимает {data} — а не JSON Schema, выведенная из конкретных типов Rust каждого
метода. Там, где метод отходит от этих соглашений, авторитетная форма — этот сайт:
чтения с доказательством кошелька, getExchangeRate и
getPeers все принимают параметры, которые соглашение не описывает.
Работающий узел также подаёт идентичный справочник вживую и с того же источника,
что и его собственный /rpc: GET /openrpc.json и GET /docs. GET /metrics
раскрывает счётчики запросов в формате Prometheus для операторов.