├── .gitattributes ├── .gitignore ├── .test.env ├── LICENSE ├── README.md └── hyperliquid ├── api.go ├── client.go ├── consts.go ├── convert.go ├── convert_test.go ├── exchange_msg_builders_test.go ├── exchange_service.go ├── exchange_signing.go ├── exchange_test.go ├── exchange_types.go ├── go.mod ├── go.sum ├── hyperliquid.go ├── hyperliquid_test.go ├── info_service.go ├── info_test.go ├── info_types.go ├── pk_manager.go ├── signature.go └── utils.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.test.env: -------------------------------------------------------------------------------- 1 | TEST_ADDRESS= 2 | TEST_PRIVATE_KEY= 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2024, Logarithm Labs 190 | Copyright (c) 2024, Open source contributors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-hyperliquid 2 | A golang SDK for Hyperliquid API. 3 | 4 | # API reference 5 | - [Hyperliquid](https://app.hyperliquid.xyz/) 6 | - [Hyperliquid API docs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api) 7 | - [Hyperliquid official Python SDK](https://github.com/hyperliquid-dex/hyperliquid-python-sdk) 8 | 9 | # How to install? 10 | ``` 11 | go get github.com/Logarithm-Labs/go-hyperliquid/hyperliquid 12 | ``` 13 | 14 | # Documentation 15 | 16 | [![GoDoc](https://godoc.org/github.com/adshao/go-binance?status.svg)](https://pkg.go.dev/github.com/Logarithm-Labs/go-hyperliquid/hyperliquid#section-documentation) 17 | 18 | 19 | # Quick start 20 | ``` 21 | package main 22 | 23 | import ( 24 | "log" 25 | 26 | "github.com/Logarithm-Labs/go-hyperliquid/hyperliquid" 27 | ) 28 | 29 | func main() { 30 | hyperliquidClient := hyperliquid.NewHyperliquid(&hyperliquid.HyperliquidClientConfig{ 31 | IsMainnet: true, 32 | AccountAddress: "0x12345", // Main address of the Hyperliquid account that you want to use 33 | PrivateKey: "abc1234", // Private key of the account or API private key from Hyperliquid 34 | }) 35 | 36 | // Get balances 37 | res, err := hyperliquidClient.GetAccountState() 38 | if err != nil { 39 | log.Print(err) 40 | } 41 | log.Printf("GetAccountState(): %+v", res) 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /hyperliquid/api.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // API implementation general error 9 | type APIError struct { 10 | Message string 11 | } 12 | 13 | func (e APIError) Error() string { 14 | return e.Message 15 | } 16 | 17 | // IAPIService is an interface for making requests to the API Service. 18 | // 19 | // It has a Request method that takes a path and a payload and returns a byte array and an error. 20 | // It has a debug method that takes a format string and args and returns nothing. 21 | // It has an Endpoint method that returns a string. 22 | type IAPIService interface { 23 | debug(format string, args ...interface{}) 24 | Request(path string, payload any) ([]byte, error) 25 | Endpoint() string 26 | KeyManager() *PKeyManager 27 | } 28 | 29 | // MakeUniversalRequest is a generic function that takes an 30 | // IAPIService and a request and returns a pointer to the result and an error. 31 | // It makes a request to the API Service and unmarshals the result into the result type T 32 | func MakeUniversalRequest[T any](api IAPIService, request any) (*T, error) { 33 | if api.Endpoint() == "" { 34 | return nil, APIError{Message: "Endpoint not set"} 35 | } 36 | if api == nil { 37 | return nil, APIError{Message: "API not set"} 38 | } 39 | if api.Endpoint() == "/exchange" && api.KeyManager() == nil { 40 | return nil, APIError{Message: "API key not set"} 41 | } 42 | 43 | response, err := api.Request(api.Endpoint(), request) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | var result T 49 | err = json.Unmarshal(response, &result) 50 | if err == nil { 51 | return &result, nil 52 | } 53 | 54 | var errResult map[string]interface{} 55 | err = json.Unmarshal(response, &errResult) 56 | if err != nil { 57 | api.debug("Error second json.Unmarshal: %s", err) 58 | return nil, APIError{Message: "Unexpected response"} 59 | } 60 | 61 | if errResult["status"] == "err" { 62 | return nil, APIError{Message: errResult["response"].(string)} 63 | } 64 | 65 | return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)} 66 | } 67 | -------------------------------------------------------------------------------- /hyperliquid/client.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // IClient is the interface that wraps the basic Requst method. 16 | // 17 | // Request method sends a POST request to the HyperLiquid API. 18 | // IsMainnet method returns true if the client is connected to the mainnet. 19 | // debug method enables debug mode. 20 | // SetPrivateKey method sets the private key for the client. 21 | type IClient interface { 22 | IAPIService 23 | SetPrivateKey(privateKey string) error 24 | SetAccountAddress(address string) 25 | AccountAddress() string 26 | SetDebugActive() 27 | IsMainnet() bool 28 | } 29 | 30 | // Client is the default implementation of the Client interface. 31 | // 32 | // It contains the base URL of the HyperLiquid API, the HTTP client, the debug mode, 33 | // the network type, the private key, and the logger. 34 | // The debug method prints the debug messages. 35 | type Client struct { 36 | baseUrl string // Base URL of the HyperLiquid API 37 | privateKey string // Private key for the client 38 | defualtAddress string // Default address for the client 39 | isMainnet bool // Network type 40 | Debug bool // Debug mode 41 | httpClient *http.Client // HTTP client 42 | keyManager *PKeyManager // Private key manager 43 | Logger *log.Logger // Logger for debug messages 44 | } 45 | 46 | // Returns the private key manager connected to the API. 47 | func (client *Client) KeyManager() *PKeyManager { 48 | return client.keyManager 49 | } 50 | 51 | // getAPIURL returns the API URL based on the network type. 52 | func getURL(isMainnet bool) string { 53 | if isMainnet { 54 | return MAINNET_API_URL 55 | } else { 56 | return TESTNET_API_URL 57 | } 58 | } 59 | 60 | // NewClient returns a new instance of the Client struct. 61 | func NewClient(isMainnet bool) *Client { 62 | logger := log.New() 63 | logger.SetFormatter(&log.TextFormatter{ 64 | FullTimestamp: true, 65 | PadLevelText: true, 66 | }) 67 | logger.SetOutput(os.Stdout) 68 | logger.SetLevel(log.DebugLevel) 69 | return &Client{ 70 | baseUrl: getURL(isMainnet), 71 | httpClient: http.DefaultClient, 72 | Debug: false, 73 | isMainnet: isMainnet, 74 | privateKey: "", 75 | defualtAddress: "", 76 | Logger: logger, 77 | keyManager: nil, 78 | } 79 | } 80 | 81 | // debug prints the debug messages. 82 | func (client *Client) debug(format string, v ...interface{}) { 83 | if client.Debug { 84 | client.Logger.Debugf(format, v...) 85 | } 86 | } 87 | 88 | // SetPrivateKey sets the private key for the client. 89 | func (client *Client) SetPrivateKey(privateKey string) error { 90 | if strings.HasPrefix(privateKey, "0x") { 91 | privateKey = strings.TrimPrefix(privateKey, "0x") // remove 0x prefix from private key 92 | } 93 | client.privateKey = privateKey 94 | var err error 95 | client.keyManager, err = NewPKeyManager(privateKey) 96 | return err 97 | } 98 | 99 | // Some methods need public address to gather info (from infoAPI). 100 | // In case you use PKeyManager from API section https://app.hyperliquid.xyz/API 101 | // Then you can use this method to set the address. 102 | func (client *Client) SetAccountAddress(address string) { 103 | client.defualtAddress = address 104 | } 105 | 106 | // Returns the public address connected to the API. 107 | func (client *Client) AccountAddress() string { 108 | return client.defualtAddress 109 | } 110 | 111 | // IsMainnet returns true if the client is connected to the mainnet. 112 | func (client *Client) IsMainnet() bool { 113 | return client.isMainnet 114 | } 115 | 116 | // SetDebugActive enables debug mode. 117 | func (client *Client) SetDebugActive() { 118 | client.Debug = true 119 | } 120 | 121 | // Request sends a POST request to the HyperLiquid API. 122 | func (client *Client) Request(endpoint string, payload any) ([]byte, error) { 123 | endpoint = strings.TrimPrefix(endpoint, "/") // Remove leading slash if present 124 | url := fmt.Sprintf("%s/%s", client.baseUrl, endpoint) 125 | client.debug("Request to %s", url) 126 | jsonPayload, err := json.Marshal(payload) 127 | if err != nil { 128 | client.debug("Error json.Marshal: %s", err) 129 | return nil, err 130 | } 131 | client.debug("Request payload: %s", string(jsonPayload)) 132 | payloadBytes, err := json.Marshal(payload) 133 | if err != nil { 134 | client.debug("Error json.Marshal: %s", err) 135 | return nil, err 136 | } 137 | request, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) 138 | if err != nil { 139 | client.debug("Error http.NewRequest: %s", err) 140 | return nil, err 141 | } 142 | request.Header.Set("Content-Type", "application/json") 143 | response, err := client.httpClient.Do(request) 144 | if err != nil { 145 | client.debug("Error client.httpClient.Do: %s", err) 146 | return nil, err 147 | } 148 | data, err := io.ReadAll(response.Body) 149 | if err != nil { 150 | return nil, err 151 | } 152 | defer func() { 153 | cerr := response.Body.Close() 154 | // Only overwrite the retured error if the original error was nil and an 155 | // error occurred while closing the body. 156 | if err == nil && cerr != nil { 157 | err = cerr 158 | } 159 | }() 160 | client.debug("response: %#v", response) 161 | client.debug("response body: %s", string(data)) 162 | client.debug("response status code: %d", response.StatusCode) 163 | if response.StatusCode >= http.StatusBadRequest { 164 | // If the status code is 400 or greater, return an error 165 | return nil, APIError{Message: fmt.Sprintf("HTTP %d: %s", response.StatusCode, data)} 166 | } 167 | return data, nil 168 | } 169 | -------------------------------------------------------------------------------- /hyperliquid/consts.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | const GLOBAL_DEBUG = false // Default debug that is used in all tests 4 | 5 | // API constants 6 | const MAINNET_API_URL = "https://api.hyperliquid.xyz" 7 | const TESTNET_API_URL = "https://api.hyperliquid-testnet.xyz" 8 | 9 | // Execution constants 10 | const DEFAULT_SLIPPAGE = 0.005 // 0.5% default slippage 11 | const SPOT_MAX_DECIMALS = 8 // Default decimals for spot 12 | const PERP_MAX_DECIMALS = 6 // Default decimals for perp 13 | var USDC_SZ_DECIMALS = 2 // Default decimals for usdc that is used for withdraw 14 | 15 | // Signing constants 16 | const HYPERLIQUID_CHAIN_ID = 1337 17 | const VERIFYING_CONTRACT = "0x0000000000000000000000000000000000000000" 18 | const ARBITRUM_CHAIN_ID = 42161 19 | const ARBITRUM_TESTNET_CHAIN_ID = 421614 20 | -------------------------------------------------------------------------------- /hyperliquid/convert.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "math" 8 | "math/big" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/ethereum/go-ethereum/common/hexutil" 13 | ) 14 | 15 | func ToTypedSig(r [32]byte, s [32]byte, v byte) RsvSignature { 16 | return RsvSignature{ 17 | R: hexutil.Encode(r[:]), 18 | S: hexutil.Encode(s[:]), 19 | V: v, 20 | } 21 | } 22 | 23 | func ArrayAppend(data []byte, toAppend []byte) []byte { 24 | return append(data, toAppend...) 25 | } 26 | 27 | func HexToBytes(addr string) []byte { 28 | if strings.HasPrefix(addr, "0x") { 29 | fAddr := strings.Replace(addr, "0x", "", 1) 30 | b, _ := hex.DecodeString(fAddr) 31 | return b 32 | } else { 33 | b, _ := hex.DecodeString(addr) 34 | return b 35 | } 36 | } 37 | 38 | func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAction { 39 | return PlaceOrderAction{ 40 | Type: "order", 41 | Grouping: grouping, 42 | Orders: orders, 43 | } 44 | } 45 | 46 | func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool) OrderWire { 47 | info := meta[req.Coin] 48 | var assetId, maxDecimals int 49 | if isSpot { 50 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids 51 | assetId = info.AssetId + 10000 52 | maxDecimals = SPOT_MAX_DECIMALS 53 | } else { 54 | assetId = info.AssetId 55 | maxDecimals = PERP_MAX_DECIMALS 56 | } 57 | return OrderWire{ 58 | Asset: assetId, 59 | IsBuy: req.IsBuy, 60 | LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals), 61 | SizePx: SizeToWire(req.Sz, info.SzDecimals), 62 | ReduceOnly: req.ReduceOnly, 63 | OrderType: OrderTypeToWire(req.OrderType), 64 | Cloid: req.Cloid, 65 | } 66 | } 67 | 68 | func ModifyOrderRequestToWire(req ModifyOrderRequest, meta map[string]AssetInfo, isSpot bool) ModifyOrderWire { 69 | info := meta[req.Coin] 70 | var assetId, maxDecimals int 71 | if isSpot { 72 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids 73 | assetId = info.AssetId + 10000 74 | maxDecimals = SPOT_MAX_DECIMALS 75 | } else { 76 | assetId = info.AssetId 77 | maxDecimals = PERP_MAX_DECIMALS 78 | } 79 | return ModifyOrderWire{ 80 | OrderId: req.OrderId, 81 | Order: OrderWire{ 82 | Asset: assetId, 83 | IsBuy: req.IsBuy, 84 | LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals), 85 | SizePx: SizeToWire(req.Sz, info.SzDecimals), 86 | ReduceOnly: req.ReduceOnly, 87 | OrderType: OrderTypeToWire(req.OrderType), 88 | }, 89 | } 90 | } 91 | 92 | func OrderTypeToWire(orderType OrderType) OrderTypeWire { 93 | if orderType.Limit != nil { 94 | return OrderTypeWire{ 95 | Limit: &LimitOrderType{ 96 | Tif: orderType.Limit.Tif, 97 | }, 98 | Trigger: nil, 99 | } 100 | } else if orderType.Trigger != nil { 101 | return OrderTypeWire{ 102 | Trigger: &TriggerOrderType{ 103 | TpSl: orderType.Trigger.TpSl, 104 | TriggerPx: orderType.Trigger.TriggerPx, 105 | IsMarket: orderType.Trigger.IsMarket, 106 | }, 107 | Limit: nil, 108 | } 109 | } 110 | return OrderTypeWire{} 111 | } 112 | 113 | // Format the float with custom decimal places, default is 6 (perp), 8 (spot). 114 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size 115 | func FloatToWire(x float64, maxDecimals int, szDecimals int) string { 116 | bigf := big.NewFloat(x) 117 | var maxDecSz uint 118 | intPart, _ := bigf.Int64() 119 | intSize := len(strconv.FormatInt(intPart, 10)) 120 | if intSize >= maxDecimals { 121 | maxDecSz = 0 122 | } else { 123 | maxDecSz = uint(maxDecimals - intSize) 124 | } 125 | x, _ = bigf.Float64() 126 | rounded := fmt.Sprintf("%.*f", maxDecSz, x) 127 | if strings.Contains(rounded, ".") { 128 | for strings.HasSuffix(rounded, "0") { 129 | rounded = strings.TrimSuffix(rounded, "0") 130 | } 131 | } 132 | if strings.HasSuffix(rounded, ".") { 133 | rounded = strings.TrimSuffix(rounded, ".") 134 | } 135 | return rounded 136 | } 137 | 138 | // fastPow10 returns 10^exp as a float64. For our purposes exp is small. 139 | func pow10(exp int) float64 { 140 | var res float64 = 1 141 | for i := 0; i < exp; i++ { 142 | res *= 10 143 | } 144 | return res 145 | } 146 | 147 | // PriceToWire converts a price value to its string representation per Hyperliquid rules. 148 | // It enforces: 149 | // - At most 5 significant figures, 150 | // - And no more than (maxDecimals - szDecimals) decimal places. 151 | // 152 | // Integer prices are returned as is. 153 | func PriceToWire(x float64, maxDecimals, szDecimals int) string { 154 | // If the price is an integer, return it without decimals. 155 | if x == math.Trunc(x) { 156 | return strconv.FormatInt(int64(x), 10) 157 | } 158 | 159 | // Rule 1: The tick rule – maximum decimals allowed is (maxDecimals - szDecimals). 160 | allowedTick := maxDecimals - szDecimals 161 | 162 | // Rule 2: The significant figures rule – at most 5 significant digits. 163 | var allowedSig int 164 | if x >= 1 { 165 | // Count digits in the integer part. 166 | digits := int(math.Floor(math.Log10(x))) + 1 167 | allowedSig = 5 - digits 168 | if allowedSig < 0 { 169 | allowedSig = 0 170 | } 171 | } else { 172 | // For x < 1, determine the effective exponent. 173 | exponent := int(math.Ceil(-math.Log10(x))) 174 | allowedSig = 4 + exponent 175 | } 176 | 177 | // Final allowed decimals is the minimum of the tick rule and the significant figures rule. 178 | allowedDecimals := allowedTick 179 | if allowedSig < allowedDecimals { 180 | allowedDecimals = allowedSig 181 | } 182 | if allowedDecimals < 0 { 183 | allowedDecimals = 0 184 | } 185 | 186 | // Round the price to allowedDecimals decimals. 187 | factor := pow10(allowedDecimals) 188 | rounded := math.Round(x*factor) / factor 189 | 190 | // Format the number with fixed precision. 191 | s := strconv.FormatFloat(rounded, 'f', allowedDecimals, 64) 192 | // Only trim trailing zeros if the formatted string contains a decimal point. 193 | if strings.Contains(s, ".") { 194 | s = strings.TrimRight(s, "0") 195 | s = strings.TrimRight(s, ".") 196 | } 197 | return s 198 | } 199 | 200 | // SizeToWire converts a size value to its string representation, 201 | // rounding it to exactly szDecimals decimals. 202 | // Integer sizes are returned without decimals. 203 | func SizeToWire(x float64, szDecimals int) string { 204 | // Return integer sizes without decimals. 205 | if szDecimals == 0 { 206 | return strconv.FormatInt(int64(x), 10) 207 | } 208 | // Return integer sizes directly. 209 | if x == math.Trunc(x) { 210 | return strconv.FormatInt(int64(x), 10) 211 | } 212 | 213 | // Round the size value to szDecimals decimals. 214 | factor := pow10(szDecimals) 215 | rounded := math.Round(x*factor) / factor 216 | 217 | // Format with fixed precision then trim any trailing zeros and the decimal point. 218 | s := strconv.FormatFloat(rounded, 'f', szDecimals, 64) 219 | return strings.TrimRight(strings.TrimRight(s, "0"), ".") 220 | } 221 | 222 | // To sign raw messages via EIP-712 223 | func StructToMap(strct any) (res map[string]interface{}, err error) { 224 | a, err := json.Marshal(strct) 225 | if err != nil { 226 | return map[string]interface{}{}, err 227 | } 228 | json.Unmarshal(a, &res) 229 | return res, nil 230 | } 231 | -------------------------------------------------------------------------------- /hyperliquid/convert_test.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConvert_SizeToWire(t *testing.T) { 8 | testCases := []struct { 9 | name string 10 | input float64 11 | szDec int 12 | expected string 13 | }{ 14 | { 15 | name: "BTC Size", 16 | input: 0.1, 17 | szDec: 5, 18 | expected: "0.1", 19 | }, 20 | { 21 | name: "PNUT Size", 22 | input: 101.22, 23 | szDec: 1, 24 | expected: "101.2", 25 | }, 26 | { 27 | name: "ETH Size", 28 | input: 0.1, 29 | szDec: 4, 30 | expected: "0.1", 31 | }, 32 | { 33 | name: "ADA Size", 34 | input: 100.123456, 35 | szDec: 0, 36 | expected: "100", 37 | }, 38 | { 39 | name: "ETH Size", 40 | input: 1.0, 41 | szDec: 4, 42 | expected: "1", 43 | }, 44 | { 45 | name: "ETH Size", 46 | input: 10.0, 47 | szDec: 4, 48 | expected: "10", 49 | }, 50 | { 51 | name: "ETH Size", 52 | input: 0.0100, 53 | szDec: 4, 54 | expected: "0.01", 55 | }, 56 | { 57 | name: "ETH Size", 58 | input: 0.010000001, 59 | szDec: 4, 60 | expected: "0.01", 61 | }, 62 | } 63 | for _, tc := range testCases { 64 | t.Run(tc.name, func(t *testing.T) { 65 | res := SizeToWire(tc.input, tc.szDec) 66 | if res != tc.expected { 67 | t.Errorf("SizeToWire() = %v, want %v", res, tc.expected) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestConvert_PriceToWire(t *testing.T) { 74 | testCases := []struct { 75 | name string 76 | input float64 77 | maxDec int 78 | szDec int 79 | expected string 80 | }{ 81 | { 82 | name: "BTC Price", 83 | input: 105000, 84 | maxDec: 6, 85 | szDec: 5, 86 | expected: "105000", 87 | }, 88 | { 89 | name: "BTC Price", 90 | input: 105000.1234, 91 | maxDec: 6, 92 | szDec: 5, 93 | expected: "105000", 94 | }, 95 | { 96 | name: "BTC Price", 97 | input: 95001.123456, 98 | maxDec: 6, 99 | szDec: 5, 100 | expected: "95001", 101 | }, 102 | } 103 | for _, tc := range testCases { 104 | t.Run(tc.name, func(t *testing.T) { 105 | res := PriceToWire(tc.input, tc.maxDec, tc.szDec) 106 | if res != tc.expected { 107 | t.Errorf("PriceToWire() = %v, want %v", res, tc.expected) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /hyperliquid/exchange_msg_builders_test.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func GetEmptyExchangeAPI() *ExchangeAPI { 9 | exchangeAPI := NewExchangeAPI(true) 10 | if GLOBAL_DEBUG { 11 | exchangeAPI.SetDebugActive() 12 | } 13 | return exchangeAPI 14 | } 15 | 16 | func TestExchangeAPI_BuildOrder(t *testing.T) { 17 | exchangeAPI := GetEmptyExchangeAPI() 18 | // input params 19 | coin := "ETH" 20 | size := 0.1 21 | price := 2500.0 22 | 23 | isBuy := IsBuy(size) 24 | orderType := OrderType{ 25 | Limit: &LimitOrderType{ 26 | Tif: TifIoc, 27 | }, 28 | } 29 | orderRequest := OrderRequest{ 30 | Coin: coin, 31 | IsBuy: isBuy, 32 | Sz: math.Abs(size), 33 | LimitPx: price, 34 | OrderType: orderType, 35 | ReduceOnly: false, 36 | } 37 | res, err := exchangeAPI.BuildOrderEIP712(orderRequest, GroupingNa) 38 | if err != nil { 39 | t.Errorf("BuildOrder() error = %v", err) 40 | } 41 | t.Logf("BuildOrder() = %+v", res) 42 | } 43 | -------------------------------------------------------------------------------- /hyperliquid/exchange_service.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/ethereum/go-ethereum/signer/core/apitypes" 8 | ) 9 | 10 | // IExchangeAPI is an interface for the /exchange service. 11 | type IExchangeAPI interface { 12 | IClient 13 | 14 | // Open orders 15 | BulkOrders(requests []OrderRequest, grouping Grouping) (*OrderResponse, error) 16 | Order(request OrderRequest, grouping Grouping) (*OrderResponse, error) 17 | MarketOrder(coin string, size float64, slippage *float64, clientOID ...string) (*OrderResponse, error) 18 | LimitOrder(orderType string, coin string, size float64, px float64, isBuy bool, reduceOnly bool, clientOID ...string) (*OrderResponse, error) 19 | 20 | // Order management 21 | CancelOrderByOID(coin string, orderID int) (any, error) 22 | CancelOrderByCloid(coin string, clientOID string) (any, error) 23 | BulkCancelOrders(cancels []CancelOidWire) (any, error) 24 | CancelAllOrdersByCoin(coin string) (any, error) 25 | CancelAllOrders() (any, error) 26 | ClosePosition(coin string) (*OrderResponse, error) 27 | 28 | // Account management 29 | Withdraw(destination string, amount float64) (*WithdrawResponse, error) 30 | UpdateLeverage(coin string, isCross bool, leverage int) (any, error) 31 | } 32 | 33 | // Implement the IExchangeAPI interface. 34 | type ExchangeAPI struct { 35 | Client 36 | infoAPI *InfoAPI 37 | address string 38 | baseEndpoint string 39 | meta map[string]AssetInfo 40 | spotMeta map[string]AssetInfo 41 | } 42 | 43 | // NewExchangeAPI creates a new default ExchangeAPI. 44 | // Run SetPrivateKey() and SetAccountAddress() to set the private key and account address. 45 | func NewExchangeAPI(isMainnet bool) *ExchangeAPI { 46 | api := ExchangeAPI{ 47 | Client: *NewClient(isMainnet), 48 | baseEndpoint: "/exchange", 49 | infoAPI: NewInfoAPI(isMainnet), 50 | address: "", 51 | } 52 | // turn on debug mode if there is an error with /info service 53 | meta, err := api.infoAPI.BuildMetaMap() 54 | if err != nil { 55 | api.SetDebugActive() 56 | api.debug("Error building meta map: %s", err) 57 | } 58 | api.meta = meta 59 | 60 | spotMeta, err := api.infoAPI.BuildSpotMetaMap() 61 | if err != nil { 62 | api.SetDebugActive() 63 | api.debug("Error building spot meta map: %s", err) 64 | } 65 | api.spotMeta = spotMeta 66 | 67 | return &api 68 | } 69 | 70 | // 71 | // Helpers 72 | // 73 | 74 | func (api *ExchangeAPI) Endpoint() string { 75 | return api.baseEndpoint 76 | } 77 | 78 | // Helper function to calculate the slippage price based on the market price. 79 | func (api *ExchangeAPI) SlippagePrice(coin string, isBuy bool, slippage float64) float64 { 80 | marketPx, err := api.infoAPI.GetMartketPx(coin) 81 | if err != nil { 82 | api.debug("Error getting market price: %s", err) 83 | return 0.0 84 | } 85 | return CalculateSlippage(isBuy, marketPx, slippage) 86 | } 87 | 88 | // SlippagePriceSpot is a helper function to calculate the slippage price for a spot coin. 89 | func (api *ExchangeAPI) SlippagePriceSpot(coin string, isBuy bool, slippage float64) float64 { 90 | marketPx, err := api.infoAPI.GetSpotMarketPx(coin) 91 | if err != nil { 92 | api.debug("Error getting market price: %s", err) 93 | return 0.0 94 | } 95 | slippagePrice := CalculateSlippage(isBuy, marketPx, slippage) 96 | return slippagePrice 97 | } 98 | 99 | // Helper function to get the chain params based on the network type. 100 | func (api *ExchangeAPI) getChainParams() (string, string) { 101 | if api.IsMainnet() { 102 | return "0xa4b1", "Mainnet" 103 | } 104 | return "0x66eee", "Testnet" 105 | } 106 | 107 | // Build bulk orders EIP712 message 108 | func (api *ExchangeAPI) BuildBulkOrdersEIP712(requests []OrderRequest, grouping Grouping) (apitypes.TypedData, error) { 109 | var wires []OrderWire 110 | for _, req := range requests { 111 | wires = append(wires, OrderRequestToWire(req, api.meta, false)) 112 | } 113 | timestamp := GetNonce() 114 | action := OrderWiresToOrderAction(wires, grouping) 115 | srequest, err := api.BuildEIP712Message(action, timestamp) 116 | if err != nil { 117 | api.debug("Error building EIP712 message: %s", err) 118 | return apitypes.TypedData{}, err 119 | } 120 | return SignRequestToEIP712TypedData(srequest), nil 121 | } 122 | 123 | // Build order EIP712 message 124 | func (api *ExchangeAPI) BuildOrderEIP712(request OrderRequest, grouping Grouping) (apitypes.TypedData, error) { 125 | return api.BuildBulkOrdersEIP712([]OrderRequest{request}, grouping) 126 | } 127 | 128 | // 129 | // Base Methods 130 | // 131 | 132 | // Place orders in bulk 133 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order 134 | func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping, isSpot bool) (*OrderResponse, error) { 135 | var wires []OrderWire 136 | var meta map[string]AssetInfo 137 | if isSpot { 138 | meta = api.spotMeta 139 | } else { 140 | meta = api.meta 141 | } 142 | for _, req := range requests { 143 | wires = append(wires, OrderRequestToWire(req, meta, isSpot)) 144 | } 145 | timestamp := GetNonce() 146 | action := OrderWiresToOrderAction(wires, grouping) 147 | v, r, s, err := api.SignL1Action(action, timestamp) 148 | if err != nil { 149 | api.debug("Error signing L1 action: %s", err) 150 | return nil, err 151 | } 152 | request := ExchangeRequest{ 153 | Action: action, 154 | Nonce: timestamp, 155 | Signature: ToTypedSig(r, s, v), 156 | VaultAddress: nil, 157 | } 158 | return MakeUniversalRequest[OrderResponse](api, request) 159 | } 160 | 161 | // Cancel order(s) 162 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s 163 | func (api *ExchangeAPI) BulkCancelOrders(cancels []CancelOidWire) (*OrderResponse, error) { 164 | timestamp := GetNonce() 165 | action := CancelOidOrderAction{ 166 | Type: "cancel", 167 | Cancels: cancels, 168 | } 169 | v, r, s, err := api.SignL1Action(action, timestamp) 170 | if err != nil { 171 | api.debug("Error signing L1 action: %s", err) 172 | return nil, err 173 | } 174 | request := ExchangeRequest{ 175 | Action: action, 176 | Nonce: timestamp, 177 | Signature: ToTypedSig(r, s, v), 178 | VaultAddress: nil, 179 | } 180 | return MakeUniversalRequest[OrderResponse](api, request) 181 | } 182 | 183 | // Bulk modify orders 184 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders 185 | func (api *ExchangeAPI) BulkModifyOrders(modifyRequests []ModifyOrderRequest, isSpot bool) (*OrderResponse, error) { 186 | wires := []ModifyOrderWire{} 187 | 188 | for _, req := range modifyRequests { 189 | wires = append(wires, ModifyOrderRequestToWire(req, api.meta, isSpot)) 190 | } 191 | action := ModifyOrderAction{ 192 | Type: "batchModify", 193 | Modifies: wires, 194 | } 195 | 196 | timestamp := GetNonce() 197 | vVal, rVal, sVal, signErr := api.SignL1Action(action, timestamp) 198 | if signErr != nil { 199 | return nil, signErr 200 | } 201 | request := ExchangeRequest{ 202 | Action: action, 203 | Nonce: timestamp, 204 | Signature: ToTypedSig(rVal, sVal, vVal), 205 | VaultAddress: nil, 206 | } 207 | return MakeUniversalRequest[OrderResponse](api, request) 208 | } 209 | 210 | // Cancel exact order by Client Order Id 211 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#cancel-order-s-by-cloid 212 | func (api *ExchangeAPI) CancelOrderByCloid(coin string, clientOID string) (*OrderResponse, error) { 213 | timestamp := GetNonce() 214 | action := CancelCloidOrderAction{ 215 | Type: "cancelByCloid", 216 | Cancels: []CancelCloidWire{ 217 | { 218 | Asset: api.meta[coin].AssetId, 219 | Cloid: clientOID, 220 | }, 221 | }, 222 | } 223 | v, r, s, err := api.SignL1Action(action, timestamp) 224 | if err != nil { 225 | api.debug("Error signing L1 action: %s", err) 226 | return nil, err 227 | } 228 | request := ExchangeRequest{ 229 | Action: action, 230 | Nonce: timestamp, 231 | Signature: ToTypedSig(r, s, v), 232 | VaultAddress: nil, 233 | } 234 | return MakeUniversalRequest[OrderResponse](api, request) 235 | } 236 | 237 | // Update leverage for a coin 238 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#update-leverage 239 | func (api *ExchangeAPI) UpdateLeverage(coin string, isCross bool, leverage int) (*DefaultExchangeResponse, error) { 240 | timestamp := GetNonce() 241 | action := UpdateLeverageAction{ 242 | Type: "updateLeverage", 243 | Asset: api.meta[coin].AssetId, 244 | IsCross: isCross, 245 | Leverage: leverage, 246 | } 247 | v, r, s, err := api.SignL1Action(action, timestamp) 248 | if err != nil { 249 | api.debug("Error signing L1 action: %s", err) 250 | return nil, err 251 | } 252 | request := ExchangeRequest{ 253 | Action: action, 254 | Nonce: timestamp, 255 | Signature: ToTypedSig(r, s, v), 256 | VaultAddress: nil, 257 | } 258 | return MakeUniversalRequest[DefaultExchangeResponse](api, request) 259 | } 260 | 261 | // Initiate a withdraw request 262 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#initiate-a-withdrawal-request 263 | func (api *ExchangeAPI) Withdraw(destination string, amount float64) (*WithdrawResponse, error) { 264 | nonce := GetNonce() 265 | action := WithdrawAction{ 266 | Type: "withdraw3", 267 | Destination: destination, 268 | Amount: SizeToWire(amount, USDC_SZ_DECIMALS), 269 | Time: nonce, 270 | } 271 | signatureChainID, chainType := api.getChainParams() 272 | action.HyperliquidChain = chainType 273 | action.SignatureChainID = signatureChainID 274 | v, r, s, err := api.SignWithdrawAction(action) 275 | if err != nil { 276 | api.debug("Error signing withdraw action: %s", err) 277 | return nil, err 278 | } 279 | request := &ExchangeRequest{ 280 | Action: action, 281 | Nonce: nonce, 282 | Signature: ToTypedSig(r, s, v), 283 | VaultAddress: nil, 284 | } 285 | return MakeUniversalRequest[WithdrawResponse](api, request) 286 | } 287 | 288 | // 289 | // Connectors Methods 290 | // 291 | 292 | // Place single order 293 | func (api *ExchangeAPI) Order(request OrderRequest, grouping Grouping) (*OrderResponse, error) { 294 | return api.BulkOrders([]OrderRequest{request}, grouping, false) 295 | } 296 | 297 | // Open a market order. 298 | // Limit order with TIF=IOC and px=market price * (1 +- slippage). 299 | // Size determines the amount of the coin to buy/sell. 300 | // 301 | // MarketOrder("BTC", 0.1, nil) // Buy 0.1 BTC 302 | // MarketOrder("BTC", -0.1, nil) // Sell 0.1 BTC 303 | // MarketOrder("BTC", 0.1, &slippage) // Buy 0.1 BTC with slippage 304 | func (api *ExchangeAPI) MarketOrder(coin string, size float64, slippage *float64, clientOID ...string) (*OrderResponse, error) { 305 | slpg := GetSlippage(slippage) 306 | isBuy := IsBuy(size) 307 | finalPx := api.SlippagePrice(coin, isBuy, slpg) 308 | orderType := OrderType{ 309 | Limit: &LimitOrderType{ 310 | Tif: TifIoc, 311 | }, 312 | } 313 | orderRequest := OrderRequest{ 314 | Coin: coin, 315 | IsBuy: isBuy, 316 | Sz: math.Abs(size), 317 | LimitPx: finalPx, 318 | OrderType: orderType, 319 | ReduceOnly: false, 320 | } 321 | if len(clientOID) > 0 { 322 | orderRequest.Cloid = clientOID[0] 323 | } 324 | return api.Order(orderRequest, GroupingNa) 325 | } 326 | 327 | // MarketOrderSpot is a market order for a spot coin. 328 | // It is used to buy/sell a spot coin. 329 | // Limit order with TIF=IOC and px=market price * (1 +- slippage). 330 | // Size determines the amount of the coin to buy/sell. 331 | // 332 | // MarketOrderSpot("HYPE", 0.1, nil) // Buy 0.1 HYPE 333 | // MarketOrderSpot("HYPE", -0.1, nil) // Sell 0.1 HYPE 334 | // MarketOrderSpot("HYPE", 0.1, &slippage) // Buy 0.1 HYPE with slippage 335 | func (api *ExchangeAPI) MarketOrderSpot(coin string, size float64, slippage *float64) (*OrderResponse, error) { 336 | slpg := GetSlippage(slippage) 337 | isBuy := IsBuy(size) 338 | finalPx := api.SlippagePriceSpot(coin, isBuy, slpg) 339 | orderType := OrderType{ 340 | Limit: &LimitOrderType{ 341 | Tif: TifIoc, 342 | }, 343 | } 344 | orderRequest := OrderRequest{ 345 | Coin: coin, 346 | IsBuy: isBuy, 347 | Sz: math.Abs(size), 348 | LimitPx: finalPx, 349 | OrderType: orderType, 350 | ReduceOnly: false, 351 | } 352 | return api.OrderSpot(orderRequest, GroupingNa) 353 | } 354 | 355 | // Open a limit order. 356 | // Order type can be Gtc, Ioc, Alo. 357 | // Size determines the amount of the coin to buy/sell. 358 | // See the constants TifGtc, TifIoc, TifAlo. 359 | func (api *ExchangeAPI) LimitOrder(orderType string, coin string, size float64, px float64, reduceOnly bool, clientOID ...string) (*OrderResponse, error) { 360 | // check if the order type is valid 361 | if orderType != TifGtc && orderType != TifIoc && orderType != TifAlo { 362 | return nil, APIError{Message: fmt.Sprintf("Invalid order type: %s. Available types: %s, %s, %s", orderType, TifGtc, TifIoc, TifAlo)} 363 | } 364 | orderTypeZ := OrderType{ 365 | Limit: &LimitOrderType{ 366 | Tif: orderType, 367 | }, 368 | } 369 | orderRequest := OrderRequest{ 370 | Coin: coin, 371 | IsBuy: IsBuy(size), 372 | Sz: math.Abs(size), 373 | LimitPx: px, 374 | OrderType: orderTypeZ, 375 | ReduceOnly: reduceOnly, 376 | } 377 | if len(clientOID) > 0 { 378 | orderRequest.Cloid = clientOID[0] 379 | } 380 | return api.Order(orderRequest, GroupingNa) 381 | } 382 | 383 | // Close all positions for a given coin. They are closing with a market order. 384 | func (api *ExchangeAPI) ClosePosition(coin string) (*OrderResponse, error) { 385 | // Get all positions and find the one for the coin 386 | // Then just make MarketOpen with the reverse size 387 | state, err := api.infoAPI.GetUserState(api.AccountAddress()) 388 | if err != nil { 389 | api.debug("Error GetUserState: %s", err) 390 | return nil, err 391 | } 392 | positions := state.AssetPositions 393 | slippage := GetSlippage(nil) 394 | 395 | // Find the position for the coin 396 | for _, position := range positions { 397 | item := position.Position 398 | if coin != item.Coin { 399 | continue 400 | } 401 | size := item.Szi 402 | // reverse the position to close 403 | isBuy := !IsBuy(size) 404 | finalPx := api.SlippagePrice(coin, isBuy, slippage) 405 | orderType := OrderType{ 406 | Limit: &LimitOrderType{ 407 | Tif: "Ioc", 408 | }, 409 | } 410 | orderRequest := OrderRequest{ 411 | Coin: coin, 412 | IsBuy: isBuy, 413 | Sz: math.Abs(size), 414 | LimitPx: finalPx, 415 | OrderType: orderType, 416 | ReduceOnly: true, 417 | } 418 | return api.Order(orderRequest, GroupingNa) 419 | } 420 | return nil, APIError{Message: fmt.Sprintf("No position found for %s", coin)} 421 | } 422 | 423 | // OrderSpot places a spot order 424 | func (api *ExchangeAPI) OrderSpot(request OrderRequest, grouping Grouping) (*OrderResponse, error) { 425 | return api.BulkOrders([]OrderRequest{request}, grouping, true) 426 | } 427 | 428 | // Cancel exact order by OID 429 | func (api *ExchangeAPI) CancelOrderByOID(coin string, orderID int64) (*OrderResponse, error) { 430 | return api.BulkCancelOrders([]CancelOidWire{{Asset: api.meta[coin].AssetId, Oid: int(orderID)}}) 431 | } 432 | 433 | // Cancel all orders for a given coin 434 | func (api *ExchangeAPI) CancelAllOrdersByCoin(coin string) (*OrderResponse, error) { 435 | orders, err := api.infoAPI.GetOpenOrders(api.AccountAddress()) 436 | if err != nil { 437 | api.debug("Error getting orders: %s", err) 438 | return nil, err 439 | } 440 | var cancels []CancelOidWire 441 | for _, order := range *orders { 442 | if coin != order.Coin { 443 | continue 444 | } 445 | cancels = append(cancels, CancelOidWire{Asset: api.meta[coin].AssetId, Oid: int(order.Oid)}) 446 | } 447 | return api.BulkCancelOrders(cancels) 448 | } 449 | 450 | // Cancel all open orders 451 | func (api *ExchangeAPI) CancelAllOrders() (*OrderResponse, error) { 452 | orders, err := api.infoAPI.GetOpenOrders(api.AccountAddress()) 453 | if err != nil { 454 | api.debug("Error getting orders: %s", err) 455 | return nil, err 456 | } 457 | if len(*orders) == 0 { 458 | return nil, APIError{Message: "No open orders to cancel"} 459 | } 460 | var cancels []CancelOidWire 461 | for _, order := range *orders { 462 | cancels = append(cancels, CancelOidWire{Asset: api.meta[order.Coin].AssetId, Oid: int(order.Oid)}) 463 | } 464 | return api.BulkCancelOrders(cancels) 465 | } 466 | -------------------------------------------------------------------------------- /hyperliquid/exchange_signing.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/signer/core/apitypes" 5 | ) 6 | 7 | func (api *ExchangeAPI) Sign(request *SignRequest) (byte, [32]byte, [32]byte, error) { 8 | signer := NewSigner(api.keyManager) 9 | v, r, s, err := signer.Sign(request) 10 | if err != nil { 11 | api.debug("Error SignInner: %s", err) 12 | return 0, [32]byte{}, [32]byte{}, err 13 | } 14 | return v, r, s, nil 15 | } 16 | 17 | func (api *ExchangeAPI) SignUserSignableAction(action any, payloadTypes []apitypes.Type, primaryType string) (byte, [32]byte, [32]byte, error) { 18 | message, err := StructToMap(action) 19 | if err != nil { 20 | return 0, [32]byte{}, [32]byte{}, err 21 | } 22 | // Remove unnecessary fields for signing 23 | delete(message, "type") 24 | delete(message, "signatureChainId") 25 | 26 | signRequest := &SignRequest{ 27 | DomainName: "HyperliquidSignTransaction", 28 | PrimaryType: primaryType, 29 | DType: payloadTypes, 30 | DTypeMsg: message, 31 | IsMainNet: api.IsMainnet(), 32 | } 33 | return api.Sign(signRequest) 34 | } 35 | 36 | func (api *ExchangeAPI) SignL1Action(action any, timestamp uint64) (byte, [32]byte, [32]byte, error) { 37 | srequest, err := api.BuildEIP712Message(action, timestamp) 38 | if err != nil { 39 | api.debug("Error building EIP712 message: %s", err) 40 | return 0, [32]byte{}, [32]byte{}, err 41 | } 42 | return api.Sign(srequest) 43 | } 44 | 45 | func (api *ExchangeAPI) BuildEIP712Message(action any, timestamp uint64) (*SignRequest, error) { 46 | hash, err := buildActionHash(action, "", timestamp) 47 | if err != nil { 48 | return nil, err 49 | } 50 | message := buildMessage(hash.Bytes(), api.IsMainnet()) 51 | srequest := &SignRequest{ 52 | DomainName: "Exchange", 53 | PrimaryType: "Agent", 54 | DType: []apitypes.Type{ 55 | { 56 | Name: "source", 57 | Type: "string", 58 | }, 59 | { 60 | Name: "connectionId", 61 | Type: "bytes32", 62 | }, 63 | }, 64 | DTypeMsg: message, 65 | IsMainNet: api.IsMainnet(), 66 | } 67 | return srequest, nil 68 | } 69 | 70 | func (api *ExchangeAPI) SignWithdrawAction(action WithdrawAction) (byte, [32]byte, [32]byte, error) { 71 | types := []apitypes.Type{ 72 | { 73 | Name: "hyperliquidChain", 74 | Type: "string", 75 | }, 76 | { 77 | Name: "destination", 78 | Type: "string", 79 | }, 80 | { 81 | Name: "amount", 82 | Type: "string", 83 | }, 84 | { 85 | Name: "time", 86 | Type: "uint64", 87 | }, 88 | } 89 | return api.SignUserSignableAction(action, types, "HyperliquidTransaction:Withdraw") 90 | } 91 | -------------------------------------------------------------------------------- /hyperliquid/exchange_test.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "log" 5 | "math" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func GetExchangeAPI() *ExchangeAPI { 12 | exchangeAPI := NewExchangeAPI(false) 13 | if GLOBAL_DEBUG { 14 | exchangeAPI.SetDebugActive() 15 | } 16 | TEST_ADDRESS := os.Getenv("TEST_ADDRESS") 17 | TEST_PRIVATE_KEY := os.Getenv("TEST_PRIVATE_KEY") 18 | err := exchangeAPI.SetPrivateKey(TEST_PRIVATE_KEY) 19 | if err != nil { 20 | panic(err) 21 | } 22 | exchangeAPI.SetAccountAddress(TEST_ADDRESS) 23 | return exchangeAPI 24 | } 25 | 26 | func TestExchangeAPI_Endpoint(t *testing.T) { 27 | exchangeAPI := GetExchangeAPI() 28 | res := exchangeAPI.Endpoint() 29 | if res != "/exchange" { 30 | t.Errorf("Endpoint() = %v, want %v", res, "/exchange") 31 | } 32 | } 33 | 34 | func TestExchangeAPI_AccountAddress(t *testing.T) { 35 | exchangeAPI := GetExchangeAPI() 36 | res := exchangeAPI.AccountAddress() 37 | TARGET_ADDRESS := os.Getenv("TEST_ADDRESS") 38 | if res != TARGET_ADDRESS { 39 | t.Errorf("AccountAddress() = %v, want %v", res, TARGET_ADDRESS) 40 | } 41 | } 42 | 43 | func TestExchangeAPI_isMainnet(t *testing.T) { 44 | exchangeAPI := GetExchangeAPI() 45 | res := exchangeAPI.IsMainnet() 46 | if res != false { 47 | t.Errorf("isMainnet() = %v, want %v", res, true) 48 | } 49 | } 50 | 51 | func TestExchageAPI_TestMetaIsNotEmpty(t *testing.T) { 52 | exchangeAPI := GetExchangeAPI() 53 | meta := exchangeAPI.meta 54 | if meta == nil { 55 | t.Errorf("Meta() = %v, want not nil", meta) 56 | } 57 | if len(meta) == 0 { 58 | t.Errorf("Meta() = %v, want not empty", meta) 59 | } 60 | t.Logf("Meta() = %+v", meta) 61 | } 62 | 63 | func TestExchangeAPI_UpdateLeverage(t *testing.T) { 64 | exchangeAPI := GetExchangeAPI() 65 | _, err := exchangeAPI.UpdateLeverage("ETH", true, 20) 66 | if err != nil { 67 | t.Errorf("UpdateLeverage() error = %v", err) 68 | } 69 | // Set incorrect leverage 2000 70 | _, err = exchangeAPI.UpdateLeverage("ETH", true, 2000) 71 | if err == nil { 72 | t.Errorf("UpdateLeverage() error = %v", err) 73 | } else if err.Error() != "Invalid leverage value" { 74 | t.Errorf("UpdateLeverage() error = %v expected Invalid leverage value", err) 75 | } 76 | t.Logf("UpdateLeverage() = %v", err) 77 | } 78 | 79 | func TestExchangeAPI_MarketOpen(t *testing.T) { 80 | exchangeAPI := GetExchangeAPI() 81 | size := -0.01 82 | coin := "ETH" 83 | res, err := exchangeAPI.MarketOrder(coin, size, nil) 84 | if err != nil { 85 | t.Errorf("MakeOpen() error = %v", err) 86 | } 87 | t.Logf("MakeOpen() = %v", res) 88 | avgPrice := res.Response.Data.Statuses[0].Filled.AvgPx 89 | if avgPrice == 0 { 90 | t.Errorf("res.Response.Data.Statuses[0].Filled.AvgPx = %v", avgPrice) 91 | } 92 | totalSize := res.Response.Data.Statuses[0].Filled.TotalSz 93 | if totalSize != math.Abs(size) { 94 | t.Errorf("res.Response.Data.Statuses[0].Filled.TotalSz = %v", totalSize) 95 | } 96 | time.Sleep(2 * time.Second) // wait to execute order 97 | accountState, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress()) 98 | if err != nil { 99 | t.Errorf("GetAccountState() error = %v", err) 100 | } 101 | positionOpened := false 102 | positionCorrect := false 103 | for _, position := range accountState.AssetPositions { 104 | if position.Position.Coin == coin { 105 | positionOpened = true 106 | } 107 | if position.Position.Coin == coin && position.Position.Szi == size { 108 | positionCorrect = true 109 | } 110 | } 111 | if !positionOpened { 112 | t.Errorf("Position not found: %v", accountState.AssetPositions) 113 | } 114 | if !positionCorrect { 115 | t.Errorf("Position not correct: %v", accountState.AssetPositions) 116 | } 117 | t.Logf("GetAccountState() = %v", accountState) 118 | time.Sleep(5 * time.Second) // wait to execute order 119 | } 120 | 121 | func TestExchangeAPI_MarketClose(t *testing.T) { 122 | exchangeAPI := GetExchangeAPI() 123 | res, err := exchangeAPI.ClosePosition("ETH") 124 | if err != nil { 125 | t.Errorf("MakeClose() error = %v", err) 126 | } 127 | t.Logf("MakeClose() = %v", res) 128 | } 129 | 130 | func TestExchangeAPI_LimitOrder(t *testing.T) { 131 | exchangeAPI := GetExchangeAPI() 132 | size := 100.1234 133 | coin := "PNUT" 134 | px := 0.154956 135 | res, err := exchangeAPI.LimitOrder(TifGtc, coin, size, px, false) 136 | if err != nil { 137 | t.Errorf("MakeLimit() error = %v", err) 138 | } 139 | t.Logf("MakeLimit() = %v", res) 140 | } 141 | 142 | func TestExchangeAPI_CancelAllOrders(t *testing.T) { 143 | exchangeAPI := GetExchangeAPI() 144 | res, err := exchangeAPI.CancelAllOrders() 145 | if err != nil { 146 | t.Errorf("CancelAllOrders() error = %v", err) 147 | } 148 | t.Logf("CancelAllOrders() = %v", res) 149 | } 150 | 151 | func TestExchangeAPI_CreateLimitOrderAndCancelOrderByCloidt(t *testing.T) { 152 | exchangeAPI := GetExchangeAPI() 153 | size := -0.01 154 | coin := "BTC" 155 | px := 105000.0 156 | cloid := "0x1234567890abcdef1234567890abcdef" 157 | res, err := exchangeAPI.LimitOrder(TifGtc, coin, size, px, false, cloid) 158 | if err != nil { 159 | t.Errorf("MakeLimit() error = %v", err) 160 | } 161 | t.Logf("MakeLimit() = %v", res) 162 | openOrders, err := exchangeAPI.infoAPI.GetOpenOrders(exchangeAPI.AccountAddress()) 163 | if err != nil { 164 | t.Errorf("GetAccountOpenOrders() error = %v", err) 165 | } 166 | t.Logf("GetAccountOpenOrders() = %v", openOrders) 167 | orderOpened := false 168 | var orderCloid string 169 | for _, order := range *openOrders { 170 | t.Logf("Order: %+v", order) 171 | if order.Coin == coin && order.Sz == -size && order.LimitPx == px { 172 | orderOpened = true 173 | orderCloid = order.Cloid 174 | break 175 | } 176 | } 177 | if !orderOpened { 178 | t.Errorf("Order not found: %v", openOrders) 179 | } 180 | time.Sleep(5 * time.Second) // wait to execute order 181 | cancelRes, err := exchangeAPI.CancelOrderByCloid(coin, orderCloid) 182 | if err != nil { 183 | t.Errorf("CancelOrderByCloid() error = %v", err) 184 | } 185 | t.Logf("CancelOrderByCloid() = %v", cancelRes) 186 | } 187 | 188 | func TestExchangeAPI_CreateLimitOrderAndCancelOrderByOid(t *testing.T) { 189 | exchangeAPI := GetExchangeAPI() 190 | size := 0.1 191 | coin := "BTC" 192 | px := 85000.0 193 | res, err := exchangeAPI.LimitOrder(TifGtc, coin, size, px, false) 194 | if err != nil { 195 | t.Errorf("MakeLimit() error = %v", err) 196 | } 197 | t.Logf("MakeLimit() = %v", res) 198 | openOrders, err := exchangeAPI.infoAPI.GetOpenOrders(exchangeAPI.AccountAddress()) 199 | if err != nil { 200 | t.Errorf("GetAccountOpenOrders() error = %v", err) 201 | } 202 | t.Logf("GetAccountOpenOrders() = %v", openOrders) 203 | orderOpened := false 204 | var orderOid int64 205 | for _, order := range *openOrders { 206 | t.Logf("Order: %+v", order) 207 | if order.Coin == coin && order.Sz == size && order.LimitPx == px { 208 | orderOpened = true 209 | orderOid = order.Oid 210 | break 211 | } 212 | } 213 | if !orderOpened { 214 | t.Errorf("Order not found: %v", openOrders) 215 | } 216 | time.Sleep(5 * time.Second) // wait to execute order 217 | cancelRes, err := exchangeAPI.CancelOrderByOID(coin, orderOid) 218 | if err != nil { 219 | t.Errorf("CancelOrderByOid() error = %v", err) 220 | } 221 | t.Logf("CancelOrderByOid() = %v", cancelRes) 222 | } 223 | 224 | func TestExchangeAPI_TestModifyOrder(t *testing.T) { 225 | exchangeAPI := GetExchangeAPI() 226 | size := 0.005 227 | coin := "ETH" 228 | px := 2000.0 229 | res, err := exchangeAPI.LimitOrder(TifGtc, coin, size, px, false) 230 | if err != nil { 231 | t.Errorf("MakeLimit() error = %v", err) 232 | } 233 | t.Logf("MakeLimit() = %v", res) 234 | openOrders, err := exchangeAPI.infoAPI.GetOpenOrders(exchangeAPI.AccountAddress()) 235 | if err != nil { 236 | t.Errorf("GetAccountOpenOrders() error = %v", err) 237 | } 238 | t.Logf("GetAccountOpenOrders() = %v", openOrders) 239 | orderOpened := false 240 | for _, order := range *openOrders { 241 | if order.Coin == coin && order.Sz == size && order.LimitPx == px { 242 | orderOpened = true 243 | break 244 | } 245 | } 246 | log.Printf("Order ID: %v", res.Response.Data.Statuses[0].Resting.OrderId) 247 | if !orderOpened { 248 | t.Errorf("Order not found: %+v", openOrders) 249 | } 250 | time.Sleep(5 * time.Second) // wait to execute order 251 | // modify order 252 | newPx := 2500.0 253 | orderType := OrderType{ 254 | Limit: &LimitOrderType{ 255 | Tif: TifGtc, 256 | }, 257 | } 258 | modifyOrderRequest := ModifyOrderRequest{ 259 | OrderId: res.Response.Data.Statuses[0].Resting.OrderId, 260 | Coin: coin, 261 | Sz: size, 262 | LimitPx: newPx, 263 | OrderType: orderType, 264 | IsBuy: true, 265 | ReduceOnly: false, 266 | } 267 | modifyRes, err := exchangeAPI.BulkModifyOrders([]ModifyOrderRequest{modifyOrderRequest}, false) 268 | if err != nil { 269 | t.Errorf("ModifyOrder() error = %v", err) 270 | } 271 | t.Logf("ModifyOrder() = %+v", modifyRes) 272 | time.Sleep(5 * time.Second) // wait to execute order 273 | cancelRes, err := exchangeAPI.CancelAllOrders() 274 | if err != nil { 275 | t.Errorf("CancelAllOrders() error = %v", err) 276 | } 277 | t.Logf("CancelAllOrders() = %v", cancelRes) 278 | } 279 | 280 | func TestExchangeAPI_TestMultipleMarketOrder(t *testing.T) { 281 | exchangeAPI := GetExchangeAPI() 282 | testCases := []struct { 283 | coin string 284 | size float64 285 | }{ 286 | {"BTC", 0.001}, 287 | {"BTC", -0.001}, 288 | {"ETH", 0.12}, 289 | {"ETH", -0.12}, 290 | {"INJ", 21.1}, 291 | {"INJ", -21.1}, 292 | {"PNUT", 100.122}, 293 | {"PNUT", -100.1}, 294 | {"ADA", 100.123456}, 295 | {"ADA", -100.123456}, 296 | } 297 | for _, tc := range testCases { 298 | t.Run(tc.coin, func(t *testing.T) { 299 | res, err := exchangeAPI.MarketOrder(tc.coin, tc.size, nil) 300 | if err != nil { 301 | t.Errorf("MarketOrder() error = %v", err) 302 | } 303 | t.Logf("MarketOrder() = %v", res) 304 | }) 305 | 306 | } 307 | } 308 | 309 | func TestExchangeAPI_TestIncorrectOrderSize(t *testing.T) { 310 | exchangeAPI := GetExchangeAPI() 311 | size := 0.1 312 | coin := "ADA" 313 | res, err := exchangeAPI.MarketOrder(coin, size, nil) 314 | if err != nil { 315 | t.Errorf("MarketOrder() error = %v", err) 316 | } 317 | if res.Response.Data.Statuses[0].Error != "Order has zero size." { 318 | t.Errorf("MarketOrder() error = %s but expected %s", res.Response.Data.Statuses[0].Error, "Order has zero size.") 319 | } 320 | } 321 | 322 | func TestExchangeAPI_TestClosePositionByMarket(t *testing.T) { 323 | exchangeAPI := GetExchangeAPI() 324 | size := -1.0 325 | coin := "ETH" 326 | res, err := exchangeAPI.MarketOrder(coin, size, nil) 327 | if err != nil { 328 | t.Errorf("MarketOrder() error = %v", err) 329 | } 330 | t.Logf("MarketOrder() = %v", res) 331 | time.Sleep(5 * time.Second) 332 | // close position with big size 333 | closeRes, err := exchangeAPI.MarketOrder(coin, -size, nil) 334 | if err != nil { 335 | t.Errorf("MarketOrder() error = %v", err) 336 | } 337 | t.Logf("MarketOrder() = %v", closeRes) 338 | accountState, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress()) 339 | if err != nil { 340 | t.Errorf("GetAccountState() error = %v", err) 341 | } 342 | // check that there is no opened position 343 | if len(accountState.AssetPositions) != 0 { 344 | t.Errorf("Account has opened positions: %v", accountState.AssetPositions) 345 | } 346 | } 347 | 348 | // Test Mainnet Only 349 | func TestExchangeAPI_TestWithdraw(t *testing.T) { 350 | exchangeAPI := GetExchangeAPI() 351 | withdrawAmount := 20.0 352 | stateBefore, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress()) 353 | if err != nil { 354 | t.Errorf("GetAccountState() error = %v", err) 355 | } 356 | t.Logf("GetAccountState() = %v", stateBefore) 357 | balanceBefore := stateBefore.Withdrawable 358 | if balanceBefore < withdrawAmount { 359 | t.Errorf("Insufficient balance: %v", stateBefore) 360 | } 361 | accountAddress := exchangeAPI.AccountAddress() // withdraw to the same address 362 | res, err := exchangeAPI.Withdraw(accountAddress, withdrawAmount) 363 | if err != nil { 364 | t.Errorf("Withdraw() error = %v", err) 365 | } 366 | t.Logf("Withdraw() = %v", res) 367 | } 368 | 369 | func TestExchageAPI_TestMarketOrderSpot(t *testing.T) { 370 | exchangeAPI := GetExchangeAPI() 371 | size := 0.81 372 | coin := "HYPE" 373 | res, err := exchangeAPI.MarketOrderSpot(coin, size, nil) 374 | if err != nil { 375 | t.Errorf("MakeOpen() error = %v", err) 376 | } 377 | t.Logf("MakeOpen() = %v", res) 378 | avgPrice := res.Response.Data.Statuses[0].Filled.AvgPx 379 | if avgPrice == 0 { 380 | t.Errorf("res.Response.Data.Statuses[0].Filled.AvgPx = %v", avgPrice) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /hyperliquid/exchange_types.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type RsvSignature struct { 9 | R string `json:"r"` 10 | S string `json:"s"` 11 | V byte `json:"v"` 12 | } 13 | 14 | // Base request for /exchange endpoint 15 | type ExchangeRequest struct { 16 | Action any `json:"action"` 17 | Nonce uint64 `json:"nonce"` 18 | Signature RsvSignature `json:"signature"` 19 | VaultAddress *string `json:"vaultAddress,omitempty" msgpack:",omitempty"` 20 | } 21 | 22 | type AssetInfo struct { 23 | SzDecimals int 24 | WeiDecimals int 25 | AssetId int 26 | SpotName string // for spot asset (e.g. "@107") 27 | } 28 | 29 | type OrderRequest struct { 30 | Coin string `json:"coin"` 31 | IsBuy bool `json:"is_buy"` 32 | Sz float64 `json:"sz"` 33 | LimitPx float64 `json:"limit_px"` 34 | OrderType OrderType `json:"order_type"` 35 | ReduceOnly bool `json:"reduce_only"` 36 | Cloid string `json:"cloid,omitempty"` 37 | } 38 | 39 | type OrderType struct { 40 | Limit *LimitOrderType `json:"limit,omitempty" msgpack:"limit,omitempty"` 41 | Trigger *TriggerOrderType `json:"trigger,omitempty" msgpack:"trigger,omitempty"` 42 | } 43 | 44 | type LimitOrderType struct { 45 | Tif string `json:"tif" msgpack:"tif"` 46 | } 47 | 48 | const ( 49 | TifGtc string = "Gtc" 50 | TifIoc string = "Ioc" 51 | TifAlo string = "Alo" 52 | ) 53 | 54 | type TriggerOrderType struct { 55 | IsMarket bool `json:"isMarket" msgpack:"isMarket"` 56 | TriggerPx string `json:"triggerPx" msgpack:"triggerPx"` 57 | TpSl TpSl `json:"tpsl" msgpack:"tpsl"` 58 | } 59 | 60 | type TpSl string 61 | 62 | const TriggerTp TpSl = "tp" 63 | const TriggerSl TpSl = "sl" 64 | 65 | type Grouping string 66 | 67 | const GroupingNa Grouping = "na" 68 | const GroupingTpSl Grouping = "positionTpsl" 69 | 70 | type Message struct { 71 | Source string `json:"source"` 72 | ConnectionId []byte `json:"connectionId"` 73 | } 74 | 75 | type OrderWire struct { 76 | Asset int `msgpack:"a" json:"a"` 77 | IsBuy bool `msgpack:"b" json:"b"` 78 | LimitPx string `msgpack:"p" json:"p"` 79 | SizePx string `msgpack:"s" json:"s"` 80 | ReduceOnly bool `msgpack:"r" json:"r"` 81 | OrderType OrderTypeWire `msgpack:"t" json:"t"` 82 | Cloid string `msgpack:"c,omitempty" json:"c,omitempty"` 83 | } 84 | type ModifyResponse struct { 85 | Status string `json:"status"` 86 | Response OrderInnerResponse `json:"response"` 87 | } 88 | type ModifyOrderWire struct { 89 | OrderId int `msgpack:"oid" json:"oid"` 90 | Order OrderWire `msgpack:"order" json:"order"` 91 | } 92 | type ModifyOrderAction struct { 93 | Type string `msgpack:"type" json:"type"` 94 | Modifies []ModifyOrderWire `msgpack:"modifies" json:"modifies"` 95 | } 96 | 97 | type ModifyOrderRequest struct { 98 | OrderId int `json:"oid"` 99 | Coin string `json:"coin"` 100 | IsBuy bool `json:"is_buy"` 101 | Sz float64 `json:"sz"` 102 | LimitPx float64 `json:"limit_px"` 103 | OrderType OrderType `json:"order_type"` 104 | ReduceOnly bool `json:"reduce_only"` 105 | Cloid string `json:"cloid,omitempty"` 106 | } 107 | 108 | type OrderTypeWire struct { 109 | Limit *LimitOrderType `json:"limit,omitempty" msgpack:"limit,omitempty"` 110 | Trigger *TriggerOrderType `json:"trigger,omitempty" msgpack:"trigger,omitempty"` 111 | } 112 | 113 | type PlaceOrderAction struct { 114 | Type string `msgpack:"type" json:"type"` 115 | Orders []OrderWire `msgpack:"orders" json:"orders"` 116 | Grouping Grouping `msgpack:"grouping" json:"grouping"` 117 | } 118 | 119 | type OrderResponse struct { 120 | Status string `json:"status"` 121 | Response OrderInnerResponse `json:"response"` 122 | } 123 | 124 | type OrderInnerResponse struct { 125 | Type string `json:"type"` 126 | Data DataResponse `json:"data"` 127 | } 128 | 129 | type DataResponse struct { 130 | Statuses []StatusResponse `json:"statuses"` 131 | } 132 | 133 | type StatusResponse struct { 134 | Resting RestingStatus `json:"resting,omitempty"` 135 | Filled FilledStatus `json:"filled,omitempty"` 136 | Error string `json:"error,omitempty"` 137 | Status string `json:"status,omitempty"` 138 | } 139 | 140 | // UnmarshalJSON implements custom unmarshaling for StatusResponse. 141 | // It first checks if the incoming JSON is a simple string. If so, it assigns the 142 | // value to the Status field. Otherwise, it unmarshals the JSON into the struct normally. 143 | func (sr *StatusResponse) UnmarshalJSON(data []byte) error { 144 | // Try to unmarshal data as a string. 145 | var s string 146 | if err := json.Unmarshal(data, &s); err == nil { 147 | sr.Status = s 148 | return nil 149 | } 150 | 151 | // Otherwise, unmarshal as a full object. 152 | // Use an alias to avoid infinite recursion. 153 | type Alias StatusResponse 154 | var alias Alias 155 | if err := json.Unmarshal(data, &alias); err != nil { 156 | return fmt.Errorf("StatusResponse: unable to unmarshal data as string or object: %w", err) 157 | } 158 | *sr = StatusResponse(alias) 159 | return nil 160 | } 161 | 162 | type CancelRequest struct { 163 | OrderId int `json:"oid"` 164 | Coin int `json:"coin"` 165 | } 166 | 167 | type CancelOidOrderAction struct { 168 | Type string `msgpack:"type" json:"type"` 169 | Cancels []CancelOidWire `msgpack:"cancels" json:"cancels"` 170 | } 171 | 172 | type CancelOidWire struct { 173 | Asset int `msgpack:"a" json:"a"` 174 | Oid int `msgpack:"o" json:"o"` 175 | } 176 | 177 | type CancelCloidWire struct { 178 | Asset int `msgpack:"asset" json:"asset"` 179 | Cloid string `msgpack:"cloid" json:"cloid"` 180 | } 181 | 182 | type CancelCloidOrderAction struct { 183 | Type string `msgpack:"type" json:"type"` 184 | Cancels []CancelCloidWire `msgpack:"cancels" json:"cancels"` 185 | } 186 | 187 | type RestingStatus struct { 188 | OrderId int `json:"oid"` 189 | Cloid string `json:"cloid,omitempty"` 190 | } 191 | 192 | type CloseRequest struct { 193 | Coin string 194 | Px float64 195 | Sz float64 196 | Slippage float64 197 | Cloid string 198 | } 199 | 200 | type FilledStatus struct { 201 | OrderId int `json:"oid"` 202 | AvgPx float64 `json:"avgPx,string"` 203 | TotalSz float64 `json:"totalSz,string"` 204 | Cloid string `json:"cloid,omitempty"` 205 | } 206 | 207 | type Liquidation struct { 208 | User string `json:"liquidatedUser"` 209 | MarkPrice string `json:"markPx"` 210 | Method string `json:"method"` 211 | } 212 | 213 | type UpdateLeverageAction struct { 214 | Type string `msgpack:"type" json:"type"` 215 | Asset int `msgpack:"asset" json:"asset"` 216 | IsCross bool `msgpack:"isCross" json:"isCross"` 217 | Leverage int `msgpack:"leverage" json:"leverage"` 218 | } 219 | 220 | type DefaultExchangeResponse struct { 221 | Status string `json:"status"` 222 | Response struct { 223 | Type string `json:"type"` 224 | } `json:"response"` 225 | } 226 | 227 | // Depending on Type this struct can has different non-nil fields 228 | type NonFundingDelta struct { 229 | Type string `json:"type"` 230 | Usdc float64 `json:"usdc,string,omitempty"` 231 | Amount float64 `json:"amount,string,omitempty"` 232 | ToPerp bool `json:"toPerp,omitempty"` 233 | Token string `json:"token,omitempty"` 234 | Fee float64 `json:"fee,string,omitempty"` 235 | Nonce int64 `json:"nonce"` 236 | } 237 | 238 | type FundingDelta struct { 239 | Asset string `json:"coin"` 240 | FundingRate string `json:"fundingRate"` 241 | Size string `json:"szi"` 242 | UsdcAmount string `json:"usdc"` 243 | } 244 | 245 | type Withdrawal struct { 246 | Time int64 `json:"time"` 247 | Hash string `json:"hash"` 248 | Amount float64 `json:"usdc"` 249 | Fee float64 `json:"fee"` 250 | Nonce int64 `json:"nonce"` 251 | } 252 | 253 | type Deposit struct { 254 | Hash string `json:"hash,omitempty"` 255 | Time int64 `json:"time,omitempty"` 256 | Amount float64 `json:"usdc,omitempty"` 257 | } 258 | 259 | type WithdrawAction struct { 260 | Type string `msgpack:"type" json:"type"` 261 | Destination string `msgpack:"destination" json:"destination"` 262 | Amount string `msgpack:"amount" json:"amount"` 263 | Time uint64 `msgpack:"time" json:"time"` 264 | HyperliquidChain string `msgpack:"hyperliquidChain" json:"hyperliquidChain"` 265 | SignatureChainID string `msgpack:"signatureChainId" json:"signatureChainId"` 266 | } 267 | 268 | type WithdrawResponse struct { 269 | Status string `json:"status"` 270 | Nonce int64 271 | } 272 | -------------------------------------------------------------------------------- /hyperliquid/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Logarithm-Labs/go-hyperliquid/hyperliquid 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.14.13 7 | github.com/sirupsen/logrus v1.9.3 8 | github.com/vmihailenco/msgpack/v5 v5.4.1 9 | ) 10 | 11 | require ( 12 | github.com/bits-and-blooms/bitset v1.13.0 // indirect 13 | github.com/consensys/bavard v0.1.13 // indirect 14 | github.com/consensys/gnark-crypto v0.12.1 // indirect 15 | github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect 16 | github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 18 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect 19 | github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect 20 | github.com/holiman/uint256 v1.3.1 // indirect 21 | github.com/mmcloughlin/addchain v0.4.0 // indirect 22 | github.com/supranational/blst v0.3.13 // indirect 23 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 24 | golang.org/x/crypto v0.31.0 // indirect 25 | golang.org/x/sync v0.7.0 // indirect 26 | golang.org/x/sys v0.28.0 // indirect 27 | rsc.io/tmplfunc v0.0.3 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /hyperliquid/go.sum: -------------------------------------------------------------------------------- 1 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 2 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 3 | github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= 4 | github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= 5 | github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= 6 | github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= 10 | github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= 11 | github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= 12 | github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= 13 | github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= 14 | github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= 15 | github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= 16 | github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 21 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 22 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 23 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 24 | github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= 25 | github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 26 | github.com/ethereum/go-ethereum v1.14.13 h1:L81Wmv0OUP6cf4CW6wtXsr23RUrDhKs2+Y9Qto+OgHU= 27 | github.com/ethereum/go-ethereum v1.14.13/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY= 28 | github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= 29 | github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= 30 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 31 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 32 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 33 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 34 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 35 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 36 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 37 | github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= 38 | github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 39 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 40 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 41 | github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= 42 | github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= 43 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 44 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 45 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 46 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 47 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 48 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 49 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 53 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 54 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 55 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 56 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 57 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 61 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= 63 | github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 64 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 65 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 66 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 67 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 68 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 69 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 70 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 71 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 72 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 73 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 74 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 75 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 76 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 77 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 78 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 80 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 83 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 84 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 86 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 88 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 89 | -------------------------------------------------------------------------------- /hyperliquid/hyperliquid.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | type IHyperliquid interface { 4 | IExchangeAPI 5 | IInfoAPI 6 | } 7 | 8 | type Hyperliquid struct { 9 | ExchangeAPI 10 | InfoAPI 11 | } 12 | 13 | // HyperliquidClientConfig is a configuration struct for Hyperliquid API. 14 | // PrivateKey can be empty if you only need to use the public endpoints. 15 | // AccountAddress is the default account address for the API that can be changed with SetAccountAddress(). 16 | // AccountAddress may be different from the address build from the private key due to Hyperliquid's account system. 17 | type HyperliquidClientConfig struct { 18 | IsMainnet bool 19 | PrivateKey string 20 | AccountAddress string 21 | } 22 | 23 | func NewHyperliquid(config *HyperliquidClientConfig) *Hyperliquid { 24 | var defaultConfig *HyperliquidClientConfig 25 | if config == nil { 26 | defaultConfig = &HyperliquidClientConfig{ 27 | IsMainnet: true, 28 | PrivateKey: "", 29 | AccountAddress: "", 30 | } 31 | } else { 32 | defaultConfig = config 33 | } 34 | exchangeAPI := NewExchangeAPI(defaultConfig.IsMainnet) 35 | exchangeAPI.SetPrivateKey(defaultConfig.PrivateKey) 36 | exchangeAPI.SetAccountAddress(defaultConfig.AccountAddress) 37 | infoAPI := NewInfoAPI(defaultConfig.IsMainnet) 38 | infoAPI.SetAccountAddress(defaultConfig.AccountAddress) 39 | return &Hyperliquid{ 40 | ExchangeAPI: *exchangeAPI, 41 | InfoAPI: *infoAPI, 42 | } 43 | } 44 | 45 | func (h *Hyperliquid) SetDebugActive() { 46 | h.ExchangeAPI.SetDebugActive() 47 | h.InfoAPI.SetDebugActive() 48 | } 49 | 50 | func (h *Hyperliquid) SetPrivateKey(privateKey string) error { 51 | err := h.ExchangeAPI.SetPrivateKey(privateKey) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func (h *Hyperliquid) SetAccountAddress(accountAddress string) { 59 | h.ExchangeAPI.SetAccountAddress(accountAddress) 60 | h.InfoAPI.SetAccountAddress(accountAddress) 61 | } 62 | 63 | func (h *Hyperliquid) AccountAddress() string { 64 | return h.ExchangeAPI.AccountAddress() 65 | } 66 | 67 | func (h *Hyperliquid) IsMainnet() bool { 68 | return h.ExchangeAPI.IsMainnet() 69 | } 70 | -------------------------------------------------------------------------------- /hyperliquid/hyperliquid_test.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func GetHyperliquidAPI() *Hyperliquid { 10 | hl := NewHyperliquid(&HyperliquidClientConfig{ 11 | IsMainnet: false, 12 | AccountAddress: os.Getenv("TEST_ADDRESS"), 13 | PrivateKey: os.Getenv("TEST_PRIVATE_KEY"), 14 | }) 15 | if GLOBAL_DEBUG { 16 | hl.SetDebugActive() 17 | } 18 | return hl 19 | } 20 | 21 | func TestHyperliquid_CheckFieldsConsistency(t *testing.T) { 22 | hl := GetHyperliquidAPI() 23 | if hl.ExchangeAPI.baseEndpoint != "/exchange" { 24 | t.Errorf("baseEndpoint = %v, want %v", hl.ExchangeAPI.baseEndpoint, "/exchange") 25 | } 26 | if hl.InfoAPI.baseEndpoint != "/info" { 27 | t.Errorf("baseEndpoint = %v, want %v", hl.InfoAPI.baseEndpoint, "/info") 28 | } 29 | var apiUrl string 30 | if hl.ExchangeAPI.IsMainnet() { 31 | apiUrl = MAINNET_API_URL 32 | } else { 33 | apiUrl = TESTNET_API_URL 34 | } 35 | if hl.InfoAPI.baseUrl != apiUrl { 36 | t.Errorf("baseUrl = %v, want %v", hl.InfoAPI.baseUrl, apiUrl) 37 | } 38 | hl.SetDebugActive() 39 | if hl.InfoAPI.Debug != hl.ExchangeAPI.Debug { 40 | t.Errorf("debug = %v, want %v", hl.InfoAPI.Debug, hl.ExchangeAPI.Debug) 41 | } 42 | savedAddress := hl.AccountAddress() 43 | newAddress := "0x1234567890" 44 | hl.SetAccountAddress(newAddress) 45 | if hl.InfoAPI.AccountAddress() != newAddress { 46 | t.Errorf("InfoAPI.AccountAddress = %v, want %v", hl.InfoAPI.AccountAddress(), newAddress) 47 | } 48 | if hl.ExchangeAPI.AccountAddress() != newAddress { 49 | t.Errorf("ExchangeAPI.AccountAddress = %v, want %v", hl.ExchangeAPI.AccountAddress(), newAddress) 50 | } 51 | if hl.AccountAddress() != newAddress { 52 | t.Errorf("gl.AccountAddress = %v, want %v", hl.AccountAddress(), newAddress) 53 | } 54 | hl.SetAccountAddress(savedAddress) 55 | } 56 | 57 | func TestHyperliquid_MakeSomeTradingLogic(t *testing.T) { 58 | client := GetHyperliquidAPI() 59 | 60 | // Make limit order 61 | res1, err := client.LimitOrder(TifGtc, "ETH", 0.01, 1234.1, false) 62 | if err != nil { 63 | t.Errorf("Error: %v", err) 64 | } 65 | t.Logf("LimitOrder(TifIoc, ETH, 0.01, 1234.1, false): %v", res1) 66 | 67 | res2, err := client.LimitOrder(TifGtc, "ETH", 0.01, 1200.1, true) 68 | if err != nil { 69 | t.Errorf("Error: %v", err) 70 | } 71 | t.Logf("LimitOrder(TifGtc, ETH, 0.01, 1200.1, true): %v", res2) 72 | 73 | res3, err := client.LimitOrder(TifGtc, "ETH", -0.01, 5000.1, true) 74 | if err != nil { 75 | t.Errorf("Error: %v", err) 76 | } 77 | t.Logf("LimitOrder(TifGtc, ETH, -0.01, 5000.1, true): %v", res3) 78 | 79 | res4, err := client.LimitOrder(TifGtc, "ETH", 0.01, 1234.1, false, "0x1234567890abcdef1234567890abcdef") 80 | if err != nil { 81 | if err != nil { 82 | t.Errorf("Error: %v", err) 83 | } 84 | } 85 | t.Logf("LimitOrder(TifIoc, ETH, 0.01, 1234.1, false, 0x1234567890abcdef1234567890abcdef): %v", res4) 86 | 87 | // Get all ordres 88 | res5, err := client.GetAccountOpenOrders() 89 | if err != nil { 90 | t.Errorf("Error: %v", err) 91 | } 92 | t.Logf("GetAccountOpenOrders(): %v", res5) 93 | 94 | // Close all orders 95 | res6, err := client.CancelAllOrders() 96 | if err != nil { 97 | t.Errorf("Error: %v", err) 98 | } 99 | t.Logf("CancelAllOrders(): %v", res6) 100 | 101 | // Make market order 102 | res7, err := client.MarketOrder("ETH", 0.01, nil) 103 | if err != nil { 104 | t.Errorf("Error: %v", err) 105 | } 106 | t.Logf("MarketOrder(ETH, 0.01, nil): %v", res7) 107 | 108 | // Close position 109 | res8, err := client.ClosePosition("ETH") 110 | if err != nil { 111 | t.Errorf("Error: %v", err) 112 | } 113 | t.Logf("ClosePosition(ETH): %v", res8) 114 | 115 | // Get account balance 116 | res9, err := client.GetAccountState() 117 | if err != nil { 118 | t.Errorf("Error: %v", err) 119 | } 120 | t.Logf("GetAccountState(): %v", res9) 121 | } 122 | 123 | func TestHyperliquid_MarketOrder(t *testing.T) { 124 | client := GetHyperliquidAPI() 125 | order, err := client.MarketOrder("ADA", 100, nil) 126 | if err != nil { 127 | t.Errorf("Error: %v", err) 128 | } 129 | t.Logf("MarketOrder(ADA, 100, nil): %+v", order) 130 | } 131 | 132 | func TestHyperliquid_LimitOrder(t *testing.T) { 133 | client := GetHyperliquidAPI() 134 | order1, err := client.LimitOrder(TifGtc, "BTC", 0.01, 70000, false) 135 | if err != nil { 136 | t.Errorf("Error: %v", err) 137 | } 138 | t.Logf("LimitOrder(TifGtc, BTC, 0.01, 70000, false): %+v", order1) 139 | order2, err := client.LimitOrder(TifGtc, "BTC", -0.01, 120000, false) 140 | if err != nil { 141 | t.Errorf("Error: %v", err) 142 | } 143 | t.Logf("LimitOrder(TifGtc, BTC, -0.01, 120000, false): %+v", order2) 144 | } 145 | 146 | func TestHyperliquid_GoLimitOrders(t *testing.T) { 147 | client := GetHyperliquidAPI() 148 | wg := &sync.WaitGroup{} 149 | for i := 0; i < 10; i++ { 150 | wg.Add(1) 151 | go func(wg *sync.WaitGroup) { 152 | defer wg.Done() 153 | order, err := client.LimitOrder(TifGtc, "BTC", 0.001, 60000, false) 154 | if err != nil { 155 | t.Errorf("Error: %v", err) 156 | } 157 | t.Logf("LimitOrder(TifGtc, BTC, 0.01, 70000, false): %+v", order) 158 | }(wg) 159 | } 160 | wg.Wait() 161 | } 162 | -------------------------------------------------------------------------------- /hyperliquid/info_service.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | // IInfoAPI is an interface for the /info service. 10 | type IInfoAPI interface { 11 | IClient // Base client interface 12 | 13 | // INFO API ENDPOINTS 14 | GetAllMids() (*map[string]string, error) 15 | GetOpenOrders(address string) (*[]Order, error) 16 | GetAccountOpenOrders() (*[]Order, error) 17 | GetUserFills(address string) (*[]OrderFill, error) 18 | GetAccountFills() (*[]OrderFill, error) 19 | GetUserRateLimits(address string) (*float64, error) 20 | GetL2BookSnapshot(coin string) (*L2BookSnapshot, error) 21 | GetCandleSnapshot(coin string, interval string, startTime int64, endTime int64) (*CandleSnapshot, error) 22 | 23 | // PERPETUALS INFO API ENDPOINTS 24 | GetMeta() (*Meta, error) 25 | GetUserState(address string) (*UserState, error) 26 | GetAccountState() (*UserState, error) 27 | GetFundingUpdates(address string, startTime int64, endTime int64) (*[]FundingUpdate, error) 28 | GetAccountFundingUpdates(startTime int64, endTime int64) (*[]FundingUpdate, error) 29 | GetNonFundingUpdates(address string, startTime int64, endTime int64) (*[]NonFundingUpdate, error) 30 | GetAccountNonFundingUpdates(startTime int64, endTime int64) (*[]NonFundingUpdate, error) 31 | GetHistoricalFundingRates() (*[]HistoricalFundingRate, error) 32 | 33 | // Additional helper functions 34 | GetMartketPx(coin string) (float64, error) 35 | BuildMetaMap() (map[string]AssetInfo, error) 36 | GetWithdrawals(address string) (*[]Withdrawal, error) 37 | GetAccountWithdrawals() (*[]Withdrawal, error) 38 | } 39 | 40 | type InfoAPI struct { 41 | Client 42 | baseEndpoint string 43 | spotMeta map[string]AssetInfo 44 | } 45 | 46 | // NewInfoAPI returns a new instance of the InfoAPI struct. 47 | // It sets the base endpoint to "/info" and the client to the NewClient function. 48 | // The isMainnet parameter is used to set the network type. 49 | func NewInfoAPI(isMainnet bool) *InfoAPI { 50 | api := InfoAPI{ 51 | baseEndpoint: "/info", 52 | Client: *NewClient(isMainnet), 53 | } 54 | spotMeta, err := api.BuildSpotMetaMap() 55 | if err != nil { 56 | api.SetDebugActive() 57 | api.debug("Error building meta map: %s", err) 58 | } 59 | api.spotMeta = spotMeta 60 | return &api 61 | } 62 | 63 | func (api *InfoAPI) Endpoint() string { 64 | return api.baseEndpoint 65 | } 66 | 67 | // Retrieve mids for all actively traded coins 68 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-mids-for-all-actively-traded-coins 69 | func (api *InfoAPI) GetAllMids() (*map[string]string, error) { 70 | request := InfoRequest{ 71 | Typez: "allMids", 72 | } 73 | return MakeUniversalRequest[map[string]string](api, request) 74 | } 75 | 76 | // Retrieve spot meta and asset contexts 77 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts 78 | func (api *InfoAPI) GetAllSpotPrices() (*map[string]string, error) { 79 | request := InfoRequest{ 80 | Typez: "spotMetaAndAssetCtxs", 81 | } 82 | response, err := MakeUniversalRequest[SpotMetaAndAssetCtxsResponse](api, request) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | marketsData, ok := response[1].([]interface{}) 88 | if !ok { 89 | return nil, fmt.Errorf("invalid markets data format") 90 | } 91 | 92 | result := make(map[string]string) 93 | 94 | marketBytes, err := json.Marshal(marketsData) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | var markets []Market 100 | if err := json.Unmarshal(marketBytes, &markets); err != nil { 101 | return nil, err 102 | } 103 | 104 | for _, market := range markets { 105 | result[market.Coin] = market.MidPx 106 | } 107 | 108 | return &result, nil 109 | } 110 | 111 | // Retrieve a user's open orders 112 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-open-orders 113 | func (api *InfoAPI) GetOpenOrders(address string) (*[]Order, error) { 114 | request := InfoRequest{ 115 | User: address, 116 | Typez: "openOrders", 117 | } 118 | return MakeUniversalRequest[[]Order](api, request) 119 | } 120 | 121 | // Retrieve a account's order history 122 | // The same as GetOpenOrders but user is set to the account address 123 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 124 | func (api *InfoAPI) GetAccountOpenOrders() (*[]Order, error) { 125 | return api.GetOpenOrders(api.AccountAddress()) 126 | } 127 | 128 | // Retrieve a user's fills 129 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-fills 130 | func (api *InfoAPI) GetUserFills(address string) (*[]OrderFill, error) { 131 | request := InfoRequest{ 132 | User: address, 133 | Typez: "userFills", 134 | } 135 | return MakeUniversalRequest[[]OrderFill](api, request) 136 | } 137 | 138 | // Retrieve a account's fill history 139 | // The same as GetUserFills but user is set to the account address 140 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 141 | func (api *InfoAPI) GetAccountFills() (*[]OrderFill, error) { 142 | return api.GetUserFills(api.AccountAddress()) 143 | } 144 | 145 | // Query user rate limits 146 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-user-rate-limits 147 | func (api *InfoAPI) GetUserRateLimits(address string) (*RatesLimits, error) { 148 | request := InfoRequest{ 149 | User: address, 150 | Typez: "userRateLimit", 151 | } 152 | return MakeUniversalRequest[RatesLimits](api, request) 153 | } 154 | 155 | // Query account rate limits 156 | // The same as GetUserRateLimits but user is set to the account address 157 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 158 | func (api *InfoAPI) GetAccountRateLimits() (*RatesLimits, error) { 159 | return api.GetUserRateLimits(api.AccountAddress()) 160 | } 161 | 162 | // L2 Book snapshot 163 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#l2-book-snapshot 164 | func (api *InfoAPI) GetL2BookSnapshot(coin string) (*L2BookSnapshot, error) { 165 | request := InfoRequest{ 166 | Typez: "l2Book", 167 | Coin: coin, 168 | } 169 | return MakeUniversalRequest[L2BookSnapshot](api, request) 170 | } 171 | 172 | // Candle snapshot (Only the most recent 5000 candles are available) 173 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot 174 | func (api *InfoAPI) GetCandleSnapshot(coin string, interval string, startTime int64, endTime int64) (*[]CandleSnapshot, error) { 175 | request := CandleSnapshotRequest{ 176 | Typez: "candleSnapshot", 177 | Req: CandleSnapshotSubRequest{ 178 | Coin: coin, 179 | Interval: interval, 180 | StartTime: startTime, 181 | EndTime: endTime, 182 | }, 183 | } 184 | return MakeUniversalRequest[[]CandleSnapshot](api, request) 185 | } 186 | 187 | // Retrieve perpetuals metadata 188 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-perpetuals-metadata 189 | func (api *InfoAPI) GetMeta() (*Meta, error) { 190 | request := InfoRequest{ 191 | Typez: "meta", 192 | } 193 | return MakeUniversalRequest[Meta](api, request) 194 | } 195 | 196 | // Retrieve spot metadata 197 | func (api *InfoAPI) GetSpotMeta() (*SpotMeta, error) { 198 | request := InfoRequest{ 199 | Typez: "spotMeta", 200 | } 201 | return MakeUniversalRequest[SpotMeta](api, request) 202 | } 203 | 204 | // Retrieve user's perpetuals account summary 205 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary 206 | func (api *InfoAPI) GetUserState(address string) (*UserState, error) { 207 | request := UserStateRequest{ 208 | User: address, 209 | Typez: "clearinghouseState", 210 | } 211 | return MakeUniversalRequest[UserState](api, request) 212 | } 213 | 214 | // Retrieve account's perpetuals account summary 215 | // The same as GetUserState but user is set to the account address 216 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 217 | func (api *InfoAPI) GetAccountState() (*UserState, error) { 218 | return api.GetUserState(api.AccountAddress()) 219 | } 220 | 221 | // Retrieve user's spot account summary 222 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances 223 | func (api *InfoAPI) GetUserStateSpot(address string) (*UserStateSpot, error) { 224 | request := UserStateRequest{ 225 | User: address, 226 | Typez: "spotClearinghouseState", 227 | } 228 | return MakeUniversalRequest[UserStateSpot](api, request) 229 | } 230 | 231 | // Retrieve account's spot account summary 232 | // The same as GetUserStateSpot but user is set to the account address 233 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 234 | func (api *InfoAPI) GetAccountStateSpot() (*UserStateSpot, error) { 235 | return api.GetUserStateSpot(api.AccountAddress()) 236 | } 237 | 238 | // Retrieve a user's funding history 239 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-a-users-funding-history-or-non-funding-ledger-updates 240 | func (api *InfoAPI) GetFundingUpdates(address string, startTime int64, endTime int64) (*[]FundingUpdate, error) { 241 | request := InfoRequest{ 242 | User: address, 243 | Typez: "userFunding", 244 | StartTime: startTime, 245 | EndTime: endTime, 246 | } 247 | return MakeUniversalRequest[[]FundingUpdate](api, request) 248 | } 249 | 250 | // Retrieve account's funding history 251 | // The same as GetFundingUpdates but user is set to the account address 252 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 253 | func (api *InfoAPI) GetAccountFundingUpdates(startTime int64, endTime int64) (*[]FundingUpdate, error) { 254 | return api.GetFundingUpdates(api.AccountAddress(), startTime, endTime) 255 | } 256 | 257 | // Retrieve a user's funding history or non-funding ledger updates 258 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-a-users-funding-history-or-non-funding-ledger-updates 259 | func (api *InfoAPI) GetNonFundingUpdates(address string, startTime int64, endTime int64) (*[]NonFundingUpdate, error) { 260 | request := InfoRequest{ 261 | User: address, 262 | Typez: "userNonFundingLedgerUpdates", 263 | StartTime: startTime, 264 | EndTime: endTime, 265 | } 266 | return MakeUniversalRequest[[]NonFundingUpdate](api, request) 267 | } 268 | 269 | // Retrieve account's funding history or non-funding ledger updates 270 | // The same as GetNonFundingUpdates but user is set to the account address 271 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 272 | func (api *InfoAPI) GetAccountNonFundingUpdates(startTime int64, endTime int64) (*[]NonFundingUpdate, error) { 273 | return api.GetNonFundingUpdates(api.AccountAddress(), startTime, endTime) 274 | } 275 | 276 | // Retrieve historical funding rates 277 | // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-historical-funding-rates 278 | func (api *InfoAPI) GetHistoricalFundingRates(coin string, startTime int64, endTime int64) (*[]HistoricalFundingRate, error) { 279 | request := InfoRequest{ 280 | Typez: "fundingHistory", 281 | Coin: coin, 282 | StartTime: startTime, 283 | EndTime: endTime, 284 | } 285 | return MakeUniversalRequest[[]HistoricalFundingRate](api, request) 286 | } 287 | 288 | // Helper function to get the market price of a given coin 289 | // The coin parameter is the name of the coin 290 | // 291 | // Example: 292 | // 293 | // api.GetMartketPx("BTC") 294 | func (api *InfoAPI) GetMartketPx(coin string) (float64, error) { 295 | allMids, err := api.GetAllMids() 296 | if err != nil { 297 | return 0, err 298 | } 299 | parsed, err := strconv.ParseFloat((*allMids)[coin], 32) 300 | if err != nil { 301 | return 0, err 302 | } 303 | return parsed, nil 304 | } 305 | 306 | // GetSpotMarketPx returns the market price of a given spot coin 307 | // The coin parameter is the name of the coin 308 | // 309 | // Example: 310 | // 311 | // api.GetSpotMarketPx("HYPE") 312 | func (api *InfoAPI) GetSpotMarketPx(coin string) (float64, error) { 313 | spotPrices, err := api.GetAllSpotPrices() 314 | if err != nil { 315 | return 0, err 316 | } 317 | spotName := api.spotMeta[coin].SpotName 318 | parsed, err := strconv.ParseFloat((*spotPrices)[spotName], 32) 319 | if err != nil { 320 | return 0, err 321 | } 322 | return parsed, nil 323 | } 324 | 325 | // Helper function to get the withdrawals of a given address 326 | // By default returns last 90 days 327 | func (api *InfoAPI) GetWithdrawals(address string) (*[]Withdrawal, error) { 328 | startTime, endTime := GetDefaultTimeRange() 329 | updates, err := api.GetNonFundingUpdates(address, startTime, endTime) 330 | if err != nil { 331 | return nil, err 332 | } 333 | var withdrawals []Withdrawal 334 | for _, update := range *updates { 335 | if update.Delta.Type == "withdraw" { 336 | withrawal := Withdrawal{ 337 | Time: update.Time, 338 | Hash: update.Hash, 339 | Amount: update.Delta.Usdc, 340 | Fee: update.Delta.Fee, 341 | Nonce: update.Delta.Nonce, 342 | } 343 | withdrawals = append(withdrawals, withrawal) 344 | } 345 | } 346 | return &withdrawals, nil 347 | } 348 | 349 | // Helper function to get the withdrawals of the account address 350 | // The same as GetWithdrawals but user is set to the account address 351 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 352 | func (api *InfoAPI) GetAccountWithdrawals() (*[]Withdrawal, error) { 353 | return api.GetWithdrawals(api.AccountAddress()) 354 | } 355 | 356 | // Helper function to get the deposits of the given address 357 | // By default returns last 90 days 358 | func (api *InfoAPI) GetDeposits(address string) (*[]Deposit, error) { 359 | startTime, endTime := GetDefaultTimeRange() 360 | updates, err := api.GetNonFundingUpdates(address, startTime, endTime) 361 | if err != nil { 362 | return nil, err 363 | } 364 | var deposits []Deposit 365 | for _, update := range *updates { 366 | if update.Delta.Type == "deposit" { 367 | deposit := Deposit{ 368 | Hash: update.Hash, 369 | Amount: update.Delta.Usdc, 370 | Time: update.Time, 371 | } 372 | deposits = append(deposits, deposit) 373 | } 374 | } 375 | return &deposits, nil 376 | } 377 | 378 | // Helper function to get the deposits of the account address 379 | // The same as GetDeposits but user is set to the account address 380 | // Check AccountAddress() or SetAccountAddress() if there is a need to set the account address 381 | func (api *InfoAPI) GetAccountDeposits() (*[]Deposit, error) { 382 | return api.GetDeposits(api.AccountAddress()) 383 | } 384 | 385 | // Helper function to build a map of asset names to asset info 386 | // It is used to get the assetId for a given asset name 387 | func (api *InfoAPI) BuildMetaMap() (map[string]AssetInfo, error) { 388 | metaMap := make(map[string]AssetInfo) 389 | result, err := api.GetMeta() 390 | if err != nil { 391 | return nil, err 392 | } 393 | for index, asset := range result.Universe { 394 | metaMap[asset.Name] = AssetInfo{ 395 | SzDecimals: asset.SzDecimals, 396 | AssetId: index, 397 | } 398 | } 399 | return metaMap, nil 400 | } 401 | 402 | // Helper function to build a map of asset names to asset info 403 | // It is used to get the assetId for a given asset name 404 | func (api *InfoAPI) BuildSpotMetaMap() (map[string]AssetInfo, error) { 405 | spotMeta, err := api.GetSpotMeta() 406 | if err != nil { 407 | return nil, err 408 | } 409 | 410 | tokenMap := make(map[int]struct { 411 | name string 412 | szDecimals int 413 | weiDecimals int 414 | }, len(spotMeta.Tokens)) 415 | 416 | for _, token := range spotMeta.Tokens { 417 | tokenMap[token.Index] = struct { 418 | name string 419 | szDecimals int 420 | weiDecimals int 421 | }{token.Name, token.SzDecimals, token.WeiDecimals} 422 | } 423 | 424 | metaMap := make(map[string]AssetInfo) 425 | for _, universe := range spotMeta.Universe { 426 | for _, tokenId := range universe.Tokens { 427 | if tokenId == 0 { 428 | continue 429 | } 430 | if token, exists := tokenMap[tokenId]; exists { 431 | metaMap[token.name] = AssetInfo{ 432 | SzDecimals: token.szDecimals, 433 | WeiDecimals: token.weiDecimals, 434 | AssetId: universe.Index, 435 | SpotName: universe.Name, 436 | } 437 | } 438 | } 439 | } 440 | return metaMap, nil 441 | } 442 | -------------------------------------------------------------------------------- /hyperliquid/info_test.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func GetInfoAPI() *InfoAPI { 9 | api := NewInfoAPI(false) 10 | if GLOBAL_DEBUG { 11 | api.SetDebugActive() 12 | } 13 | // It should be active account to pass all tests 14 | // like GetAccountFills, GetAccountWithdrawals, etc. 15 | TEST_ADDRESS := os.Getenv("TEST_ADDRESS") 16 | if TEST_ADDRESS == "" { 17 | panic("Set TEST_ADDRESS in .env file") 18 | } 19 | api.SetAccountAddress(TEST_ADDRESS) 20 | return api 21 | } 22 | 23 | func TestInfoAPI_AccountAddress(t *testing.T) { 24 | api := GetInfoAPI() 25 | address := api.AccountAddress() 26 | targetAddress := os.Getenv("TEST_ADDRESS") 27 | if targetAddress == "" { 28 | t.Errorf("Set TEST_ADDRESS in .env file") 29 | } 30 | if address != targetAddress { 31 | t.Errorf("AccountAddress() = %v, want %v", address, targetAddress) 32 | } 33 | } 34 | 35 | func TestInfoAPI_Endpoint(t *testing.T) { 36 | api := GetInfoAPI() 37 | res := api.Endpoint() 38 | if res != "/info" { 39 | t.Errorf("Endpoint() = %v, want %v", res, "/info") 40 | } 41 | } 42 | 43 | func TestInfoAPI_GetAllMids(t *testing.T) { 44 | api := GetInfoAPI() 45 | res, err := api.GetAllMids() 46 | if err != nil { 47 | t.Errorf("GetAllMids() error = %v", err) 48 | } 49 | 50 | // Check BTC and ETH are in the map 51 | if _, ok := (*res)["BTC"]; !ok { 52 | t.Errorf("GetAllMids() doesnt return %v, want %v", res, "BTC") 53 | } 54 | if _, ok := (*res)["ETH"]; !ok { 55 | t.Errorf("GetAllMids() doesnt return %v, want %v", res, "ETH") 56 | } 57 | t.Logf("GetAllMids() = %v", res) 58 | } 59 | 60 | func TestInfoAPI_GetAccountFills(t *testing.T) { 61 | api := GetInfoAPI() 62 | res, err := api.GetAccountFills() 63 | if err != nil { 64 | t.Errorf("GetAccountFills() error = %v", err) 65 | } 66 | if len(*res) == 0 { 67 | t.Errorf("GetAccountFills() len = %v, want > %v", res, 0) 68 | } 69 | res0 := (*res)[0] 70 | t.Logf("res0 = %+v", res0) 71 | if res0.Px == 0 { 72 | t.Errorf("res0.Px = %v, want > %v", res0.Px, 0) 73 | } 74 | if res0.Sz == 0 { 75 | t.Errorf("res0.Sz = %v, want > %v", res0.Sz, 0) 76 | } 77 | if res0.Fee == 0 { 78 | t.Errorf("res0.Fee = %v, want > %v", res0.Fee, 0) 79 | } 80 | t.Logf("GetAccountFills() = %v", res) 81 | } 82 | 83 | func TestInfoAPI_GetAccountRateLimits(t *testing.T) { 84 | api := GetInfoAPI() 85 | res, err := api.GetAccountRateLimits() 86 | if err != nil { 87 | t.Errorf("GetAccountRateLimits() error = %v", err) 88 | } 89 | if res.CumVlm == 0 { 90 | t.Errorf("GetAccountRateLimits() len = %v, want > %v", res.CumVlm, 0) 91 | } 92 | 93 | t.Logf("GetAccountRateLimits() = %v", res) 94 | } 95 | 96 | func TestInfoAPI_GetL2BookSnapshot(t *testing.T) { 97 | api := GetInfoAPI() 98 | res, err := api.GetL2BookSnapshot("BTC") 99 | if err != nil { 100 | t.Errorf("GetL2BookSnapshot() error = %v", err) 101 | } 102 | if res.Levels[0][0].Px <= 0 { 103 | t.Errorf("res.Levels[0][0].Px = %v, want > %v", res.Levels[0][0].Px, 0) 104 | } 105 | t.Logf("GetL2BookSnapshot() = %v", res) 106 | } 107 | 108 | func TestInfoAPI_GetCandleSnapshot(t *testing.T) { 109 | api := GetInfoAPI() 110 | startTime, endTime := GetDefaultTimeRange() 111 | res, err := api.GetCandleSnapshot("ETH", "1d", startTime, endTime) 112 | if err != nil { 113 | t.Errorf("GetCandleSnapshot() error = %v", err) 114 | } 115 | if len(*res) == 0 { 116 | t.Errorf("GetCandleSnapshot() len = %v, want > %v", res, 0) 117 | } 118 | if (*res)[0].Open <= 0 { 119 | t.Errorf("*res)[0].Open = %v, want > %v", (*res)[0].Open, 0) 120 | } 121 | t.Logf("GetCandleSnapshot() = %v", res) 122 | } 123 | 124 | func TestInfoAPI_GetMeta(t *testing.T) { 125 | api := GetInfoAPI() 126 | res, err := api.GetMeta() 127 | if err != nil { 128 | t.Errorf("GetMeta() error = %v", err) 129 | } 130 | t.Logf("GetMeta() = %v", res) 131 | if res.Universe[0].Name != "SOL" { 132 | t.Errorf("GetMeta() doesnt return %v, want %v", res.Universe[0].Name, "SOL") 133 | } 134 | } 135 | 136 | func TestInfoAPI_GetUserState(t *testing.T) { 137 | api := GetInfoAPI() 138 | res, err := api.GetAccountState() 139 | if err != nil { 140 | t.Errorf("GetUserState() error = %v", err) 141 | } 142 | if res.Withdrawable == 0 { 143 | t.Errorf("GetUserState.Withdrawable = %v, want > %v", res.Withdrawable, 0) 144 | } 145 | if res.CrossMarginSummary.AccountValue == 0 { 146 | t.Errorf("GetUserState.AccountValue = %v, want > %v", res.CrossMarginSummary.AccountValue, 0) 147 | } 148 | t.Logf("GetUserState() = %v", res) 149 | } 150 | 151 | func TestInfoAPI_GetAccountOpenOrders(t *testing.T) { 152 | api := GetInfoAPI() 153 | res, err := api.GetAccountOpenOrders() 154 | if err != nil { 155 | t.Errorf("GetAccountOpenOrders() error = %v", err) 156 | } 157 | if len(*res) == 0 { 158 | t.Errorf("GetAccountOpenOrders() len = %v, want > %v", res, 0) 159 | } 160 | t.Logf("GetAccountOpenOrders() = %v", res) 161 | } 162 | 163 | func TestInfoAPI_GetAccountFundingUpdates(t *testing.T) { 164 | api := GetInfoAPI() 165 | startTime, endTime := GetDefaultTimeRange() 166 | res, err := api.GetAccountFundingUpdates(startTime, endTime) 167 | if err != nil { 168 | t.Errorf("GetAccountFundingUpdates() error = %v", err) 169 | } 170 | if len(*res) == 0 { 171 | t.Errorf("GetAccountFundingUpdates() len = %v, want > %v", res, 0) 172 | } 173 | t.Logf("GetAccountFundingUpdates() = %v", res) 174 | } 175 | 176 | func TestInfoAPI_GetHistoricalFundingRates(t *testing.T) { 177 | api := GetInfoAPI() 178 | startTime, endTime := GetDefaultTimeRange() 179 | res, err := api.GetHistoricalFundingRates("BTC", startTime, endTime) 180 | if err != nil { 181 | t.Errorf("GetHistoricalFundingRates() error = %v", err) 182 | } 183 | if len(*res) == 0 { 184 | t.Errorf("GetHistoricalFundingRates() len = %v, want > %v", res, 0) 185 | } 186 | t.Logf("GetHistoricalFundingRates() = %v", res) 187 | } 188 | 189 | func TestInfoAPI_GetAccountNonFundingUpdates(t *testing.T) { 190 | api := GetInfoAPI() 191 | startTime, endTime := GetDefaultTimeRange() 192 | res, err := api.GetAccountNonFundingUpdates(startTime, endTime) 193 | if err != nil { 194 | t.Errorf("GetAccountNonFundingUpdates() error = %v", err) 195 | } 196 | if len(*res) == 0 { 197 | t.Errorf("GetAccountNonFundingUpdates() len = %v, want > %v", res, 0) 198 | } 199 | // find first deposit 200 | for _, update := range *res { 201 | if update.Delta.Type == "deposit" { 202 | // check that usdc is in the deposit 203 | if update.Delta.Usdc == 0 { 204 | t.Errorf("update.Delta.Usdc = %v, want > %v", update.Delta.Amount, 0) 205 | } 206 | } 207 | if update.Delta.Type == "withdrawal" { 208 | if update.Delta.Usdc == 0 { 209 | t.Errorf("update.Delta.Usdc = %v, want > %v", update.Delta.Amount, 0) 210 | } 211 | if update.Delta.Nonce == 0 { 212 | t.Errorf("update.Delta.Nonce = %v, want > %v", update.Delta.Nonce, 0) 213 | } 214 | if update.Delta.Fee == 0 { 215 | t.Errorf("update.Delta.Fee = %v, want > %v", update.Delta.Fee, 0) 216 | } 217 | } 218 | if update.Delta.Type == "spotGenesis" { 219 | if update.Delta.Token == "" { 220 | t.Errorf("update.Delta.Token = %v", update.Delta.Amount) 221 | } 222 | if update.Delta.Amount == 0 { 223 | t.Errorf("update.Delta.Amount = %v, want > %v", update.Delta.Amount, 0) 224 | } 225 | } 226 | if update.Delta.Type == "accountClassTransfer" { 227 | if update.Delta.Usdc == 0 { 228 | t.Errorf("update.Delta.Usdc = %v, want > %v", update.Delta.Amount, 0) 229 | } 230 | } 231 | } 232 | t.Logf("GetAccountNonFundingUpdates() = %v", res) 233 | } 234 | 235 | func TestInfoAPI_GetAccountWithdrawals(t *testing.T) { 236 | api := GetInfoAPI() 237 | res, err := api.GetAccountWithdrawals() 238 | if err != nil { 239 | t.Errorf("GetAccountWithdrawals() error = %v", err) 240 | } 241 | if len(*res) == 0 { 242 | t.Errorf("GetAccountWithdrawals() len = %v, want > %v", res, 0) 243 | } 244 | for _, withdrawal := range *res { 245 | if withdrawal.Amount == 0 { 246 | t.Errorf("withdrawal.Amount = %v, want > %v", withdrawal.Amount, 0) 247 | } 248 | } 249 | t.Logf("GetAccountWithdrawals() = %v", res) 250 | } 251 | 252 | func TestInfoAPI_GetAccountDeposits(t *testing.T) { 253 | api := GetInfoAPI() 254 | res, err := api.GetAccountDeposits() 255 | if err != nil { 256 | t.Errorf("GetAccountDeposits() error = %v", err) 257 | } 258 | if len(*res) == 0 { 259 | t.Errorf("GetAccountDeposits() len = %v, want > %v", res, 0) 260 | } 261 | for _, deposit := range *res { 262 | if deposit.Amount == 0 { 263 | t.Errorf("deposit.Amount = %v, want > %v", deposit.Amount, 0) 264 | } 265 | } 266 | t.Logf("GetAccountDeposits() = %v", res) 267 | } 268 | 269 | func TestInfoAPI_GetMarketPx(t *testing.T) { 270 | api := GetInfoAPI() 271 | res, err := api.GetMartketPx("BTC") 272 | if err != nil { 273 | t.Errorf("GetMartketPx() error = %v", err) 274 | } 275 | if res < 10_000 { 276 | t.Errorf("GetMartketPx() = %v, want > %v", res, 10_000) 277 | } 278 | t.Logf("GetMartketPx() = %v", res) 279 | } 280 | 281 | func TestInfoAPI_BuildMetaMap(t *testing.T) { 282 | api := GetInfoAPI() 283 | res, err := api.BuildMetaMap() 284 | if err != nil { 285 | t.Errorf("BuildMetaMap() error = %v", err) 286 | } 287 | if len(res) == 0 { 288 | t.Errorf("BuildMetaMap() = %v, want > %v", res, 0) 289 | } 290 | // check BTC, ETH in map 291 | if _, ok := res["BTC"]; !ok { 292 | t.Errorf("BuildMetaMap() = %v, want %v", res, "BTC") 293 | } 294 | if _, ok := res["ETH"]; !ok { 295 | t.Errorf("BuildMetaMap() = %v, want %v", res, "ETH") 296 | } 297 | t.Logf("BuildMetaMap() = %v", res) 298 | } 299 | 300 | func TestInfoAPI_BuildSpotMetaMap(t *testing.T) { 301 | api := GetInfoAPI() 302 | res, err := api.BuildSpotMetaMap() 303 | if err != nil { 304 | t.Errorf("BuildSpotMetaMap() error = %v", err) 305 | } 306 | if len(res) == 0 { 307 | t.Errorf("BuildSpotMetaMap() = %v, want > %v", res, 0) 308 | } 309 | // check PURR, HYPE in map 310 | if _, ok := res["PURR"]; !ok { 311 | t.Errorf("BuildSpotMetaMap() = %v, want %v", res, "PURR") 312 | } 313 | if _, ok := res["HYPE"]; !ok { 314 | t.Errorf("BuildSpotMetaMap() = %v, want %v", res, "HYPE") 315 | } 316 | t.Logf("map(PURR) = %+v", res["PURR"]) 317 | t.Logf("BuildSpotMetaMap() = %+v", res) 318 | } 319 | 320 | func TestInfoAPI_GetSpotMeta(t *testing.T) { 321 | api := GetInfoAPI() 322 | res, err := api.GetSpotMeta() 323 | if err != nil { 324 | t.Errorf("GetSpotMeta() error = %v", err) 325 | } 326 | if len(res.Tokens) == 0 { 327 | t.Errorf("GetSpotMeta() = %v, want > %v", res, 0) 328 | } 329 | t.Logf("GetSpotMeta() = %v", res) 330 | } 331 | 332 | func TestInfoAPI_GetAllSpotPrices(t *testing.T) { 333 | api := GetInfoAPI() 334 | res, err := api.GetAllSpotPrices() 335 | if err != nil { 336 | t.Errorf("GetAllSpotPrices() error = %v", err) 337 | } 338 | if len(*res) == 0 { 339 | t.Errorf("GetAllSpotPrices() = %v, want > %v", res, 0) 340 | } 341 | t.Logf("GetAllSpotPrices() = %+v", res) 342 | } 343 | 344 | func TestInfoAPI_GetSpotMarketPx(t *testing.T) { 345 | api := GetInfoAPI() 346 | res, err := api.GetSpotMarketPx("HYPE") 347 | if err != nil { 348 | t.Errorf("GetSpotMarketPx() error = %v", err) 349 | } 350 | if res < 0 { 351 | t.Errorf("GetSpotMarketPx() = %v, want > %v", res, 0) 352 | } 353 | t.Logf("GetSpotMarketPx(HYPE) = %v", res) 354 | } 355 | 356 | func TestInfoAPI_GetUserStateSpot(t *testing.T) { 357 | api := GetInfoAPI() 358 | res, err := api.GetAccountStateSpot() 359 | if err != nil { 360 | t.Errorf("GetUserStateSpot() error = %v", err) 361 | } 362 | if len(res.Balances) == 0 { 363 | t.Errorf("GetUserStateSpot() = %v, want > %v", res, 0) 364 | } 365 | t.Logf("GetUserStateSpot() = %+v", res) 366 | } 367 | -------------------------------------------------------------------------------- /hyperliquid/info_types.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | // Base request for /info 4 | type InfoRequest struct { 5 | User string `json:"user,omitempty"` 6 | Typez string `json:"type"` 7 | Oid string `json:"oid,omitempty"` 8 | Coin string `json:"coin,omitempty"` 9 | StartTime int64 `json:"startTime,omitempty"` 10 | EndTime int64 `json:"endTime,omitempty"` 11 | } 12 | 13 | type UserStateRequest struct { 14 | User string `json:"user"` 15 | Typez string `json:"type"` 16 | } 17 | 18 | type Asset struct { 19 | Name string `json:"name"` 20 | SzDecimals int `json:"szDecimals"` 21 | MaxLeverage int `json:"maxLeverage"` 22 | OnlyIsolated bool `json:"onlyIsolated"` 23 | } 24 | 25 | type UserState struct { 26 | Withdrawable float64 `json:"withdrawable,string"` 27 | CrossMaintenanceMarginUsed float64 `json:"crossMaintenanceMarginUsed,string"` 28 | AssetPositions []AssetPosition `json:"assetPositions"` 29 | CrossMarginSummary MarginSummary `json:"crossMarginSummary"` 30 | MarginSummary MarginSummary `json:"marginSummary"` 31 | Time int64 `json:"time"` 32 | } 33 | 34 | type AssetPosition struct { 35 | Position Position `json:"position"` 36 | Type string `json:"type"` 37 | } 38 | 39 | type Position struct { 40 | Coin string `json:"coin"` 41 | EntryPx float64 `json:"entryPx,string"` 42 | Leverage Leverage `json:"leverage"` 43 | LiquidationPx float64 `json:"liquidationPx,string"` 44 | MarginUsed float64 `json:"marginUsed,string"` 45 | PositionValue float64 `json:"positionValue,string"` 46 | ReturnOnEquity float64 `json:"returnOnEquity,string"` 47 | Szi float64 `json:"szi,string"` 48 | UnrealizedPnl float64 `json:"unrealizedPnl,string"` 49 | MaxLeverage int `json:"maxLeverage"` 50 | CumFunding struct { 51 | AllTime float64 `json:"allTime,string"` 52 | SinceOpne float64 `json:"sinceOpen,string"` 53 | SinceChan float64 `json:"sinceChange,string"` 54 | } `json:"cumFunding"` 55 | } 56 | 57 | type UserStateSpot struct { 58 | Balances []SpotAssetPosition `json:"balances"` 59 | } 60 | 61 | type SpotAssetPosition struct { 62 | /* 63 | "coin": "USDC", 64 | "token": 0, 65 | "hold": "0.0", 66 | "total": "14.625485", 67 | "entryNtl": "0.0" 68 | */ 69 | Coin string `json:"coin"` 70 | Token int `json:"token"` 71 | Hold float64 `json:"hold,string"` 72 | Total float64 `json:"total,string"` 73 | EntryNtl float64 `json:"entryNtl,string"` 74 | } 75 | 76 | type Order struct { 77 | Children []any `json:"children,omitempty"` 78 | Cloid string `json:"cloid,omitempty"` 79 | Coin string `json:"coin"` 80 | IsPositionTpsl bool `json:"isPositionTpsl,omitempty"` 81 | IsTrigger bool `json:"isTrigger,omitempty"` 82 | LimitPx float64 `json:"limitPx,string,omitempty"` 83 | Oid int64 `json:"oid"` 84 | OrderType string `json:"orderType,omitempty"` 85 | OrigSz float64 `json:"origSz,string,omitempty"` 86 | ReduceOnly bool `json:"reduceOnly,omitempty"` 87 | Side string `json:"side"` 88 | Sz float64 `json:"sz,string,omitempty"` 89 | Tif string `json:"tif,omitempty"` 90 | Timestamp int64 `json:"timestamp"` 91 | TriggerCondition string `json:"triggerCondition,omitempty"` 92 | TriggerPx float64 `json:"triggerPx,string,omitempty"` 93 | } 94 | 95 | type Leverage struct { 96 | Type string `json:"type"` 97 | Value int `json:"value"` 98 | } 99 | 100 | type MarginSummary struct { 101 | AccountValue float64 `json:"accountValue,string"` 102 | TotalMarginUsed float64 `json:"totalMarginUsed,string"` 103 | TotalNtlPos float64 `json:"totalNtlPos,string"` 104 | TotalRawUsd float64 `json:"totalRawUsd,string"` 105 | } 106 | 107 | type SpotMeta struct { 108 | Universe []struct { 109 | Tokens []int `json:"tokens"` 110 | Name string `json:"name"` 111 | Index int `json:"index"` 112 | IsCanonical bool `json:"isCanonical"` 113 | } `json:"universe"` 114 | Tokens []struct { 115 | Name string `json:"name"` 116 | SzDecimals int `json:"szDecimals"` 117 | WeiDecimals int `json:"weiDecimals"` 118 | Index int `json:"index"` 119 | TokenID string `json:"tokenId"` 120 | IsCanonical bool `json:"isCanonical"` 121 | EvmContract any `json:"evmContract"` 122 | FullName any `json:"fullName"` 123 | } `json:"tokens"` 124 | } 125 | 126 | type Meta struct { 127 | Universe []Asset `json:"universe"` 128 | } 129 | 130 | type OrderFill struct { 131 | Cloid string `json:"cloid"` 132 | ClosedPnl float64 `json:"closedPnl,string"` 133 | Coin string `json:"coin"` 134 | Crossed bool `json:"crossed"` 135 | Dir string `json:"dir"` 136 | Fee float64 `json:"fee,string"` 137 | FeeToken string `json:"feeToken"` 138 | Hash string `json:"hash"` 139 | Oid int `json:"oid"` 140 | Px float64 `json:"px,string"` 141 | Side string `json:"side"` 142 | StartPosition string `json:"startPosition"` 143 | Sz float64 `json:"sz,string"` 144 | Tid int64 `json:"tid"` 145 | Time int64 `json:"time"` 146 | Liquidation *Liquidation `json:"liquidation"` 147 | } 148 | 149 | type Context struct { 150 | DayNtlVlm string `json:"dayNtlVlm"` 151 | Funding string `json:"funding"` 152 | ImpactPxs []string `json:"impactPxs"` 153 | MarkPx string `json:"markPx"` 154 | MidPx string `json:"midPx"` 155 | OpenInterest string `json:"openInterest"` 156 | OraclePx string `json:"oraclePx"` 157 | Premium string `json:"premium"` 158 | PrevDayPx string `json:"prevDayPx"` 159 | } 160 | 161 | type HistoricalFundingRate struct { 162 | Coin string `json:"coin"` 163 | FundingRate string `json:"fundingRate"` 164 | Premium string `json:"premium"` 165 | Time int64 `json:"time"` 166 | } 167 | 168 | type L2BookSnapshot struct { 169 | Coin string `json:"coin"` 170 | Time int64 `json:"time"` 171 | Levels [][]struct { 172 | Px float64 `json:"px,string"` 173 | Sz float64 `json:"sz,string"` 174 | N int `json:"n"` 175 | } `json:"levels"` 176 | } 177 | 178 | type CandleSnapshotSubRequest struct { 179 | Coin string `json:"coin"` 180 | Interval string `json:"interval"` 181 | StartTime int64 `json:"startTime"` 182 | EndTime int64 `json:"endTime"` 183 | } 184 | 185 | type CandleSnapshotRequest struct { 186 | Typez string `json:"type"` 187 | Req CandleSnapshotSubRequest `json:"req"` 188 | } 189 | 190 | type CandleSnapshot struct { 191 | CloseTime int64 `json:"t"` 192 | OpenTime int64 `json:"T"` 193 | Symbol string `json:"s"` 194 | Interval string `json:"i"` 195 | Open float64 `json:"o,string"` 196 | Close float64 `json:"c,string"` 197 | High float64 `json:"h,string"` 198 | Low float64 `json:"l,string"` 199 | Volume float64 `json:"v,string"` 200 | N int `json:"n"` 201 | } 202 | 203 | type NonFundingUpdate struct { 204 | Hash string `json:"hash"` 205 | Time int64 `json:"time"` 206 | Delta NonFundingDelta `json:"delta"` 207 | } 208 | 209 | type FundingUpdate struct { 210 | Hash string `json:"hash"` 211 | Time int64 `json:"time"` 212 | Delta FundingDelta `json:"delta"` 213 | } 214 | 215 | type RatesLimits struct { 216 | CumVlm float64 `json:"cumVlm,string"` 217 | NRequestsUsed int `json:"nRequestsUsed"` 218 | NRequestsCap int `json:"nRequestsCap"` 219 | } 220 | 221 | type SpotMetaAndAssetCtxsResponse [2]interface{} // Array of exactly 2 elements 222 | 223 | type Market struct { 224 | PrevDayPx string `json:"prevDayPx,omitempty"` 225 | DayNtlVlm string `json:"dayNtlVlm,omitempty"` 226 | MarkPx string `json:"markPx,omitempty"` 227 | MidPx string `json:"midPx,omitempty"` 228 | CirculatingSupply string `json:"circulatingSupply,omitempty"` 229 | Coin string `json:"coin,omitempty"` 230 | TotalSupply string `json:"totalSupply,omitempty"` 231 | DayBaseVlm string `json:"dayBaseVlm,omitempty"` 232 | } 233 | -------------------------------------------------------------------------------- /hyperliquid/pk_manager.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/crypto" 8 | ) 9 | 10 | type PKeyManager struct { 11 | PrivateKeyStr string 12 | privateKey *ecdsa.PrivateKey 13 | publicKey *ecdsa.PublicKey 14 | } 15 | 16 | func (km *PKeyManager) PublicECDSA() *ecdsa.PublicKey { 17 | return km.publicKey 18 | } 19 | 20 | func (km *PKeyManager) PrivateECDSA() *ecdsa.PrivateKey { 21 | return km.privateKey 22 | } 23 | 24 | func (km *PKeyManager) PublicAddress() common.Address { 25 | return crypto.PubkeyToAddress(*km.publicKey) 26 | } 27 | 28 | func (km *PKeyManager) PublicAddressHex() string { 29 | return km.PublicAddress().Hex() 30 | } 31 | 32 | // NewPKeyManager creates a new PKeyManager instance from a private key string 33 | func NewPKeyManager(privateKey string) (*PKeyManager, error) { 34 | privKey, err := crypto.HexToECDSA(privateKey) 35 | if err != nil { 36 | return nil, err 37 | } 38 | publicKey, ok := privKey.Public().(*ecdsa.PublicKey) 39 | if !ok { 40 | return nil, err 41 | } 42 | return &PKeyManager{privateKey: privKey, publicKey: publicKey, PrivateKeyStr: privateKey}, nil 43 | } 44 | -------------------------------------------------------------------------------- /hyperliquid/signature.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/common/math" 10 | "github.com/ethereum/go-ethereum/crypto" 11 | "github.com/ethereum/go-ethereum/signer/core/apitypes" 12 | "github.com/vmihailenco/msgpack/v5" 13 | ) 14 | 15 | // SignRequest is the implementation of EIP-712 typed data 16 | type SignRequest struct { 17 | PrimaryType string 18 | DType []apitypes.Type 19 | DTypeMsg map[string]interface{} 20 | IsMainNet bool 21 | DomainName string 22 | } 23 | 24 | func (request *SignRequest) getChainId() *math.HexOrDecimal256 { 25 | if request.DomainName == "HyperliquidSignTransaction" { 26 | if request.IsMainNet { 27 | return math.NewHexOrDecimal256(int64(ARBITRUM_CHAIN_ID)) 28 | } 29 | return math.NewHexOrDecimal256(int64(ARBITRUM_TESTNET_CHAIN_ID)) 30 | } 31 | return math.NewHexOrDecimal256(int64(HYPERLIQUID_CHAIN_ID)) 32 | } 33 | 34 | func (request *SignRequest) GetTypes() apitypes.Types { 35 | types := apitypes.Types{ 36 | request.PrimaryType: request.DType, 37 | "EIP712Domain": { 38 | {Name: "name", Type: "string"}, 39 | {Name: "version", Type: "string"}, 40 | {Name: "chainId", Type: "uint256"}, 41 | {Name: "verifyingContract", Type: "address"}, 42 | }, 43 | } 44 | return types 45 | } 46 | 47 | func (request *SignRequest) GetDomain() apitypes.TypedDataDomain { 48 | return apitypes.TypedDataDomain{ 49 | Name: request.DomainName, 50 | Version: "1", 51 | ChainId: request.getChainId(), 52 | VerifyingContract: VERIFYING_CONTRACT, 53 | } 54 | } 55 | 56 | type Signer struct { 57 | manager *PKeyManager 58 | } 59 | 60 | func NewSigner(manager *PKeyManager) Signer { 61 | return Signer{ 62 | manager: manager, 63 | } 64 | } 65 | 66 | func (signer *Signer) Sign(request *SignRequest) (byte, [32]byte, [32]byte, error) { 67 | return signer.signInternal(SignRequestToEIP712TypedData(request)) 68 | } 69 | 70 | // signInternal signs the typed data and returns the signature in VRS format 71 | func (signer *Signer) signInternal(message apitypes.TypedData) (byte, [32]byte, [32]byte, error) { 72 | pkey := signer.manager.PrivateECDSA() 73 | bytes, _, err := apitypes.TypedDataAndHash(message) 74 | if err != nil { 75 | log.Printf("Error hashing typed data: %s", err) 76 | return 0, [32]byte{}, [32]byte{}, err 77 | } 78 | signature, err := crypto.Sign(bytes, pkey) 79 | if err != nil { 80 | log.Printf("Error signing typed data: %s", err) 81 | return 0, [32]byte{}, [32]byte{}, err 82 | } 83 | return SignatureToVRS(signature) 84 | } 85 | 86 | func SignRequestToEIP712TypedData(request *SignRequest) apitypes.TypedData { 87 | return apitypes.TypedData{ 88 | Domain: request.GetDomain(), 89 | Types: request.GetTypes(), 90 | PrimaryType: request.PrimaryType, 91 | Message: request.DTypeMsg, 92 | } 93 | } 94 | 95 | func SignatureToVRS(sig []byte) (byte, [32]byte, [32]byte, error) { 96 | var v byte 97 | var r [32]byte 98 | var s [32]byte 99 | v = sig[64] + 27 100 | copy(r[:], sig[:32]) 101 | copy(s[:], sig[32:64]) 102 | return v, r, s, nil 103 | } 104 | 105 | // Create a hash of an action (json object) 106 | func buildActionHash(action any, vaultAd string, nonce uint64) (common.Hash, error) { 107 | data, err := msgpack.Marshal(action) 108 | if err != nil { 109 | return common.Hash{}, fmt.Errorf("error while marshaling action: %s", err) 110 | } 111 | nonceBytes := make([]byte, 8) 112 | binary.BigEndian.PutUint64(nonceBytes, uint64(nonce)) 113 | data = ArrayAppend(data, nonceBytes) 114 | 115 | if vaultAd == "" { 116 | data = ArrayAppend(data, []byte("\x00")) 117 | } else { 118 | data = ArrayAppend(data, []byte("\x01")) 119 | data = ArrayAppend(data, HexToBytes(vaultAd)) 120 | } 121 | result := crypto.Keccak256Hash(data) 122 | return result, nil 123 | } 124 | 125 | func getNetSource(isMainnet bool) string { 126 | if isMainnet { 127 | return "a" 128 | } else { 129 | return "b" 130 | } 131 | } 132 | 133 | // Build a message to sign 134 | func buildMessage(hash []byte, isMainnet bool) apitypes.TypedDataMessage { 135 | source := getNetSource(isMainnet) 136 | return apitypes.TypedDataMessage{ 137 | "source": source, 138 | "connectionId": hash, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /hyperliquid/utils.go: -------------------------------------------------------------------------------- 1 | package hyperliquid 2 | 3 | import ( 4 | "crypto/rand" 5 | "strconv" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/ethereum/go-ethereum/common/hexutil" 10 | ) 11 | 12 | // global nonce counter 13 | var nonceCounter = time.Now().UnixMilli() 14 | 15 | // Hyperliquid uses timestamps in milliseconds for nonce 16 | // GetNonce returns a unique nonce that is always at least the current time in milliseconds. 17 | // It ensures thread-safe updates using atomic operations. 18 | func GetNonce() uint64 { 19 | now := time.Now().UnixMilli() 20 | for { 21 | // Load the current nonce value atomically. 22 | current := atomic.LoadInt64(&nonceCounter) 23 | 24 | // If the current time is greater than the stored nonce, 25 | // attempt to update the nonce to the current time. 26 | if current < now { 27 | if atomic.CompareAndSwapInt64(&nonceCounter, current, now) { 28 | return uint64(now) 29 | } 30 | // If the swap fails, retry. 31 | continue 32 | } 33 | 34 | // Otherwise, increment the nonce by one. 35 | newNonce := current + 1 36 | if atomic.CompareAndSwapInt64(&nonceCounter, current, newNonce) { 37 | return uint64(newNonce) 38 | } 39 | } 40 | } 41 | 42 | // Retruns a random cloid (Client Order ID) 43 | func GetRandomCloid() string { 44 | buf := make([]byte, 16) 45 | // then we can call rand.Read. 46 | _, err := rand.Read(buf) 47 | if err != nil { 48 | return "" 49 | } 50 | return hexutil.Encode(buf) 51 | } 52 | 53 | // Calculate the slippage of a trade 54 | func CalculateSlippage(isBuy bool, px float64, slippage float64) float64 { 55 | if isBuy { 56 | px = px * (1 + slippage) 57 | } else { 58 | px = px * (1 - slippage) 59 | } 60 | // Format the float with a precision of 6 significant figures 61 | pxStr := strconv.FormatFloat(px, 'g', 5, 64) 62 | // Convert the formatted string to a float 63 | pxFloat, err := strconv.ParseFloat(pxStr, 64) 64 | if err != nil { 65 | return px 66 | } 67 | // Round the float to 6 decimal places 68 | return pxFloat 69 | } 70 | 71 | func IsBuy(szi float64) bool { 72 | if szi > 0 { 73 | return true 74 | } else { 75 | return false 76 | } 77 | } 78 | 79 | // Get the slippage of a trade 80 | // Returns the default slippage if the slippage is nil 81 | func GetSlippage(sl *float64) float64 { 82 | slippage := DEFAULT_SLIPPAGE 83 | if sl != nil { 84 | slippage = *sl 85 | } 86 | return slippage 87 | } 88 | 89 | // Returns default time range of 90 days 90 | // Returns the start time and end time in milliseconds 91 | func GetDefaultTimeRange() (int64, int64) { 92 | endTime := time.Now().UnixMilli() 93 | startTime := time.Now().AddDate(0, 0, -90).UnixMilli() 94 | return startTime, endTime 95 | } 96 | --------------------------------------------------------------------------------