diff --git a/package.json b/package.json index 6ab7364fef..c0434bc1cb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@google-cloud/storage": "^6.10.1", "@maticnetwork/maticjs": "^3.6.0", "@maticnetwork/maticjs-ethers": "^1.0.3", + "@nktkas/hyperliquid": "^0.25.4", "@openzeppelin/hardhat-upgrades": "^1.28.0", "@solana-program/address-lookup-table": "^0.7.0", "@solana-program/compute-budget": "^0.8.0", diff --git a/src/utils/HyperliquidUtils.ts b/src/utils/HyperliquidUtils.ts new file mode 100644 index 0000000000..1bed3e246e --- /dev/null +++ b/src/utils/HyperliquidUtils.ts @@ -0,0 +1,76 @@ +import { Signer, isSignerWallet, assert, delay } from "./"; +import * as hl from "@nktkas/hyperliquid"; + +export function getDefaultHlTransport(extraOptions: ConstructorParameters = []) { + return new hl.HttpTransport(...extraOptions); +} + +export function getHlExchangeClient( + signer: Signer, + transport: hl.HttpTransport | hl.WebSocketTransport = getDefaultHlTransport() +) { + assert( + isSignerWallet(signer), + "HyperliquidUtils#getHlExchangeClient: Cannot define an exchange client without a wallet." + ); + return new hl.ExchangeClient({ wallet: signer, transport }); +} + +export function getHlInfoClient(transport: hl.HttpTransport | hl.WebSocketTransport = getDefaultHlTransport()) { + return new hl.InfoClient({ transport }); +} + +export async function getL2Book( + infoClient: hl.InfoClient, + params: hl.L2BookParameters, + nRetries = 0, + maxRetries = 3 +): Promise { + return await _callWithRetry(infoClient.l2Book.bind(infoClient), [params], nRetries, maxRetries); +} + +export async function getOpenOrders( + infoClient: hl.InfoClient, + params: hl.OpenOrdersParameters, + nRetries = 0, + maxRetries = 3 +): Promise { + return _callWithRetry(infoClient.openOrders.bind(infoClient), [params], nRetries, maxRetries); +} + +export async function getHistoricalOrders( + infoClient: hl.InfoClient, + params: hl.HistoricalOrdersParameters, + nRetries = 0, + maxRetries = 3 +): Promise { + return _callWithRetry(infoClient.historicalOrders.bind(infoClient), [params], nRetries, maxRetries); +} + +export async function getOrderStatus( + infoClient: hl.InfoClient, + params: hl.OrderStatusParameters, + nRetries = 0, + maxRetries = 3 +): Promise { + return _callWithRetry(infoClient.orderStatus.bind(infoClient), [params], nRetries, maxRetries); +} + +async function _callWithRetry( + apiCall: (...args: A) => Promise, + args: A, + nRetries: number, + maxRetries: number +): Promise { + try { + return await apiCall(...args); + } catch (e) { + if (nRetries > maxRetries) { + throw new Error(`Max retries exceeded when querying the hyperliquid API: ${e}`); + } + const delaySeconds = 2 ** nRetries + Math.random(); + await delay(delaySeconds); + // @todo Once we have a better idea on the types of errors we can suppress/retry on, then choose appropriate action based on the error thrown. + return await _callWithRetry(apiCall, args, ++nRetries, maxRetries); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index a0a0f10a7d..b64de2742e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -78,3 +78,4 @@ export * from "./BNUtils"; export * from "./CCTPUtils"; export * from "./RetryUtils"; export * from "./BinanceUtils"; +export * from "./HyperliquidUtils"; diff --git a/yarn.lock b/yarn.lock index c6135a6a79..6adcee9269 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1589,11 +1589,27 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@msgpack/msgpack@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" + integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== + "@multiformats/base-x@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== +"@nktkas/hyperliquid@^0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@nktkas/hyperliquid/-/hyperliquid-0.25.4.tgz#1d29e85e4ef6e6d02fffcb15293c582757948620" + integrity sha512-o2qGBfB+8pRwfx1ypug7pH70utIwiNt2efsgRqWHjjvd/ZAj10o9et9i9QsFuQMywif2QuFCPfWYHoSF8YQZqg== + dependencies: + "@msgpack/msgpack" "^3.1.2" + "@noble/hashes" "^2.0.0" + "@noble/secp256k1" "^3.0.0" + typescript-event-target "1.1.1" + valibot "1.1.0" + "@noble/ciphers@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" @@ -1671,11 +1687,21 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== +"@noble/hashes@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" + integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + "@noble/secp256k1@1.5.5", "@noble/secp256k1@~1.5.2": version "1.5.5" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.5.5.tgz#315ab5745509d1a8c8e90d0bdf59823ccf9bcfc3" integrity sha512-sZ1W6gQzYnu45wPrWx8D3kwI2/U29VYTx9OjbDAd7jwRItJ0cSTMPRL/C8AWZFn9kWFLQGqEXVEE86w4Z8LpIQ== +"@noble/secp256k1@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-3.0.0.tgz#29711361db8f37b1b7e0b8d80c933013fc887475" + integrity sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -16266,6 +16292,11 @@ typeorm@^0.3.20: uuid "^11.1.0" yargs "^17.7.2" +typescript-event-target@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/typescript-event-target/-/typescript-event-target-1.1.1.tgz#20a6d491b77d2e37dc432c5394ab74c0d7065539" + integrity sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg== + typescript@^5.9: version "5.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" @@ -16549,6 +16580,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +valibot@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.1.0.tgz#873bb1af9e1577391690307bfe0520bd1360ec2d" + integrity sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw== + validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"