Skip to content

Commit 9c1af99

Browse files
authored
Merge pull request #5 from quiknode-labs/add-golang-examples-to-public-repo
golang examples
2 parents 9206c40 + ed4138d commit 9c1af99

16 files changed

Lines changed: 945 additions & 0 deletions

File tree

golang/client/client.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Package client provides a REST client for the Hyperliquid API (via QuickNode builder API).
2+
//
3+
// No SDK required -- just net/http + go-ethereum.
4+
package client
5+
6+
import (
7+
"bytes"
8+
"crypto/ecdsa"
9+
"encoding/hex"
10+
"encoding/json"
11+
"fmt"
12+
"io"
13+
"net/http"
14+
"os"
15+
"strconv"
16+
"strings"
17+
18+
"github.com/ethereum/go-ethereum/crypto"
19+
)
20+
21+
const (
22+
apiURL = "https://send.hyperliquidapi.com"
23+
hlInfoURL = "https://api.hyperliquid.xyz/info"
24+
)
25+
26+
var (
27+
privateKey *ecdsa.PrivateKey
28+
Address string
29+
httpClient = &http.Client{}
30+
)
31+
32+
func init() {
33+
pk := os.Getenv("PRIVATE_KEY")
34+
if pk == "" {
35+
fmt.Println("Set PRIVATE_KEY environment variable (hex, with or without 0x)")
36+
os.Exit(1)
37+
}
38+
39+
pk = strings.TrimPrefix(pk, "0x")
40+
key, err := crypto.HexToECDSA(pk)
41+
if err != nil {
42+
fmt.Printf("Invalid PRIVATE_KEY: %v\n", err)
43+
os.Exit(1)
44+
}
45+
46+
privateKey = key
47+
Address = crypto.PubkeyToAddress(key.PublicKey).Hex()
48+
fmt.Printf("Wallet: %s\n", Address)
49+
}
50+
51+
// Exchange sends a POST to /exchange -- build (no signature) or send (with signature).
52+
func Exchange(body map[string]interface{}) map[string]interface{} {
53+
jsonBody, _ := json.Marshal(body)
54+
resp, err := httpClient.Post(apiURL+"/exchange", "application/json", bytes.NewReader(jsonBody))
55+
if err != nil {
56+
fmt.Printf("HTTP request failed: %v\n", err)
57+
os.Exit(1)
58+
}
59+
defer resp.Body.Close()
60+
61+
raw, _ := io.ReadAll(resp.Body)
62+
var data map[string]interface{}
63+
if err := json.Unmarshal(raw, &data); err != nil {
64+
fmt.Printf("Invalid JSON response: %v\n", err)
65+
os.Exit(1)
66+
}
67+
68+
if errVal, ok := data["error"]; ok && errVal != nil {
69+
fmt.Printf("\nError (%d):\n", resp.StatusCode)
70+
fmt.Printf(" error: %v\n", data["error"])
71+
fmt.Printf(" message: %v\n", data["message"])
72+
if guidance, ok := data["guidance"]; ok && guidance != nil {
73+
fmt.Printf(" guidance: %v\n", guidance)
74+
}
75+
os.Exit(1)
76+
}
77+
78+
return data
79+
}
80+
81+
// GetApproval checks builder fee approval status via GET /approval?user=<addr>.
82+
func GetApproval(user string) map[string]interface{} {
83+
resp, err := httpClient.Get(fmt.Sprintf("%s/approval?user=%s", apiURL, user))
84+
if err != nil {
85+
fmt.Printf("HTTP request failed: %v\n", err)
86+
os.Exit(1)
87+
}
88+
defer resp.Body.Close()
89+
90+
var data map[string]interface{}
91+
json.NewDecoder(resp.Body).Decode(&data)
92+
return data
93+
}
94+
95+
// GetMarkets lists all available markets via GET /markets.
96+
func GetMarkets() map[string]interface{} {
97+
resp, err := httpClient.Get(apiURL + "/markets")
98+
if err != nil {
99+
fmt.Printf("HTTP request failed: %v\n", err)
100+
os.Exit(1)
101+
}
102+
defer resp.Body.Close()
103+
104+
var data map[string]interface{}
105+
json.NewDecoder(resp.Body).Decode(&data)
106+
return data
107+
}
108+
109+
// PostEndpoint sends a POST to a utility endpoint (e.g. /openOrders, /orderStatus).
110+
func PostEndpoint(path string, body map[string]interface{}) map[string]interface{} {
111+
jsonBody, _ := json.Marshal(body)
112+
resp, err := httpClient.Post(apiURL+path, "application/json", bytes.NewReader(jsonBody))
113+
if err != nil {
114+
fmt.Printf("HTTP request failed: %v\n", err)
115+
os.Exit(1)
116+
}
117+
defer resp.Body.Close()
118+
119+
var data map[string]interface{}
120+
json.NewDecoder(resp.Body).Decode(&data)
121+
return data
122+
}
123+
124+
// SignHash signs a 32-byte hash and returns {r, s, v}.
125+
func SignHash(hashHex string) map[string]interface{} {
126+
hashHex = strings.TrimPrefix(hashHex, "0x")
127+
hashBytes, err := hex.DecodeString(hashHex)
128+
if err != nil {
129+
fmt.Printf("Invalid hash hex: %v\n", err)
130+
os.Exit(1)
131+
}
132+
133+
sig, err := crypto.Sign(hashBytes, privateKey)
134+
if err != nil {
135+
fmt.Printf("Signing failed: %v\n", err)
136+
os.Exit(1)
137+
}
138+
139+
// sig is 65 bytes: R (32) || S (32) || V (1)
140+
r := hex.EncodeToString(sig[:32])
141+
s := hex.EncodeToString(sig[32:64])
142+
v := int(sig[64]) + 27
143+
144+
return map[string]interface{}{
145+
"r": "0x" + r,
146+
"s": "0x" + s,
147+
"v": v,
148+
}
149+
}
150+
151+
// GetMid gets the current mid price for a coin from Hyperliquid.
152+
func GetMid(coin string) float64 {
153+
body, _ := json.Marshal(map[string]string{"type": "allMids"})
154+
resp, err := httpClient.Post(hlInfoURL, "application/json", bytes.NewReader(body))
155+
if err != nil {
156+
return 0
157+
}
158+
defer resp.Body.Close()
159+
160+
var data map[string]interface{}
161+
json.NewDecoder(resp.Body).Decode(&data)
162+
163+
if val, ok := data[coin]; ok {
164+
if s, ok := val.(string); ok {
165+
f, err := strconv.ParseFloat(s, 64)
166+
if err == nil {
167+
return f
168+
}
169+
}
170+
}
171+
return 0
172+
}
173+
174+
// GetHip3Mid gets the mid price for a HIP-3 market (requires dex parameter).
175+
func GetHip3Mid(coin string) float64 {
176+
dex := strings.Split(coin, ":")[0]
177+
body, _ := json.Marshal(map[string]interface{}{"type": "allMids", "dex": dex})
178+
resp, err := httpClient.Post(hlInfoURL, "application/json", bytes.NewReader(body))
179+
if err != nil {
180+
return 0
181+
}
182+
defer resp.Body.Close()
183+
184+
var data map[string]interface{}
185+
json.NewDecoder(resp.Body).Decode(&data)
186+
187+
if val, ok := data[coin]; ok {
188+
if s, ok := val.(string); ok {
189+
f, err := strconv.ParseFloat(s, 64)
190+
if err == nil {
191+
return f
192+
}
193+
}
194+
}
195+
return 0
196+
}
197+
198+
// PrettyJSON marshals a value to indented JSON string.
199+
func PrettyJSON(v interface{}) string {
200+
b, err := json.MarshalIndent(v, "", " ")
201+
if err != nil {
202+
return fmt.Sprintf("%v", v)
203+
}
204+
return string(b)
205+
}

golang/examples/approve/main.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Approve builder fee (one-time setup).
2+
package main
3+
4+
import (
5+
"fmt"
6+
7+
"hyperliquid-api-examples/client"
8+
)
9+
10+
const maxFee = "1%"
11+
12+
func main() {
13+
res := client.Exchange(map[string]interface{}{
14+
"action": map[string]interface{}{"type": "approveBuilderFee", "maxFeeRate": maxFee},
15+
})
16+
sig := client.SignHash(res["hash"].(string))
17+
18+
client.Exchange(map[string]interface{}{
19+
"action": map[string]interface{}{"type": "approveBuilderFee", "maxFeeRate": maxFee},
20+
"nonce": res["nonce"],
21+
"signature": sig,
22+
})
23+
24+
fmt.Println("Builder fee approved.")
25+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Place a resting order (GTC, 3% below mid), then cancel it.
2+
package main
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"hyperliquid-api-examples/client"
9+
)
10+
11+
const coin = "BTC"
12+
13+
func main() {
14+
mid := client.GetMid(coin)
15+
if mid == 0 {
16+
fmt.Printf("Could not fetch %s mid price\n", coin)
17+
os.Exit(1)
18+
}
19+
20+
sz := fmt.Sprintf("%.5f", 11.0/mid)
21+
restPx := fmt.Sprintf("%d", int(mid*0.97))
22+
23+
fmt.Printf("%s mid: $%.2f\n", coin, mid)
24+
fmt.Printf("Placing resting BUY %s @ %s (GTC, 3%% below mid)\n\n", sz, restPx)
25+
26+
res := client.Exchange(map[string]interface{}{
27+
"action": map[string]interface{}{
28+
"type": "order",
29+
"orders": []interface{}{
30+
map[string]interface{}{
31+
"asset": coin,
32+
"side": "buy",
33+
"price": restPx,
34+
"size": sz,
35+
"tif": "gtc",
36+
},
37+
},
38+
},
39+
})
40+
sig := client.SignHash(res["hash"].(string))
41+
42+
result := client.Exchange(map[string]interface{}{
43+
"action": res["action"],
44+
"nonce": res["nonce"],
45+
"signature": sig,
46+
})
47+
48+
exchangeResp := result["exchangeResponse"].(map[string]interface{})
49+
response := exchangeResp["response"].(map[string]interface{})
50+
data := response["data"].(map[string]interface{})
51+
statuses := data["statuses"].([]interface{})
52+
53+
var oid float64
54+
found := false
55+
for _, s := range statuses {
56+
sm := s.(map[string]interface{})
57+
if resting, ok := sm["resting"]; ok {
58+
rm := resting.(map[string]interface{})
59+
oid = rm["oid"].(float64)
60+
found = true
61+
break
62+
}
63+
}
64+
65+
if !found {
66+
fmt.Println("Could not extract OID from resting order")
67+
fmt.Println(client.PrettyJSON(exchangeResp))
68+
os.Exit(1)
69+
}
70+
71+
fmt.Printf("Order resting (OID: %v)\n", oid)
72+
fmt.Println("Cancelling...")
73+
fmt.Println()
74+
75+
cancelAction := map[string]interface{}{
76+
"type": "cancel",
77+
"cancels": []interface{}{
78+
map[string]interface{}{"a": coin, "o": oid},
79+
},
80+
}
81+
82+
res = client.Exchange(map[string]interface{}{"action": cancelAction})
83+
sig = client.SignHash(res["hash"].(string))
84+
85+
cancelResult := client.Exchange(map[string]interface{}{
86+
"action": cancelAction,
87+
"nonce": res["nonce"],
88+
"signature": sig,
89+
})
90+
91+
fmt.Println(client.PrettyJSON(cancelResult["exchangeResponse"]))
92+
fmt.Println("\nOrder cancelled.")
93+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Check builder fee approval status.
2+
package main
3+
4+
import (
5+
"fmt"
6+
7+
"hyperliquid-api-examples/client"
8+
)
9+
10+
func main() {
11+
res := client.GetApproval(client.Address)
12+
fmt.Println(client.PrettyJSON(res))
13+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Close a position -- worker queries your position and builds the counter-order.
2+
package main
3+
4+
import (
5+
"fmt"
6+
7+
"hyperliquid-api-examples/client"
8+
)
9+
10+
const coin = "HYPE"
11+
12+
func main() {
13+
fmt.Printf("Closing %s position for %s\n\n", coin, client.Address)
14+
15+
// Optional: custom slippage (default 3%, range 0.1%-10%)
16+
// res := client.Exchange(map[string]interface{}{"action": ..., "slippage": 0.05}) // 5% slippage
17+
18+
res := client.Exchange(map[string]interface{}{
19+
"action": map[string]interface{}{
20+
"type": "closePosition",
21+
"asset": coin,
22+
"user": client.Address,
23+
},
24+
})
25+
26+
if ctx, ok := res["closePositionContext"].(map[string]interface{}); ok {
27+
fmt.Printf("Position: %v %v\n", ctx["positionSize"], ctx["positionSide"])
28+
fmt.Printf("Close: %v %v @ %v\n", ctx["closeSide"], ctx["closeSize"], ctx["slippedPrice"])
29+
}
30+
31+
sig := client.SignHash(res["hash"].(string))
32+
33+
result := client.Exchange(map[string]interface{}{
34+
"action": res["action"],
35+
"nonce": res["nonce"],
36+
"signature": sig,
37+
})
38+
39+
fmt.Println(client.PrettyJSON(result["exchangeResponse"]))
40+
fmt.Println("\nPosition closed.")
41+
}

0 commit comments

Comments
 (0)