feat: HL EVM signing via sonirico/go-hyperliquid SDK - testnet orders work
This commit is contained in:
@@ -7,6 +7,8 @@ BITGET_API_SECRET=e914e1...d620
|
||||
BITGET_PASSPHRASE=Yoyousoft007
|
||||
|
||||
# HyperLiquid testnet API (EVM wallet)
|
||||
# 主账号地址(主网有 $13.65)
|
||||
# Private key = API钱包私钥
|
||||
# Address = 主账号地址(有999 USDC现货)
|
||||
HL_PRIVATE_KEY=0xcb4e58b6c0921fd4aa8e49cd8a9f06b98b4e6b9cdf7c60c63ba1416856bb0b2a
|
||||
HL_ADDRESS=0x601B09803A46F640C9416C124A7DB858a615F5fF
|
||||
HL_ADDRESS=0xA834b6d3Fa1D8A55ea8e502685ef5cbD2b2D3343
|
||||
HL_API_ADDRESS=0x601B09803A46F640C9416C124A7DB858a615F5fF
|
||||
|
||||
+17
-2
@@ -263,5 +263,20 @@
|
||||
2026/05/04 16:14:51 main.go:165: [Status] 1956 prices / 1951 coins connected
|
||||
2026/05/04 16:15:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:15:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
5:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:15:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:16:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:16:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:17:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:17:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:18:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:18:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:19:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:19:37 connector.go:126: [HyperLiquid WS] Read error: websocket: close 1000 (normal): Expired
|
||||
2026/05/04 16:19:37 connector.go:52: [HyperLiquid WS] Connecting... (attempt 1)
|
||||
2026/05/04 16:19:38 hyperliquid.go:32: [HL WS] Connected
|
||||
2026/05/04 16:19:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:20:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:20:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:21:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:21:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
1:21 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
2026/05/04 16:21:51 main.go:165: [Status] 1958 prices / 1953 coins connected
|
||||
|
||||
@@ -21,7 +21,7 @@ type HyperLiquidTrade struct {
|
||||
exchange *hl.Exchange
|
||||
info *hl.Info
|
||||
privateKey *ecdsa.PrivateKey
|
||||
mainAddress string // main account address
|
||||
mainAddress string
|
||||
nonceMu sync.Mutex
|
||||
lastNonce int64
|
||||
configured bool
|
||||
@@ -32,7 +32,6 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL
|
||||
return &HyperLiquidTrade{}, nil
|
||||
}
|
||||
|
||||
// Parse ECDSA private key (supports 0x prefix)
|
||||
keyHex := strings.TrimPrefix(privateKeyHex, "0x")
|
||||
keyBytes, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
@@ -44,24 +43,20 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL
|
||||
return nil, fmt.Errorf("convert to ECDSA: %w", err)
|
||||
}
|
||||
|
||||
// Initialize SDK Info (auto-fetches meta + spotMeta)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil)
|
||||
|
||||
t := &HyperLiquidTrade{
|
||||
privateKey: privKey,
|
||||
mainAddress: mainAddress,
|
||||
info: info,
|
||||
configured: true,
|
||||
}
|
||||
|
||||
// Initialize SDK info (fetch meta for exchange)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
t.info = hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil)
|
||||
if t.info == nil {
|
||||
return nil, fmt.Errorf("failed to create HL Info")
|
||||
}
|
||||
|
||||
// Wait briefly for meta to be fetched (Info fetches meta in constructor)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Initialize exchange lazily on first order
|
||||
return t, nil
|
||||
}
|
||||
|
||||
@@ -69,53 +64,37 @@ func (h *HyperLiquidTrade) initExchange() error {
|
||||
if h.exchange != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h.configured {
|
||||
return fmt.Errorf("HyperLiquid not configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
meta, err := h.fetchMeta(ctx)
|
||||
meta, err := h.info.Meta(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch meta: %w", err)
|
||||
}
|
||||
|
||||
spotMeta, err := h.info.SpotMeta(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch spot meta: %w", err)
|
||||
}
|
||||
|
||||
h.exchange = hl.NewExchange(
|
||||
ctx,
|
||||
h.privateKey,
|
||||
hl.TestnetAPIURL,
|
||||
meta,
|
||||
"", // vault
|
||||
h.mainAddress, // account address
|
||||
nil, // spot meta
|
||||
nil, // perp dex
|
||||
"",
|
||||
h.mainAddress,
|
||||
spotMeta,
|
||||
nil,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) fetchMeta(ctx context.Context) (*hl.Meta, error) {
|
||||
// Use info endpoint to get meta
|
||||
resp, err := h.postInfo(ctx, map[string]any{"type": "meta"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse as Meta struct
|
||||
var meta hl.Meta
|
||||
if err := json.Unmarshal(resp, &meta); err != nil {
|
||||
return nil, fmt.Errorf("parse meta: %w", err)
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) postInfo(ctx context.Context, payload map[string]any) ([]byte, error) {
|
||||
// Simple HTTP POST to info endpoint
|
||||
return nil, fmt.Errorf("not implemented via SDK - use Info directly")
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) IsConfigured() bool {
|
||||
return h.configured
|
||||
}
|
||||
@@ -131,58 +110,42 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
|
||||
}
|
||||
|
||||
isBuy := side == "buy"
|
||||
|
||||
// Parse size from string to float64
|
||||
size, err := strconv.ParseFloat(sz, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse size %s: %w", sz, err)
|
||||
}
|
||||
|
||||
// Get current price for slippage calculation
|
||||
// Get current price for slippage
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
allMids, err := h.fetchAllMids(ctx)
|
||||
mids, err := h.info.AllMids(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch prices: %w", err)
|
||||
return "", fmt.Errorf("fetch mids: %w", err)
|
||||
}
|
||||
|
||||
priceStr, ok := allMids[coin]
|
||||
priceStr, ok := mids[coin]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("coin %s not found in allMids", coin)
|
||||
return "", fmt.Errorf("coin %s not found", coin)
|
||||
}
|
||||
midPx, _ := strconv.ParseFloat(priceStr, 64)
|
||||
|
||||
// Slippage price: buy = ask (mid * 1.02), sell = bid (mid * 0.98)
|
||||
var limitPx float64
|
||||
if isBuy {
|
||||
limitPx = midPx * 2.0 // aggressive buy
|
||||
} else {
|
||||
limitPx = midPx * 0.5 // aggressive sell
|
||||
// Aggressive IOC: buy above market, sell below
|
||||
limitPx := midPx * 2.0
|
||||
if !isBuy {
|
||||
limitPx = midPx * 0.5
|
||||
}
|
||||
|
||||
result, err := h.exchange.MarketOpen(ctx, coin, isBuy, size, &limitPx, 0.05, nil, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("market order: %w", err)
|
||||
return "", fmt.Errorf("market open: %w", err)
|
||||
}
|
||||
|
||||
// Serialize response
|
||||
// Marshal response
|
||||
respJSON, _ := json.Marshal(result)
|
||||
return string(respJSON), nil
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) fetchAllMids(ctx context.Context) (map[string]string, error) {
|
||||
// Try to get allMids via the SDK's Info if available
|
||||
if h.info != nil {
|
||||
mids, err := h.info.AllMids(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mids, nil
|
||||
}
|
||||
return nil, fmt.Errorf("info not initialized")
|
||||
}
|
||||
|
||||
// GetHLSize calculates size for a given USD amount on HyperLiquid.
|
||||
func GetHLSize(coin string, amountUSD, price float64) string {
|
||||
sz := amountUSD / price
|
||||
|
||||
Generated
-118
@@ -1,118 +0,0 @@
|
||||
{
|
||||
"name": "hl_helper",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0",
|
||||
"ethers": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adraffy/ens-normalize": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
|
||||
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw=="
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
||||
"integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
|
||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-js": {
|
||||
"version": "4.0.0-beta.5",
|
||||
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
|
||||
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q=="
|
||||
},
|
||||
"node_modules/ethers": {
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz",
|
||||
"integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/ethers-io/"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.buymeacoffee.com/ricmoo"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@adraffy/ens-normalize": "1.10.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.2",
|
||||
"@types/node": "22.7.5",
|
||||
"aes-js": "4.0.0-beta.5",
|
||||
"tslib": "2.7.0",
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"ethers": "^6.0.0",
|
||||
"@msgpack/msgpack": "^3.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
const https = require('https');
|
||||
|
||||
// Read args: [action_json, nonce, private_key, address, isMainnet]
|
||||
const [actionJson, nonceStr, privKey, address, isMainnetStr] = process.argv.slice(2);
|
||||
|
||||
if (!actionJson || !nonceStr || !privKey) {
|
||||
console.log(JSON.stringify({ error: "Usage: node hl_sign.js '<action_json>' <nonce> <priv_key> [address] [isMainnet]" }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const action = JSON.parse(actionJson);
|
||||
const nonce = parseInt(nonceStr);
|
||||
const isMainnet = isMainnetStr === 'true';
|
||||
|
||||
// ---- Build the EIP-712 typed data ----
|
||||
const { ethers } = require('ethers');
|
||||
|
||||
async function main() {
|
||||
const wallet = new ethers.Wallet(privKey);
|
||||
const apiAddr = wallet.address;
|
||||
|
||||
// 1. Normalize action (remove trailing zeros)
|
||||
let normalizedAction = JSON.parse(JSON.stringify(action));
|
||||
function normalize(obj) {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) normalize(item);
|
||||
} else {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if ((key === 'p' || key === 's') && typeof obj[key] === 'string') {
|
||||
obj[key] = obj[key].replace(/\.?0+$/, '').replace(/^-0$/, '0');
|
||||
} else {
|
||||
normalize(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
normalize(normalizedAction);
|
||||
|
||||
// 2. msgpack encode the action
|
||||
const { encode } = require('@msgpack/msgpack');
|
||||
const msgBytes = encode(normalizedAction);
|
||||
|
||||
// 3. Build hash: msgpack + nonce(8 bytes big endian) + flag(1 byte)
|
||||
const totalLen = msgBytes.length + 8 + 1;
|
||||
const data = new Uint8Array(totalLen);
|
||||
data.set(msgBytes, 0);
|
||||
const view = new DataView(data.buffer);
|
||||
view.setBigUint64(msgBytes.length, BigInt(nonce), false);
|
||||
view.setUint8(msgBytes.length + 8, 0); // no vault address
|
||||
|
||||
const hash = ethers.keccak256(data);
|
||||
|
||||
// 4. EIP-712 signing
|
||||
const source = isMainnet ? 'a' : 'b';
|
||||
const domain = {
|
||||
name: 'Exchange',
|
||||
version: '1',
|
||||
chainId: 1337,
|
||||
verifyingContract: '0x0000000000000000000000000000000000000000',
|
||||
};
|
||||
const types = {
|
||||
Agent: [
|
||||
{ name: 'source', type: 'string' },
|
||||
{ name: 'connectionId', type: 'bytes32' },
|
||||
],
|
||||
};
|
||||
const message = { source, connectionId: hash };
|
||||
|
||||
const signature = await wallet.signTypedData(domain, types, message);
|
||||
const sig = ethers.Signature.from(signature);
|
||||
|
||||
// 5. Build result
|
||||
const result = {
|
||||
action: action,
|
||||
nonce: nonce,
|
||||
signature: {
|
||||
r: sig.r,
|
||||
s: sig.s,
|
||||
v: sig.v,
|
||||
},
|
||||
};
|
||||
|
||||
// If address is provided and != wallet.address, it's the main account
|
||||
// The SDK automatically handles this via the accountAddress constructor param
|
||||
// For order placement via exchange API, we just send the signature
|
||||
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.log(JSON.stringify({ error: err.message }));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
const https = require('https');
|
||||
const { ethers } = require('ethers');
|
||||
const { encode } = require('@msgpack/msgpack');
|
||||
|
||||
const host = "api.hyperliquid-testnet.xyz";
|
||||
const privKey = "0xcb4e58b6c0921fd4aa8e49cd8a9f06b98b4e6b9cdf7c60c63ba1416856bb0b2a";
|
||||
const wallet = new ethers.Wallet(privKey);
|
||||
|
||||
async function api(method, path, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = JSON.stringify(payload);
|
||||
const opts = { hostname: host, path, method, headers: { 'Content-Type': 'application/json' } };
|
||||
const req = https.request(opts, res => {
|
||||
let body = '';
|
||||
res.on('data', c => body += c);
|
||||
res.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve(body); } });
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(data); req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function sign(action) {
|
||||
const nonce = Date.now() * 1000000 + Math.floor(Math.random() * 1000000);
|
||||
|
||||
// Normalize
|
||||
function normalize(obj) {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
if (Array.isArray(obj)) { for (const i of obj) normalize(i); }
|
||||
else { for (const k of Object.keys(obj)) {
|
||||
if ((k === 'p' || k === 's') && typeof obj[k] === 'string')
|
||||
obj[k] = obj[k].replace(/\.?0+$/, '').replace(/^-0$/, '0');
|
||||
else normalize(obj[k]);
|
||||
}}
|
||||
}
|
||||
const act = JSON.parse(JSON.stringify(action));
|
||||
normalize(act);
|
||||
|
||||
const msgBytes = encode(act);
|
||||
const buf = new Uint8Array(msgBytes.length + 8 + 1);
|
||||
buf.set(msgBytes, 0);
|
||||
new DataView(buf.buffer).setBigUint64(msgBytes.length, BigInt(nonce), false);
|
||||
buf[msgBytes.length + 8] = 0;
|
||||
const hash = ethers.keccak256(buf);
|
||||
|
||||
const sig = ethers.Signature.from(await wallet.signTypedData(
|
||||
{ name: 'Exchange', version: '1', chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000' },
|
||||
{ Agent: [{ name: 'source', type: 'string' }, { name: 'connectionId', type: 'bytes32' }] },
|
||||
{ source: 'b', connectionId: hash }
|
||||
));
|
||||
|
||||
return { action, nonce, signature: { r: sig.r, s: sig.s, v: sig.v } };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Get meta to find asset index
|
||||
const meta = await api('POST', '/info', { type: 'meta' });
|
||||
const btcIdx = meta.universe.findIndex(u => u.name === 'BTC');
|
||||
console.log(`BTC asset index: ${btcIdx}`);
|
||||
|
||||
// Get mid price for BTC
|
||||
const mids = await api('POST', '/info', { type: 'allMids' });
|
||||
const btcMid = parseFloat(mids['BTC']);
|
||||
console.log(`BTC mid: ${btcMid}`);
|
||||
|
||||
// Place a small market buy order (IOC, high limit price)
|
||||
const orderAction = {
|
||||
type: 'order',
|
||||
orders: [{
|
||||
a: btcIdx,
|
||||
b: true, // buy
|
||||
p: (btcMid * 2).toFixed(0),
|
||||
s: '0.001',
|
||||
r: false,
|
||||
t: { limit: { tif: 'Ioc' } }
|
||||
}],
|
||||
grouping: 'na'
|
||||
};
|
||||
|
||||
const signed = await sign(orderAction);
|
||||
console.log('\nSending order...');
|
||||
const result = await api('POST', '/exchange', signed);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,110 +0,0 @@
|
||||
const https = require('https');
|
||||
const { Wallet, keccak256, Signature, verifyTypedData } = require('ethers');
|
||||
const { encode } = require('@msgpack/msgpack');
|
||||
|
||||
const host = "api.hyperliquid-testnet.xyz";
|
||||
const privKey = "0xcb4e58b6c0921fd4aa8e49cd8a9f06b98b4e6b9cdf7c60c63ba1416856bb0b2a";
|
||||
const wallet = new Wallet(privKey);
|
||||
console.log("Wallet address:", wallet.address);
|
||||
|
||||
async function api(method, path, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = JSON.stringify(payload);
|
||||
const req = https.request({ hostname: host, path, method, headers: { 'Content-Type': 'application/json' } }, res => {
|
||||
let body = '';
|
||||
res.on('data', c => body += c);
|
||||
res.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve(body); } });
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(data); req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function floatToWire(x) {
|
||||
const rounded = x.toFixed(8);
|
||||
let normalized = rounded.replace(/\.?0+$/, '');
|
||||
if (normalized === '-0') normalized = '0';
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Get meta
|
||||
const meta = await api('POST', '/info', { type: 'meta' });
|
||||
const universe = meta.universe;
|
||||
const btc = universe.find(u => u.name === 'BTC');
|
||||
const btcIdx = universe.indexOf(btc);
|
||||
console.log(`BTC index: ${btcIdx}`);
|
||||
|
||||
const mids = await api('POST', '/info', { type: 'allMids' });
|
||||
const mid = parseFloat(mids['BTC']);
|
||||
const limitPx = mid * 2;
|
||||
const sz = 0.001;
|
||||
|
||||
// Build exactly like floatToWire does
|
||||
const pxStr = floatToWire(limitPx);
|
||||
const szStr = floatToWire(sz);
|
||||
console.log(`pxStr: ${pxStr}, szStr: ${szStr}`);
|
||||
|
||||
// Build order wire EXACTLY like SDK
|
||||
const orderWire = {
|
||||
a: btcIdx,
|
||||
b: true,
|
||||
p: pxStr,
|
||||
s: szStr,
|
||||
r: false,
|
||||
t: { limit: { tif: 'Ioc' } }
|
||||
};
|
||||
console.log("Order wire:", JSON.stringify(orderWire));
|
||||
|
||||
// Sign step by step
|
||||
const nonce = Date.now() * 1000000 + Math.floor(Math.random() * 1000000);
|
||||
|
||||
// Normalize trailing zeros
|
||||
function normalize(obj) {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
if (Array.isArray(obj)) { for (const i of obj) normalize(i); }
|
||||
else { for (const k of Object.keys(obj)) {
|
||||
if ((k === 'p' || k === 's') && typeof obj[k] === 'string')
|
||||
obj[k] = obj[k].replace(/\.?0+$/, '').replace(/^-0$/, '0');
|
||||
else normalize(obj[k]);
|
||||
}}
|
||||
}
|
||||
|
||||
const action = { type: 'order', orders: [JSON.parse(JSON.stringify(orderWire))], grouping: 'na' };
|
||||
normalize(action); // Apply to full action with orders array
|
||||
console.log("Normalized action:", JSON.stringify(action));
|
||||
|
||||
const msgBytes = encode(action);
|
||||
console.log(`Msgpack bytes: ${Buffer.from(msgBytes).toString('hex')}`);
|
||||
|
||||
const buf = new Uint8Array(msgBytes.length + 8 + 1);
|
||||
buf.set(msgBytes, 0);
|
||||
new DataView(buf.buffer).setBigUint64(msgBytes.length, BigInt(nonce), false);
|
||||
buf[msgBytes.length + 8] = 0;
|
||||
const hash = keccak256(buf);
|
||||
console.log(`Hash: ${hash}`);
|
||||
|
||||
// Sign and verify
|
||||
const sig = Signature.from(await wallet.signTypedData(
|
||||
{ name: 'Exchange', version: '1', chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000' },
|
||||
{ Agent: [{ name: 'source', type: 'string' }, { name: 'connectionId', type: 'bytes32' }] },
|
||||
{ source: 'b', connectionId: hash }
|
||||
));
|
||||
|
||||
// Verify the signature recovers to our wallet
|
||||
const domain = { name: 'Exchange', version: '1', chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000' };
|
||||
const types = { Agent: [{ name: 'source', type: 'string' }, { name: 'connectionId', type: 'bytes32' }] };
|
||||
const message = { source: 'b', connectionId: hash };
|
||||
const recoveredAddr = verifyTypedData(domain, types, message, sig);
|
||||
console.log(`Recovered address: ${recoveredAddr}`);
|
||||
console.log(`Wallet address: ${wallet.address.toLowerCase()}`);
|
||||
console.log(`Match: ${recoveredAddr.toLowerCase() === wallet.address.toLowerCase()}`);
|
||||
|
||||
// Send order
|
||||
const payload = { action, nonce, signature: { r: sig.r, s: sig.s, v: sig.v } };
|
||||
console.log('\nSending order...');
|
||||
const result = await api('POST', '/exchange', payload);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user