Эта страница переведена автоматически. Оригинал на английском языке является каноническим. Читать на английском →
Перейти к основному содержимому

Аутентификация и подпись

Требования к подписи EIP-712 для операций записи.

Домен подписи в основной сети

В продакшене используйте chain ID 999. Тестнет временно отключён, пока Hypercall не получит больше тестовых HYPE.

Обзор​

Все операции записи требуют подписи типизированных данных EIP-712. Подписант должен быть:

  1. Самим адресом кошелька (прямая подпись), ИЛИ
  2. Авторизованным агентом этого кошелька (см. Авторизация агентов)

Домен 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"), ОБЯЗАН в точности совпадать со значением в JSON
  • price: цена в виде строки (например, "100.0"), ОБЯЗАНА в точности совпадать со значением в JSON
  • tif: срок действия ордера (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

Авторизация агентов​

Если вы подписываете кошельком агента:

  1. Одобрите агента (однократно):

    POST /approve-agent
    {
    "agent": "0x...",
    "nonce": 1791273600000,
    "signature": "0x..." # Signed by wallet owner
    }
  2. Подписывайте ордера кошельком агента:

    • Подписывайте сообщения 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 всегда даёт какой-то адрес. Если подписанные данные хоть в чём-то расходятся с отправленными, восстановленный адрес не совпадёт с вашим ключом, и запрос упадёт именно на этой ошибке, а не на ошибке подписи.

Причины:

  1. Форматирование строк price или size изменилось между подписью и отправкой (например, "100.0" и "100")
  2. Подписанное поле расходится с телом JSON (route, clientId/client_id, nonce, tif), либо route был подписан, но не передан в теле, или наоборот
  3. Неверный домен (chain ID, name, version, verifying contract)
  4. Неверная структура для данного запроса (например, PlaceOrder вместо ReplaceOrder для PUT /order)
  5. Подписант действительно не является ни кошельком, ни одобренным агентом. Одобрите агента через 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);

Проверка подписей​

Порядок проверки генерации подписи:

  1. Сгенерируйте подпись своей реализацией
  2. Отправьте тестовый ордер через POST /order
  3. Проверьте ответ: HTTP 200 со status "ACKED" либо "REJECTED" с полем reason
  4. Если запрос вернул HTTP 401 unauthorized с незнакомым адресом подписанта или HTTP 400 signature_verification_failed, проверьте:
    • Форматирование строк price и size
    • Параметры домена (особенно chainId)
    • Что каждое подписанное поле совпадает с телом запроса, включая route и nonce

Вопросы безопасности​

  1. Никогда не раскрывайте приватные ключи: используйте аппаратные кошельки или защищённое управление ключами
  2. Управление nonce: используйте персистентные nonce на основе временных меток в миллисекундах для каждого ключа подписи
  3. Авторизация агентов: регулярно проверяйте список авторизованных агентов через GET /authorized-agents
  4. Кодирование строк: убедитесь, что price и size передаются строками и при подписи, и в запросе

Ссылки​