Аутентификация и подпись
Требования к подписи EIP-712 для операций записи.
В продакшене используйте chain ID 999. Тестнет временно отключён, пока Hypercall не получит больше тестовых HYPE.
Обзор
Все операции записи требуют подписи типизированных данных EIP-712. Подписант должен быть:
- Самим адресом кошелька (прямая подпись), ИЛИ
- Авторизованным агентом этого кошелька (см. Авторизация агентов)
Домен EIP-712
Разделитель домена:
{
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Chain ID: 999 (основная сеть)
Типы сообщений
PlaceOrder
Структура с явно указанным маршрутом:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
uint64 nonce;
}
Структура без поля маршрута:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
Требования к полям:
wallet: адрес кошелька (hex-строка с префиксом 0x)symbol: символ опциона (например,"BTC-20250131-100000-C")side:"Buy"или"Sell"(требуется точное совпадение строки)size: размер в виде строки (например,"0.1"), ОБЯЗАН в точности совпадать со значением в JSONprice: цена в виде строки (например,"100.0"), ОБЯЗАНА в точности совпадать со значением в JSONtif: срок действия ордера (time-in-force):"gtc","ioc"или"fok"(требуется точное совпадение строки)route: необязательный маршрут ордера:"best_execution"или"book_only"(если поле передано, требуется точное совпадение строки).POST /bulk_orderпринимает только"book_only". Значение"rfq_only"входит в подписываемый словарь, но сейчас все эндпоинты ордеров его отклоняют; для исполнения через RFQ используйте эндпоинты RFQ.clientId: идентификатор ордера на стороне клиента (строка, может быть пустой"")nonce: nonce в виде временной метки в миллисекундах (uint64), уникальный для каждого подписанта. См. Управление nonce
Важно: price и size ОБЯЗАНЫ быть строками и в подписываемом сообщении, и в теле JSON-запроса. Форматирование строк должно совпадать в точности.
Маршрут по умолчанию: передавайте route. Ордер без этого поля пока принимается и направляется как best_execution, однако POST /order возвращает заголовки Deprecation, Sunset и Warning, а объявленная дата прекращения поддержки (4 июля 2026 года) уже прошла. Форма без маршрута может быть удалена без дополнительного переходного периода. Если вы всё же не передаёте route в JSON, не включайте его и в типизированные данные.
PlaceOrderReduceOnly
Ордер только на сокращение позиции ("reduce_only": true в теле JSON) подписывается отдельной структурой. Для него обязателен явный route.
struct PlaceOrderReduceOnly {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
bool reduceOnly;
uint64 nonce;
}
reduceOnly должен быть true и совпадать с телом запроса. Ордер reduce-only без подписанного route отклоняется с ошибкой missing_field.
ReplaceOrder
PUT /order и каждая запись PUT /bulk_order подписываются как ReplaceOrder. В этой структуре нет поля route; ключ route в теле JSON игнорируется.
struct ReplaceOrder {
address wallet;
string orderId;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
orderId содержит десятичную строку идентификатора заменяемого ордера. Замена reduce-only ("reduce_only": true) подписывается как ReplaceOrderReduceOnly, где между clientId и nonce добавлено поле bool reduceOnly. TypeScript SDK экспортирует обе карты типов.
CancelOrder
Структура:
struct CancelOrder {
address wallet;
string orderId;
uint64 nonce;
}
Требования к полям:
wallet: адрес кошелькаorderId: идентификатор ордера в виде строки (например,"123")nonce: уникальный nonce
CancelOrderByClientId
Структура:
struct CancelOrderByClientId {
address wallet;
string clientId;
uint64 nonce;
}
Требования к полям:
wallet: адрес кошелькаclientId: клиентский идентификатор ордера (строка)nonce: уникальный nonce
ApproveAgent
Структура:
struct ApproveAgent {
address agent;
uint64 nonce;
}
Требования к полям:
agent: адрес кошелька агента, которого нужно авторизоватьnonce: уникальный nonce
Примечание: кошелёк, авторизующий агента, определяется по восстановленной подписи.
RevokeAgent
Структура:
struct RevokeAgent {
address agent;
uint64 nonce;
}
Требования к полям:
agent: адрес кошелька агента, авторизацию которого нужно отозватьnonce: уникальный nonce
RevokeAllAgents
DELETE /revoke-all-agents удаляет всех агентов подписывающего кошелька.
struct RevokeAllAgents {
uint64 nonce;
}
Симуляция риска
POST /risk/simulate/portfolio и POST /risk/simulate/orders представляют собой публичные
запросы только на чтение к движку как источнику истины. Они не требуют подписи, аутентификации
или nonce. Поле wallet определяет, снимок какого аккаунта оценивается.
Пример запроса по портфелю:
curl -X POST https://api.hypercall.xyz/risk/simulate/portfolio \
-H 'Content-Type: application/json' \
-d '{
"wallet": "0x1111111111111111111111111111111111111111",
"add_positions": true,
"positions": [
{"symbol": "BTC-20261231-100000-C", "quantity": "1.25"}
]
}'
Пример атомарного запроса по ордерам:
curl -X POST https://api.hypercall.xyz/risk/simulate/orders \
-H 'Content-Type: application/json' \
-d '{
"wallet": "0x1111111111111111111111111111111111111111",
"execution_mode": "atomic",
"hypothetical_open_orders": [
{"symbol": "BTC-PERP", "side": "Buy", "size": "0.1", "price": "100000", "reduce_only": false}
],
"orders": [
{"symbol": "BTC-20261231-100000-C", "side": "Buy", "size": "1", "price": "4200", "reduce_only": false},
{"symbol": "BTC-20261231-120000-C", "side": "Sell", "size": "1", "price": "1800", "reduce_only": false}
]
}'
Поле hypothetical_open_orders необязательно. Его записи рассматриваются как стоящие в книге GTC-ордера
и допускаются последовательно, в порядке массива, до orders. Только в рамках этого
запроса они оцениваются вместе с фактическими открытыми ордерами аккаунта
и активными резервированиями. Состояние движка не изменяется. При
успешной симуляции simulated и delta учитывают всю гипотетическую
последовательность плюс предлагаемый лимитный ордер или атомарный пакет. Суммарно два массива ордеров
могут содержать не более 50 записей.
Ответы симуляции отражают снимок состояния и не гарантируют исполнения. Маршрутизация, доступность RPI, мэтчинг, допуск nonce и будущие приостановки торгов не моделируются. Реальный ордер, отправленный позже, может дать другой результат, поскольку состояние движка к тому моменту изменится.
Процесс подписи
Шаг 1: сформируйте сообщение
Пример для PlaceOrder с явным маршрутом:
const message = {
wallet: "0x1111111111111111111111111111111111111111",
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1", // MUST be string
price: "100.0", // MUST be string
tif: "gtc",
route: "best_execution",
clientId: "mm-1",
nonce: Date.now() // current ms epoch; send this exact value in the body
};
Шаг 2: задайте домен
const domain = {
name: "Hypercall",
version: "1",
chainId: 999,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
Шаг 3: подпишите типизированные данные
С помощью ethers.js v6 (в v5 метод называется _signTypedData):
const signature = await signer.signTypedData(domain, {
PlaceOrder: [
{ name: "wallet", type: "address" },
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "size", type: "string" },
{ name: "price", type: "string" },
{ name: "tif", type: "string" },
{ name: "route", type: "string" },
{ name: "clientId", type: "string" },
{ name: "nonce", type: "uint64" }
]
}, message);
Шаг 4: отправьте запрос
Важно: в теле JSON-запроса должны использоваться те же самые строковые значения price и size:
const body = {
wallet: message.wallet,
symbol: message.symbol,
price: message.price, // same string as signed
size: message.size, // same string as signed
side: message.side,
tif: message.tif,
route: message.route,
client_id: message.clientId,
nonce: message.nonce, // same value as signed
signature
};
Управление nonce
Требования:
- Nonce должен находиться строго в пределах от двух суток до и одних суток после серверного времени в миллисекундах, поэтому используйте текущее Unix-время в миллисекундах. Небольшие счётчики вроде
1или123отклоняются. - Nonce отслеживаются по ключу подписи. Для ордеров и отмен это агент, если подписывает агент; для одобрения и отзыва агентов это кошелёк владельца.
- Повторное использование nonce невозможно. Nonce ордеров общие для
POST /order, пакетных ордеров иPlaceOrderчерез WebSocket: nonce, использованный в одном транспорте, нельзя повторить в другом. - Пропуски допустимы. Движок хранит 100 последних nonce на подписанта; когда их набирается 100, новый nonce должен быть больше наименьшего из сохранённых.
Рекомендации:
- Инициализируйте значение временем эпохи в миллисекундах и сохраняйте последний nonce для каждого подписанта, возвращая
max(now_ms, last + 1) - Используйте один общий аллокатор во всех процессах, которые подписывают одним и тем же ключом
- Если запрос завершился ошибкой, подпишите его заново с новым nonce
Авторизация агентов
Если вы подписываете кошельком агента:
-
Одобрите агента (однократно):
POST /approve-agent{"agent": "0x...","nonce": 1791273600000,"signature": "0x..." # Signed by wallet owner} -
Подписывайте ордера кошельком агента:
- Подписывайте сообщения
PlaceOrder/CancelOrderкошельком агента - В поле
walletукажите адрес торгового кошелька - Middleware проверяет, что агент авторизован для этого кошелька
- Подписывайте сообщения
Подробнее об авторизации агентов см. в разделе Авторизация агентов.
Ордера на перпы
Ордера на бессрочные контракты не используют описанный выше домен Hypercall. Они подписываются как HLOrder в домене HypercallApiSign (chain ID 999, нулевой verifying contract) API-кошельком, зарегистрированным в вашем аккаунте Hypercall. Структура и единицы измерения описаны в разделе Подпись EIP-712.
Маршрут через TEE в API включается явно: вы выбираете его, обращаясь к эндпоинтам TEE. В приложении Hypercall он используется по умолчанию. Чтобы воспользоваться им, отправьте подписанный ордер на POST /v1/tee-perp/orders/submit с полями account, nonce, action и signature. Hypercall выполняет проверку маржи, после чего защищённый анклав подписывает и отправляет нативный ордер Hyperliquid от имени вашего аккаунта. Каждый ордер на перп несёт builder code Hypercall с комиссией 0%, поэтому менеджер аккаунта должен один раз одобрить этого билдера перед первым ордером (шаг Enable trading в приложении). Без этого ордер отклоняется с ошибкой builder_not_approved. Описание процесса см. в разделе Перпы, схемы запросов и ответов приведены в справочнике API.
Собственный домен Hyperliquid Exchange (chain ID 1337, payload Agent в формате MessagePack) API Hypercall не принимает.
Типичные ошибки
unauthorized (HTTP 401): "Signer 0x... is not authorized to act for wallet 0x..."
Восстановление подписи EIP-712 всегда даёт какой-то адрес. Если подписанные данные хоть в чём-то расходятся с отправленными, восстановленный адрес не совпадёт с вашим ключом, и запрос упадёт именно на этой ошибке, а не на ошибке подписи.
Причины:
- Форматирование строк
priceилиsizeизменилось между подписью и отправкой (например,"100.0"и"100") - Подписанное поле расходится с телом JSON (
route,clientId/client_id,nonce,tif), либоrouteбыл подписан, но не передан в теле, или наоборот - Неверный домен (chain ID, name, version, verifying contract)
- Неверная структура для данного запроса (например,
PlaceOrderвместоReplaceOrderдляPUT /order) - Подписант действительно не является ни кошельком, ни одобренным агентом. Одобрите агента через
POST /approve-agentили подписывайте напрямую кошельком
Если в сообщении указан незнакомый вам адрес подписанта, причина в пунктах 1–4.
signature_verification_failed (HTTP 400)
Причина: подпись вообще не удалось декодировать или восстановить, например из-за неверной длины или некорректного hex.
invalid_parameter (HTTP 400): "price must be provided as a string for signature verification"
Причина: price или size переданы как числа JSON. Передавайте оба поля строками.
Примеры реализации
Python (eth_account 0.10 и новее)
import time
from eth_account import Account
from eth_account.messages import encode_typed_data
domain = {
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
message = {
"wallet": "0x1111111111111111111111111111111111111111",
"symbol": "BTC-20250131-100000-C",
"side": "Buy",
"size": "0.1",
"price": "100.0",
"tif": "gtc",
"route": "best_execution",
"clientId": "mm-1",
"nonce": int(time.time() * 1000) # current ms epoch; send this exact value in the body
}
types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"PlaceOrder": [
{"name": "wallet", "type": "address"},
{"name": "symbol", "type": "string"},
{"name": "side", "type": "string"},
{"name": "size", "type": "string"},
{"name": "price", "type": "string"},
{"name": "tif", "type": "string"},
{"name": "route", "type": "string"},
{"name": "clientId", "type": "string"},
{"name": "nonce", "type": "uint64"}
]
}
structured_msg = {
"types": types,
"domain": domain,
"primaryType": "PlaceOrder",
"message": message
}
encoded = encode_typed_data(full_message=structured_msg)
signed = Account.sign_message(encoded, private_key)
signature = signed.signature.hex()
JavaScript (ethers.js v6)
const { ethers } = require("ethers");
const domain = {
name: "Hypercall",
version: "1",
chainId: 999,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
const types = {
PlaceOrder: [
{ name: "wallet", type: "address" },
{ name: "symbol", type: "string" },
{ name: "side", type: "string" },
{ name: "size", type: "string" },
{ name: "price", type: "string" },
{ name: "tif", type: "string" },
{ name: "route", type: "string" },
{ name: "clientId", type: "string" },
{ name: "nonce", type: "uint64" }
]
};
const message = {
wallet: "0x1111111111111111111111111111111111111111",
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1",
price: "100.0",
tif: "gtc",
route: "best_execution",
clientId: "mm-1",
nonce: Date.now() // current ms epoch; send this exact value in the body
};
const signature = await signer.signTypedData(domain, types, message);
Проверка подписей
Порядок проверки генерации подписи:
- Сгенерируйте подпись своей реализацией
- Отправьте тестовый ордер через
POST /order - Проверьте ответ: HTTP 200 со
status"ACKED"либо"REJECTED"с полемreason - Если запрос вернул HTTP 401
unauthorizedс незнакомым адресом подписанта или HTTP 400signature_verification_failed, проверьте:- Форматирование строк
priceиsize - Параметры домена (особенно
chainId) - Что каждое подписанное поле совпадает с телом запроса, включая
routeиnonce
- Форматирование строк
Вопросы безопасности
- Никогда не раскрывайте приватные ключи: используйте аппаратные кошельки или защищённое управление ключами
- Управление nonce: используйте персистентные nonce на основе временных меток в миллисекундах для каждого ключа подписи
- Авторизация агентов: регулярно проверяйте список авторизованных агентов через
GET /authorized-agents - Кодирование строк: убедитесь, что
priceиsizeпередаются строками и при подписи, и в запросе
Ссылки
- EIP-712: https://eips.ethereum.org/EIPS/eip-712
- Реализация: обеспечивается стеком восстановления подписи и middleware.