인증 및 서명
쓰기 작업을 위한 EIP-712 서명 요건입니다.
프로덕션에서는 체인 ID 999를 사용하세요. 테스트넷은 Hypercall이 추가 테스트넷 HYPE를 확보할 때까지 일시적으로 비활성화되어 있습니다.
개요
모든 쓰기 작업에는 EIP-712 타입 데이터 서명이 필요합니다. 서명자는 다음 중 하나여야 합니다:
- 지갑 주소 자체(직접 서명), 또는
- 해당 지갑에 대해 권한이 부여된 에이전트(에이전트 인증 참조)
EIP-712 도메인
도메인 구분자:
{
"name": "Hypercall",
"version": "1",
"chainId": 999,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
체인 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: 지갑 주소 (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", 또는"rfq_only"(포함 시 정확한 문자열 일치 필요)clientId: 클라이언트가 제공하는 주문 ID (문자열, 빈 문자열""가능)nonce: 재전송 공격 방지를 위한 고유 논스 (uint64)
중요: price와 size는 서명된 메시지와 JSON 요청 본문 모두에서 반드시 문자열이어야 합니다. 문자열 형식이 정확히 일치해야 합니다.
라우트 기본값: route는 최소 2026년 7월 4일까지 선택 사항입니다. JSON에서 생략하는 경우 타입 데이터에서도 생략하세요. 서버는 생략된 route를 best_execution으로 처리합니다. POST /order는 라우트 생략 시 지원 중단(deprecation) 헤더를 반환합니다. 클라이언트는 route를 전송하는 것이 좋습니다. 2026년 7월 4일 이전에는 필수가 되지 않지만, 이후에는 필수가 될 수 있습니다.
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: 고유 논스
서명 프로세스
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: 123
};
2단계: 도메인 정의
const domain = {
name: "Hypercall",
version: "1",
chainId: 999,
verifyingContract: "0x0000000000000000000000000000000000000000"
};
3단계: 타입 데이터 서명
ethers.js 사용 예:
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에 서명 시 사용한 것과 정확히 동일한 문자열 값을 사용해야 합니다:
{
"wallet": "0x1111111111111111111111111111111111111111",
"symbol": "BTC-20250131-100000-C",
"price": "100.0", // Same string as signed
"size": "0.1", // Same string as signed
"side": "Buy",
"tif": "gtc",
"route": "best_execution",
"client_id": "mm-1",
"nonce": 123,
"signature": "0x..."
}
논스 관리
요건:
- 논스는 지갑별로 반드시 고유해야 합니다
- 논스는 증가하는 값을 사용하는 것이 좋습니다 (재전송 공격 방지)
- 논스는 엄격한 단조 증가 여부를 검증하지 않습니다 (간격 허용)
모범 사례:
- 지갑별로 영구적인 카운터를 사용하세요
- 각 서명이 성공할 때마다 카운터를 증가시키세요
- 논스 간격을 유연하게 처리하세요 (예: 요청 실패 시 동일한 논스로 재시도)
에이전트 권한 부여
에이전트 지갑으로 서명하는 경우:
-
에이전트 승인 (1회):
POST /approve-agent{"agent": "0x...","nonce": 1,"signature": "0x..." # Signed by wallet owner} -
에이전트 지갑으로 주문 서명:
- 에이전트 지갑을 사용하여
PlaceOrder/CancelOrder메시지에 서명 wallet필드를 거래 지갑 주소로 설정- 미들웨어가 해당 지갑에 대해 에이전트 권한이 있는지 검증
- 에이전트 지갑을 사용하여
전체 에이전트 권한 부여에 대한 자세한 내용은 에이전트 인증을 참조하세요.
무기한 계약 주문 (Hyperliquid 호환)
무기한 계약 주문은 다른 EIP-712 도메인과 메시지 형식을 사용합니다 (Hyperliquid Core 호환성):
도메인:
{
"name": "Exchange",
"version": "1",
"chainId": 1337,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
메시지: MessagePack으로 인코딩된 주문 데이터가 포함된 Agent 구조체.
정확한 필드는 최신 서명 인코딩 가이드를 참조하세요.
일반적인 오류
"Signature verification failed"
원인:
price또는size를 문자열이 아닌 숫자로 전송- 서명과 전송 사이에 문자열 형식이 변경됨 (예:
"100.0"vs"100") - 잘못된 논스
- 잘못된 도메인 (체인 ID, name, version)
- 지갑에 대한 에이전트 권한 없음
"Unauthorized: signer not authorized for wallet"
원인: 서명자가 지갑 자체가 아니며, 권한이 부여된 에이전트도 아닙니다.
해결책: POST /approve-agent를 통해 에이전트를 승인하거나 지갑으로 직접 서명하세요.
구현 예시
Python (eth_account)
from eth_account import Account
from eth_account.messages import encode_structured_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": 123
}
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_structured_data(structured_msg)
signed = Account.sign_message(encoded, private_key)
signature = signed.signature.hex()
JavaScript (ethers.js)
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: 123
};
const signature = await signer._signTypedData(domain, types, message);
서명 테스트
서명 생성 테스트 예시:
- 구현체로 서명을 생성합니다
POST /order를 통해 테스트 주문을 전송합니다- 응답을 확인합니다:
status="ACKED"또는 사유가 포함된status="REJECTED" "signature_verification_failed"인 경우 다음을 확인하세요:price와size의 문자열 형식- 도메인 파라미터 (특히
chainId) - 논스의 정확성
보안 고려 사항
- 개인 키를 절대 노출하지 마세요: 하드웨어 지갑 또는 안전한 키 관리 솔루션을 사용하세요
- 논스 관리: 지갑별로 영구적이고 증가하는 논스를 사용하세요
- 에이전트 권한 부여:
GET /authorized-agents를 통해 권한이 부여된 에이전트를 정기적으로 점검하세요 - 문자열 인코딩: 서명과 요청 모두에서
price와size가 문자열인지 확인하세요
참고 자료
- EIP-712: https://eips.ethereum.org/EIPS/eip-712
- 구현: 서명 복구 및 미들웨어 스택에서 처리됩니다.