이 페이지는 자동 번역되었습니다. 영어 원문이 정본입니다. 영어로 읽기 →
메인 콘텐츠로 건너뛰기

인증 및 서명

쓰기 작업에 필요한 EIP-712 서명 요건입니다.

프로덕션 서명 도메인

프로덕션에서는 체인 ID 999를 사용합니다. 테스트넷은 Hypercall이 테스트넷 HYPE를 추가로 확보할 때까지 일시적으로 비활성화되어 있습니다.

개요​

모든 쓰기 작업에는 EIP-712 타입 데이터 서명이 필요합니다. 서명자는 다음 중 하나여야 합니다.

  1. 지갑 주소 자체(직접 서명), 또는
  2. 해당 지갑에 대해 권한을 부여받은 에이전트(에이전트 인증 참조)

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, 일괄 주문, WebSocket PlaceOrder 간에 공유되므로 한 전송 경로에서 사용한 논스를 다른 경로에서 재사용할 수 없습니다.
  • 논스 사이의 간격은 허용됩니다. 엔진은 서명자별로 가장 최근 논스 100개를 보관하며, 100개가 채워진 이후에는 새 논스가 보관 중인 가장 작은 값보다 커야 합니다.

권장 사항:

  • 밀리초 epoch 시간으로 초기값을 설정하고 서명자별 마지막 논스를 영구 저장한 뒤 max(now_ms, last + 1)을 반환하십시오
  • 동일한 키로 서명하는 모든 프로세스가 하나의 할당기를 공유하도록 하십시오
  • 요청이 실패하면 새 논스로 다시 서명하십시오

에이전트 권한 부여​

에이전트 지갑으로 서명하는 경우:

  1. 에이전트 승인(최초 1회):

    POST /approve-agent
    {
    "agent": "0x...",
    "nonce": 1791273600000,
    "signature": "0x..." # Signed by wallet owner
    }
  2. 에이전트 지갑으로 주문 서명:

    • 에이전트 지갑으로 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 복원은 항상 어떤 주소든 결과로 반환합니다. 서명한 내용과 전송한 내용이 조금이라도 다르면 복원된 주소는 사용자의 키가 아니게 되며, 요청은 서명 오류가 아니라 이 오류로 실패합니다.

원인:

  1. 서명과 전송 사이에 price 또는 size의 문자열 형식이 바뀐 경우(예: "100.0"과 "100")
  2. 서명한 필드가 JSON 본문과 다른 경우(route, clientId/client_id, nonce, tif), 또는 route에 서명했지만 본문에서 생략했거나 그 반대인 경우
  3. 잘못된 도메인(체인 ID, 이름, 버전, 검증 컨트랙트)
  4. 요청에 맞지 않는 구조체(예: PUT /order에 ReplaceOrder 대신 PlaceOrder 사용)
  5. 서명자가 실제로 지갑도 아니고 승인된 에이전트도 아닌 경우. 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);

서명 테스트​

서명 생성을 테스트하는 예시입니다.

  1. 직접 구현한 코드로 서명을 생성합니다
  2. POST /order로 테스트 주문을 전송합니다
  3. 응답을 확인합니다: HTTP 200과 함께 status가 "ACKED"이거나, reason과 함께 "REJECTED"입니다
  4. 요청이 알 수 없는 서명자 주소와 함께 HTTP 401 unauthorized로 실패하거나 HTTP 400 signature_verification_failed로 실패하면 다음을 확인하십시오.
    • price와 size의 문자열 형식
    • 도메인 파라미터(특히 chainId)
    • route와 nonce를 포함한 모든 서명 필드가 본문과 일치하는지 여부

보안 고려 사항​

  1. 개인 키를 절대 노출하지 마십시오: 하드웨어 지갑이나 안전한 키 관리 수단을 사용하십시오
  2. 논스 관리: 서명 키별로 영구 저장되는 밀리초 타임스탬프 논스를 사용하십시오
  3. 에이전트 권한 부여: GET /authorized-agents로 권한이 부여된 에이전트를 정기적으로 점검하십시오
  4. 문자열 인코딩: 서명과 요청 모두에서 price와 size가 문자열인지 확인하십시오

참고 자료​