인증 및 서명
쓰기 작업에 필요한 EIP-712 서명 요건입니다.
프로덕션에서는 체인 ID 999를 사용합니다. 테스트넷은 Hypercall이 테스트넷 HYPE를 추가로 확보할 때까지 일시적으로 비활성화되어 있습니다.
개요
모든 쓰기 작업에는 EIP-712 타입 데이터 서명이 필요합니다. 서명자는 다음 중 하나여야 합니다.
- 지갑 주소 자체(직접 서명), 또는
- 해당 지갑에 대해 권한을 부여받은 에이전트(에이전트 인증 참조)
EIP-712 도메인
도메인 구분자:
{
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
체인 ID: 999 (프로덕션)
메시지 타입
PlaceOrder
route를 명시한 구조체:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string route;
string clientId;
uint64 nonce;
}
route를 생략한 경우의 구조체:
struct PlaceOrder {
address wallet;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
필드 요건:
wallet: 지갑 주소(0x 접두사가 붙은 16진수 문자열)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: 클라이언트가 지정한 주문 ID(문자열, 빈 값""가능)nonce: 밀리초 타임스탬프 논스(uint64), 서명자별로 고유해야 합니다. 논스 관리를 참조하십시오
중요: price와 size는 서명 메시지와 JSON 요청 본문 모두에서 반드시 문자열이어야 합니다. 문자열 형식도 정확히 일치해야 합니다.
route 기본값: route를 전송하십시오. route가 없는 주문도 여전히 접수되며 best_execution으로 라우팅되지만, POST /order는 Deprecation, Sunset, Warning 헤더를 함께 반환하며 공지된 종료일(2026년 7월 4일)은 이미 지났습니다. route를 생략하는 형식은 추가 유예 기간 없이 제거될 수 있습니다. JSON에서 route를 생략하는 경우 타입 데이터에서도 생략하십시오.
PlaceOrderReduceOnly
감소 전용(reduce-only) 주문(JSON 본문의 "reduce_only": true)은 별도의 구조체에 서명합니다. 이 주문에는 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여야 하며 요청 본문과 일치해야 합니다. 서명된 route가 없는 감소 전용 주문은 missing_field로 거부됩니다.
ReplaceOrder
PUT /order와 PUT /bulk_order의 각 항목은 ReplaceOrder에 서명합니다. 이 구조체에는 route 필드가 없으며, JSON 본문의 route 키는 무시됩니다.
struct ReplaceOrder {
address wallet;
string orderId;
string symbol;
string side;
string size;
string price;
string tif;
string clientId;
uint64 nonce;
}
orderId는 정정 대상 주문의 10진수 문자열입니다. 감소 전용 정정 주문("reduce_only": true)은 ReplaceOrderReduceOnly에 서명하며, 이 구조체는 clientId와 nonce 사이에 bool reduceOnly를 추가합니다. TypeScript SDK는 두 타입 맵을 모두 제공합니다.
CancelOrder
구조체:
struct CancelOrder {
address wallet;
string orderId;
uint64 nonce;
}
필드 요건:
wallet: 지갑 주소orderId: 문자열 형식의 주문 ID(예:"123")nonce: 고유 논스
CancelOrderByClientId
구조체:
struct CancelOrderByClientId {
address wallet;
string clientId;
uint64 nonce;
}
필드 요건:
wallet: 지갑 주소clientId: 클라이언트 주문 ID(문자열)nonce: 고유 논스
ApproveAgent
구조체:
struct ApproveAgent {
address agent;
uint64 nonce;
}
필드 요건:
agent: 권한을 부여할 에이전트 지갑 주소nonce: 고유 논스
참고: 에이전트에게 권한을 부여하는 지갑은 복원된 서명으로부터 도출됩니다.
RevokeAgent
구조체:
struct RevokeAgent {
address agent;
uint64 nonce;
}
필드 요건:
agent: 권한을 철회할 에이전트 지갑 주소nonce: 고유 논스
RevokeAllAgents
DELETE /revoke-all-agents는 서명한 지갑의 모든 에이전트를 제거합니다.
struct RevokeAllAgents {
uint64 nonce;
}
리스크 시뮬레이션
POST /risk/simulate/portfolio와 POST /risk/simulate/orders는 공개된 읽기 전용 엔진 권한 요청입니다. 서명, 인증, 논스가 필요하지 않습니다. 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 가용성, 매칭, 논스 접수, 향후 거래 중단 상태는 시뮬레이션되지 않습니다. 엔진 상태가 진행된 후의 실제 주문은 결과가 다를 수 있습니다.
서명 절차
1단계: 메시지 구성
route를 명시한 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
};
논스 관리
요건:
- 논스는 밀리초 단위 서버 시간 기준으로 2일 전부터 1일 후 사이의 범위 안에 엄격히 들어와야 하므로, 현재 Unix 시간을 밀리초 단위로 사용하십시오.
1이나123같은 작은 카운터 값은 거부됩니다. - 논스는 서명 키별로 추적됩니다. 주문과 취소의 경우 에이전트가 서명하면 에이전트 키 기준이며, 에이전트 승인 및 철회의 경우 소유자 지갑 기준입니다.
- 논스는 재사용할 수 없습니다. 주문 논스는
POST /order, 일괄 주문, WebSocketPlaceOrder간에 공유되므로 한 전송 경로에서 사용한 논스를 다른 경로에서 재사용할 수 없습니다. - 논스 사이의 간격은 허용됩니다. 엔진은 서명자별로 가장 최근 논스 100개를 보관하며, 100개가 채워진 이후에는 새 논스가 보관 중인 가장 작은 값보다 커야 합니다.
권장 사항:
- 밀리초 epoch 시간으로 초기값을 설정하고 서명자별 마지막 논스를 영구 저장한 뒤
max(now_ms, last + 1)을 반환하십시오 - 동일한 키로 서명하는 모든 프로세스가 하나의 할당기를 공유하도록 하십시오
- 요청이 실패하면 새 논스로 다시 서명하십시오
에이전트 권한 부여
에이전트 지갑으로 서명하는 경우:
-
에이전트 승인(최초 1회):
POST /approve-agent{"agent": "0x...","nonce": 1791273600000,"signature": "0x..." # Signed by wallet owner} -
에이전트 지갑으로 주문 서명:
- 에이전트 지갑으로
PlaceOrder/CancelOrder메시지에 서명합니다 wallet필드에는 거래 지갑 주소를 설정합니다- 미들웨어가 해당 지갑에 대한 에이전트의 권한 여부를 검증합니다
- 에이전트 지갑으로
에이전트 권한 부여에 대한 자세한 내용은 에이전트 인증을 참조하십시오.
퍼프 주문
퍼프 주문은 위의 Hypercall 도메인을 사용하지 않습니다. 퍼프 주문은 Hypercall 계정에 등록된 API 지갑이 HypercallApiSign 도메인(체인 ID 999, 검증 컨트랙트 주소 0)에서 HLOrder로 서명합니다. 구조체와 단위는 EIP-712 서명을 참조하십시오.
TEE 경로는 API에서 선택적으로 사용하며, TEE 엔드포인트를 호출하는 방식으로 선택합니다. Hypercall 앱에서는 기본값입니다. 이 경로를 사용하려면 서명한 주문을 account, nonce, action, signature와 함께 POST /v1/tee-perp/orders/submit으로 제출하십시오. Hypercall이 증거금 검사를 수행한 다음, 보안 엔클레이브가 계정을 대신해 네이티브 Hyperliquid 주문에 서명하고 전송합니다. 모든 퍼프 주문에는 수수료 0%의 Hypercall 빌더 코드가 포함되므로, 계정 관리자는 첫 주문 전에 해당 빌더를 한 번 승인해야 합니다(앱의 거래 활성화 단계). 승인하지 않으면 주문이 builder_not_approved로 거부됩니다. 흐름은 퍼프를, 요청 및 응답 스키마는 API 레퍼런스를 참조하십시오.
Hyperliquid 자체의 Exchange 도메인(체인 ID 1337, MessagePack Agent 페이로드)은 Hypercall API에서 허용되지 않습니다.
일반적인 오류
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에 서명했지만 본문에서 생략했거나 그 반대인 경우 - 잘못된 도메인(체인 ID, 이름, 버전, 검증 컨트랙트)
- 요청에 맞지 않는 구조체(예:
PUT /order에ReplaceOrder대신PlaceOrder사용) - 서명자가 실제로 지갑도 아니고 승인된 에이전트도 아닌 경우.
POST /approve-agent로 에이전트를 승인하거나 지갑으로 직접 서명하십시오
메시지에 알 수 없는 서명자 주소가 표시된다면 원인 1~4에 해당합니다.
signature_verification_failed (HTTP 400)
원인: 서명을 아예 디코딩하거나 복원할 수 없는 경우입니다. 예를 들어 길이가 잘못되었거나 16진수 형식이 유효하지 않은 경우입니다.
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"이거나,reason과 함께"REJECTED"입니다 - 요청이 알 수 없는 서명자 주소와 함께 HTTP 401
unauthorized로 실패하거나 HTTP 400signature_verification_failed로 실패하면 다음을 확인하십시오.price와size의 문자열 형식- 도메인 파라미터(특히
chainId) route와nonce를 포함한 모든 서명 필드가 본문과 일치하는지 여부
보안 고려 사항
- 개인 키를 절대 노출하지 마십시오: 하드웨어 지갑이나 안전한 키 관리 수단을 사용하십시오
- 논스 관리: 서명 키별로 영구 저장되는 밀리초 타임스탬프 논스를 사용하십시오
- 에이전트 권한 부여:
GET /authorized-agents로 권한이 부여된 에이전트를 정기적으로 점검하십시오 - 문자열 인코딩: 서명과 요청 모두에서
price와size가 문자열인지 확인하십시오
참고 자료
- EIP-712: https://eips.ethereum.org/EIPS/eip-712
- 구현: 서명 복원 및 미들웨어 스택에서 처리합니다.