├── test ├── test_helper.exs └── hyperliquid_test.exs ├── .gitattributes ├── .formatter.exs ├── lib ├── hyperliquid.ex └── hyperliquid │ ├── application.ex │ ├── cache │ ├── updater.ex │ └── cache.ex │ ├── utils │ ├── config.ex │ ├── interval.ex │ ├── utils.ex │ ├── signer.ex │ └── encoder.ex │ ├── streamer │ ├── supervisor.ex │ └── stream.ex │ ├── orders │ ├── price_converter.ex │ ├── order_wire.ex │ └── orders.ex │ ├── api │ ├── api.ex │ ├── explorer.ex │ ├── subscription.ex │ ├── info.ex │ └── exchange.ex │ └── manager.ex ├── config ├── dev.exs ├── test.exs └── config.exs ├── CHANGELOG.md ├── .gitignore ├── LICENSE.md ├── mix.exs ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/hyperliquid.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid do 2 | @moduledoc """ 3 | Documentation for `Hyperliquid`. 4 | """ 5 | 6 | end 7 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hyperliquid, 4 | ws_url: "wss://api.hyperliquid.xyz/ws", 5 | http_url: "https://api.hyperliquid.xyz" 6 | -------------------------------------------------------------------------------- /test/hyperliquid_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HyperliquidTest do 2 | use ExUnit.Case 3 | doctest Hyperliquid 4 | 5 | test "greets the world" do 6 | assert Hyperliquid.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hyperliquid, 4 | ws_url: "wss://api.hyperliquid-testnet.xyz/ws", 5 | http_url: "https://api.hyperliquid-testnet.xyz", 6 | hl_bridge_contract: "0x1870dc7a474e045026f9ef053d5bb20a250cc084" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.1.3 - added functions to cache for easier access and allow intelisense to help you see whats available 2 | 0.1.4 - added nSigFigs and mantissa optional params to l2Book subscription, add streamer pid to msg 3 | 0.1.5 - added new userFillsByTime endpoint to info context 4 | 0.1.6 - updated l2Book post req to include sigFig and mantissa values -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hyperliquid, 4 | is_mainnet: Mix.env() != :test, 5 | ws_url: "wss://api.hyperliquid.xyz/ws", 6 | http_url: "https://api.hyperliquid.xyz", 7 | hl_bridge_contract: "0x2df1c51e09aecf9cacb7bc98cb1742757f163df7", 8 | private_key: "YOUR_KEY_HERE" 9 | 10 | config :hyperliquid, Hyperliquid.Cache.Updater, 11 | update_interval: :timer.minutes(5) 12 | 13 | import_config "#{Mix.env()}.exs" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hyperliquid-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /lib/hyperliquid/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | @workers :worker_registry 7 | @users :user_registry 8 | @cache :hyperliquid 9 | 10 | @impl true 11 | def start(_type, _args) do 12 | children = [ 13 | {Phoenix.PubSub, name: Hyperliquid.PubSub}, 14 | {Registry, [keys: :unique, name: @workers]}, 15 | {Registry, [keys: :unique, name: @users]}, 16 | {Cachex, name: @cache}, 17 | {Hyperliquid.Cache.Updater, []}, 18 | Hyperliquid.Streamer.Supervisor, 19 | Hyperliquid.Manager 20 | ] 21 | 22 | opts = [strategy: :one_for_one, name: Hyperliquid.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/hyperliquid/cache/updater.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Cache.Updater do 2 | use GenServer 3 | alias Hyperliquid.Cache 4 | 5 | def start_link(_opts) do 6 | GenServer.start_link(__MODULE__, %{}, name: __MODULE__) 7 | end 8 | 9 | def init(state) do 10 | schedule_update() 11 | {:ok, state} 12 | end 13 | 14 | def handle_info(:update_cache, state) do 15 | Cache.init() 16 | schedule_update() 17 | {:noreply, state} 18 | end 19 | 20 | defp schedule_update do 21 | interval = Application.get_env(:hyperliquid, __MODULE__)[:update_interval] || :timer.minutes(5) 22 | Process.send_after(self(), :update_cache, interval) 23 | end 24 | 25 | def set_update_interval(interval) when is_integer(interval) and interval > 0 do 26 | :ok = Application.put_env(:hyperliquid, __MODULE__, update_interval: interval) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/hyperliquid/utils/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Config do 2 | @moduledoc """ 3 | Configuration module for Hyperliquid application. 4 | """ 5 | 6 | @doc """ 7 | Returns the base URL of the API. 8 | """ 9 | def api_base do 10 | Application.get_env(:hyperliquid, :http_url, "https://api.hyperliquid.xyz") 11 | end 12 | 13 | @doc """ 14 | Returns the ws URL of the API. 15 | """ 16 | def ws_url do 17 | Application.get_env(:hyperliquid, :ws_url, "wss://api.hyperliquid.xyz/ws") 18 | end 19 | 20 | @doc """ 21 | Returns whether the application is running on mainnet. 22 | """ 23 | def mainnet? do 24 | Application.get_env(:hyperliquid, :is_mainnet, true) 25 | end 26 | 27 | @doc """ 28 | Returns the private key. 29 | """ 30 | def secret do 31 | Application.get_env(:hyperliquid, :private_key, nil) 32 | end 33 | 34 | @doc """ 35 | Returns the bridge contract address, used for deposits. 36 | """ 37 | def bridge_contract do 38 | Application.get_env(:hyperliquid, :hl_bridge_contract, "0x2df1c51e09aecf9cacb7bc98cb1742757f163df7") 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Steven Kedzior 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/skedzior/hyperliquid" 5 | @version "0.1.6" 6 | 7 | def project do 8 | [ 9 | app: :hyperliquid, 10 | version: @version, 11 | elixir: "~> 1.16", 12 | start_permanent: Mix.env() == :prod, 13 | package: package(), 14 | deps: deps(), 15 | docs: docs() 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger, :runtime_tools, :wx, :observer], 23 | mod: {Hyperliquid.Application, []} 24 | ] 25 | end 26 | 27 | defp package do 28 | [ 29 | description: "Elixir api wrapper for the Hyperliquid exchange", 30 | maintainers: ["Steven Kedzior"], 31 | licenses: ["MIT"], 32 | links: %{"GitHub" => @source_url} 33 | ] 34 | end 35 | 36 | # Run "mix help deps" to learn about dependencies. 37 | defp deps do 38 | [ 39 | {:phoenix_pubsub, "~> 2.1"}, 40 | {:httpoison, "~> 1.7"}, 41 | {:jason, "~> 1.4"}, 42 | {:websockex, "~> 0.4.3"}, 43 | {:cachex, "~> 3.6"}, 44 | {:ex_eip712, "~> 0.3.0"}, 45 | {:ethers, "~> 0.4.5"}, 46 | {:msgpax, "~> 2.4"}, 47 | {:ex_doc, "~> 0.31", only: :dev, runtime: false}, 48 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false} 49 | ] 50 | end 51 | 52 | defp docs do 53 | [ 54 | extras: [ 55 | "CHANGELOG.md": [title: "Changelog"], 56 | "LICENSE.md": [title: "License"], 57 | "README.md": [title: "Overview"] 58 | ], 59 | main: "readme", 60 | source_url: @source_url, 61 | formatters: ["html"] 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/hyperliquid/streamer/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Streamer.Supervisor do 2 | @moduledoc """ 3 | Supervisor for WebSocket stream processes in the Hyperliquid application. 4 | 5 | This module implements a dynamic supervisor that manages WebSocket stream 6 | processes. It allows for dynamically starting and stopping stream processes, 7 | providing flexibility in managing multiple WebSocket connections. 8 | 9 | ## Key Features 10 | 11 | - Dynamically supervises WebSocket stream processes 12 | - Provides functions to start and stop individual stream processes 13 | - Allows querying of currently supervised children 14 | 15 | ## Usage 16 | 17 | This supervisor is typically started as part of your application's supervision tree. 18 | It can then be used to dynamically manage WebSocket stream processes. 19 | 20 | Example: 21 | 22 | # Start the supervisor 23 | {:ok, pid} = Hyperliquid.Streamer.Supervisor.start_link() 24 | 25 | # Start a new stream process 26 | {:ok, stream_pid} = Hyperliquid.Streamer.Supervisor.start_stream([%{type: "allMids"}]) 27 | 28 | # Stop a stream process 29 | :ok = Hyperliquid.Streamer.Supervisor.stop_child(stream_pid) 30 | 31 | Note: This supervisor uses a :one_for_one strategy, meaning each child is 32 | supervised independently. 33 | """ 34 | use DynamicSupervisor 35 | 36 | alias Hyperliquid.Streamer.Stream 37 | 38 | def start_link(_args) do 39 | DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) 40 | end 41 | 42 | def init(_arg) do 43 | DynamicSupervisor.init(strategy: :one_for_one) 44 | end 45 | 46 | def children do 47 | DynamicSupervisor.which_children(__MODULE__) 48 | end 49 | 50 | def start_stream(args \\ []) do 51 | DynamicSupervisor.start_child(__MODULE__, {Stream, args}) 52 | end 53 | 54 | def stop_child(pids) when is_list(pids), do: Enum.map(pids, &stop_child(&1)) 55 | 56 | def stop_child(pid) when is_pid(pid) do 57 | DynamicSupervisor.terminate_child(__MODULE__, pid) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/hyperliquid/utils/interval.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Interval do 2 | @moduledoc """ 3 | Provides helper functions for handling time intervals supported by Hyperliquid. 4 | 5 | This module offers utilities for working with various time intervals, from 6 | 1 minute to 1 month. It includes functions to list supported intervals, 7 | convert intervals to milliseconds, and calculate the next interval start time. 8 | 9 | ## Supported Intervals 10 | 11 | The following intervals are supported: 12 | 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 1d, 3d, 1w, 1M 13 | 14 | ## Usage 15 | 16 | You can use this module to: 17 | - Get a list of supported intervals 18 | - Convert intervals to milliseconds 19 | - Calculate the next start time for a given interval 20 | 21 | Example: 22 | 23 | iex> Hyperliquid.Interval.list() 24 | ["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w", "1M"] 25 | 26 | iex> Hyperliquid.Interval.to_milliseconds("1h") 27 | 3600000 28 | 29 | iex> current_time = :os.system_time(:millisecond) 30 | iex> Hyperliquid.Interval.next_start(current_time, "15m") 31 | # Returns the next 15-minute interval start time 32 | """ 33 | 34 | @minute 60_000 35 | @hour @minute * 60 36 | @day @hour * 24 37 | @week @day * 7 38 | @month trunc(@day * 30.44) 39 | 40 | def list, do: 41 | [ 42 | "1m", 43 | "3m", 44 | "5m", 45 | "15m", 46 | "30m", 47 | "1h", 48 | "2h", 49 | "4h", 50 | "8h", 51 | "12h", 52 | "1d", 53 | "3d", 54 | "1w", 55 | "1M" 56 | ] 57 | 58 | @doc """ 59 | Converts interval to milliseconds. 60 | """ 61 | def to_milliseconds(interval) do 62 | case interval do 63 | "1m" -> @minute 64 | "3m" -> @minute * 3 65 | "5m" -> @minute * 5 66 | "15m" -> @minute * 15 67 | "30m" -> @minute * 30 68 | "1h" -> @hour 69 | "2h" -> @hour * 2 70 | "4h" -> @hour * 4 71 | "8h" -> @hour * 8 72 | "12h" -> @hour * 12 73 | "1d" -> @day 74 | "3d" -> @day * 3 75 | "1w" -> @week 76 | "1M" -> @month 77 | _ -> {:error, "Unsupported interval"} 78 | end 79 | end 80 | 81 | @doc """ 82 | Rounds the current time up to the nearest interval period specified in milliseconds. 83 | """ 84 | def next_start(time, interval) do 85 | interval_period = to_milliseconds(interval) 86 | remainder = rem(time, interval_period) 87 | 88 | if remainder == 0 do 89 | time 90 | else 91 | time + (interval_period - remainder) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/hyperliquid/utils/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Utils do 2 | @moduledoc """ 3 | Provides utility functions for the Hyperliquid application. 4 | 5 | This module offers a collection of helper functions that are used across the 6 | Hyperliquid application. It includes utilities for data manipulation, 7 | PubSub operations, number formatting, random ID generation, and hexadecimal 8 | conversions. 9 | 10 | ## Key Features 11 | 12 | - Atomize keys in data structures 13 | - PubSub subscription and broadcasting 14 | - Number to string conversions with special float handling 15 | - Random client order ID (cloid) generation 16 | - Hexadecimal string manipulations 17 | - Timestamp generation 18 | """ 19 | 20 | @pubsub Hyperliquid.PubSub 21 | 22 | def subscribe(channel) do 23 | Phoenix.PubSub.subscribe(@pubsub, channel) 24 | end 25 | 26 | def numbers_to_strings(struct, fields) do 27 | Enum.reduce(fields, struct, fn field, acc -> 28 | value = Map.get(acc, field) 29 | Map.put(acc, field, float_to_string(value)) 30 | end) 31 | end 32 | 33 | def float_to_string(value) when is_float(value) do 34 | if value == trunc(value) do 35 | Integer.to_string(trunc(value)) 36 | else 37 | Float.to_string(value) 38 | end 39 | end 40 | 41 | def float_to_string(value) when is_integer(value) do 42 | Integer.to_string(value) 43 | end 44 | 45 | def float_to_string(value) when is_binary(value) do 46 | case Float.parse(value) do 47 | {float_value, ""} -> float_to_string(float_value) 48 | :error -> value 49 | end 50 | end 51 | 52 | def make_cloid do 53 | :crypto.strong_rand_bytes(16) 54 | |> Base.encode16(case: :lower) 55 | end 56 | 57 | def hex_string_to_integer(hex_string) do 58 | hex_string 59 | |> String.trim_leading("0x") 60 | |> Base.decode16!(case: :lower) 61 | |> :binary.decode_unsigned() 62 | end 63 | 64 | def to_hex(number) when is_nil(number), do: nil 65 | 66 | def to_hex(number) when is_number(number) do 67 | Integer.to_string(number, 16) 68 | |> String.downcase() 69 | |> then(&"0x#{&1}") 70 | end 71 | 72 | def to_full_hex(number) when is_number(number) do 73 | Integer.to_string(number, 16) 74 | |> String.downcase() 75 | |> then(&"0x#{String.duplicate("0", 40 - String.length(&1))}#{&1}") 76 | end 77 | 78 | def trim_0x(nil), do: nil 79 | def trim_0x(string), do: Regex.replace(~r/^0x/, string, "") 80 | 81 | def get_timestamp, do: :os.system_time(:millisecond) 82 | 83 | @doc """ 84 | Utils for converting map keys to atoms. 85 | """ 86 | def atomize_keys(data) when is_map(data) do 87 | Enum.reduce(data, %{}, fn {key, value}, acc -> 88 | atom_key = if is_binary(key), do: String.to_atom(key), else: key 89 | Map.put(acc, atom_key, atomize_keys(value)) 90 | end) 91 | end 92 | 93 | def atomize_keys(data) when is_list(data) do 94 | Enum.map(data, &atomize_keys/1) 95 | end 96 | 97 | def atomize_keys({key, value}) when is_binary(key) do 98 | atom_key = String.to_atom(key) 99 | {atom_key, atomize_keys(value)} 100 | end 101 | 102 | def atomize_keys({key, value}) do 103 | {key, atomize_keys(value)} 104 | end 105 | 106 | def atomize_keys(data), do: data 107 | end 108 | -------------------------------------------------------------------------------- /lib/hyperliquid/orders/price_converter.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Orders.PriceConverter do 2 | @moduledoc """ 3 | Provides helper methods for converting prices to the proper significant figures and decimal places. 4 | 5 | This module offers functionality to convert prices for both perpetual (perp) and spot markets 6 | in the Hyperliquid system. It ensures that prices are formatted correctly with the appropriate 7 | number of significant figures and decimal places based on the market type. 8 | 9 | Key features: 10 | - Converts prices for both perpetual and spot markets 11 | - Handles input as strings, floats, or integers 12 | - Rounds to 5 significant figures for both market types 13 | - Limits decimal places to 6 for perpetual markets and 8 for spot markets 14 | - Provides error handling for invalid inputs or conversion failures 15 | 16 | Usage: 17 | The main function `convert_price/2` takes a price value and an optional market type 18 | (defaulting to `:perp`). It returns either `{:ok, formatted_price}` or `{:error, reason}`. 19 | 20 | Example: 21 | iex> Hyperliquid.Orders.PriceConverter.convert_price(1234.56789, :perp) 22 | {:ok, "1234.57"} 23 | 24 | iex> Hyperliquid.Orders.PriceConverter.convert_price("0.00012345", :spot) 25 | {:ok, "0.00012345"} 26 | """ 27 | def convert_price(price, type \\ :perp) 28 | def convert_price(price, type) when type in [:perp, :spot] do 29 | cond do 30 | is_binary(price) -> 31 | case Float.parse(price) do 32 | {float_value, ""} -> convert_significant_figures_and_decimals(float_value, type) 33 | :error -> {:error, "Invalid number format"} 34 | end 35 | 36 | is_float(price) -> 37 | convert_significant_figures_and_decimals(price, type) 38 | 39 | is_integer(price) -> 40 | convert_significant_figures_and_decimals(price, type) 41 | 42 | true -> 43 | {:error, "Unsupported price format"} 44 | end 45 | end 46 | 47 | defp convert_significant_figures_and_decimals(value, :perp) do 48 | rounded_value = round_to_significant_figures(value, 5) 49 | rounded_value = round_to_decimal_places(rounded_value, 6) 50 | 51 | if valid_decimal_places?(rounded_value, 6) do 52 | {:ok, Float.to_string(rounded_value)} 53 | else 54 | {:error, "Unable to convert to valid perp price"} 55 | end 56 | end 57 | 58 | defp convert_significant_figures_and_decimals(value, :spot) do 59 | rounded_value = round_to_significant_figures(value, 5) 60 | rounded_value = round_to_decimal_places(rounded_value, 8) 61 | 62 | if valid_decimal_places?(rounded_value, 8) do 63 | {:ok, Float.to_string(rounded_value)} 64 | else 65 | {:error, "Unable to convert to valid spot price"} 66 | end 67 | end 68 | 69 | defp round_to_significant_figures(0, _sig_figs), do: 0 70 | 71 | defp round_to_significant_figures(value, sig_figs) do 72 | power = :math.pow(10, sig_figs - :math.ceil(:math.log10(abs(value)))) 73 | round(value * power) / power 74 | end 75 | 76 | defp round_to_decimal_places(value, decimal_places) do 77 | factor = :math.pow(10, decimal_places) 78 | Float.round(value * factor) / factor 79 | end 80 | 81 | defp valid_decimal_places?(value, max_decimal_places) do 82 | decimal_places = 83 | value 84 | |> Float.to_string() 85 | |> String.split(".") 86 | |> case do 87 | [_whole] -> 0 88 | [_whole, fraction] -> String.length(fraction) 89 | end 90 | 91 | decimal_places <= max_decimal_places 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/hyperliquid/orders/order_wire.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Orders.OrderWire do 2 | @moduledoc """ 3 | The OrderWire struct represents the essential parameters for placing an order on the 4 | Hyperliquid exchange. It encapsulates all necessary information such as asset index, 5 | buy/sell direction, price, size, and additional order properties. 6 | 7 | ## Struct Fields 8 | 9 | * `:a` - Asset index (integer) representing the asset's position in the coin list 10 | * `:b` - Boolean indicating if it's a buy order (true) or sell order (false) 11 | * `:p` - Limit price for the order 12 | * `:s` - Size of the order 13 | * `:r` - Boolean indicating if it's a reduce-only order 14 | * `:t` - Trigger conditions for the order 15 | * `:c` - Client Order ID (optional) 16 | 17 | ## Usage 18 | 19 | Create a new OrderWire struct: 20 | 21 | iex> OrderWire.new(1, true, 50000, 1.0, false, %{limit: %{tif: "Gtc"}}) 22 | %OrderWire{a: 1, b: true, p: 50000, s: 1.0, r: false, t: %{limit: %{tif: "Gtc"}}, c: nil} 23 | 24 | Purify the OrderWire for API submission: 25 | 26 | iex> order = OrderWire.new(1, true, 50000, 1.0, false, %{limit: %{tif: "Gtc"}}) 27 | iex> OrderWire.purify(order) 28 | %{a: 1, b: true, p: "50000", s: "1.0", r: false, t: %{limit: %{tif: "Gtc"}}} 29 | 30 | ## Full Contextual Example 31 | 32 | The OrderWire struct is typically used within order placement functions in the Orders module. Here's an example 33 | of how it is used for a `limit_order` function: 34 | 35 | def limit_order(coin, sz, is_buy?, px, tif \\ "gtc", reduce? \\ false, vault_address \\ nil) do 36 | trigger = trigger_from_order_type(tif) 37 | asset = Cache.asset_from_coin(coin) 38 | 39 | OrderWire.new(asset, is_buy?, px, sz, reduce?, trigger) 40 | |> OrderWire.purify() 41 | |> Exchange.place_order("na", vault_address) 42 | end 43 | 44 | In this example: 45 | 1. The function takes order details as parameters. 46 | 2. It determines the trigger type and fetches the asset index for the given coin. 47 | 3. An OrderWire struct is created using the `new/7` function. 48 | 4. The struct is then purified using the `purify/1` function. 49 | 5. Finally, the purified order data is passed to an `Exchange.place_order/3` function for execution. 50 | 51 | This demonstrates how OrderWire fits into the broader order placement process, encapsulating 52 | order details in a standardized format before submission to the exchange. 53 | 54 | Note: The `purify/1` function converts numeric values to strings and removes nil fields, 55 | preparing the order data for API submission. The asset index (`:a`) remains an integer. 56 | 57 | Important: Ensure you're using the correct asset index based on the current coin list. 58 | """ 59 | 60 | import Hyperliquid.Utils 61 | 62 | defstruct [:a, :b, :p, :s, :r, :t, c: nil] 63 | 64 | @doc """ 65 | Creates a new OrderWire struct. 66 | """ 67 | def new(asset, is_buy, limit_px, sz, reduce_only, trigger, cloid \\ nil) do 68 | %__MODULE__{ 69 | a: asset, 70 | b: is_buy, 71 | p: limit_px, 72 | s: sz, 73 | r: reduce_only, 74 | t: trigger, 75 | c: cloid 76 | } 77 | end 78 | 79 | @doc """ 80 | Converts price and size to string if they are integer or float, and removes nil values from the struct. 81 | """ 82 | def purify(%__MODULE__{} = wire) do 83 | wire 84 | |> numbers_to_strings([:p, :s]) 85 | |> Map.from_struct() 86 | |> Enum.reject(fn {_, v} -> is_nil(v) end) 87 | |> Map.new() 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/hyperliquid/api/api.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Api do 2 | @moduledoc """ 3 | A base API macro for interacting with the Hyperliquid API. 4 | 5 | This module provides a macro that sets up common functionality for API interactions, 6 | including signing requests, handling different types of actions, and processing responses. 7 | 8 | When used, it imports necessary modules, sets up aliases, and defines several helper functions 9 | for making API calls. 10 | 11 | ## Usage 12 | 13 | Use this module in other API-specific modules like this: 14 | 15 | use Hyperliquid.Api, context: "evm" 16 | 17 | ## Configuration 18 | 19 | This module relies on the following application environment variables: 20 | 21 | - `:http_url` - The base URL for API requests 22 | - `:is_mainnet` - Boolean indicating whether to use mainnet or testnet 23 | - `:private_key` - The private key used for signing requests 24 | 25 | """ 26 | defmacro __using__(opts) do 27 | quote do 28 | import Hyperliquid.{Api, Utils} 29 | alias Hyperliquid.{Config, Signer} 30 | 31 | @context unquote(Keyword.get(opts, :context, "")) 32 | @headers [{"Content-Type", "application/json"}] 33 | 34 | def api_base, do: Config.api_base() 35 | def mainnet?, do: Config.mainnet?() 36 | def endpoint, do: "#{api_base()}/#{@context}" 37 | defp secret, do: Config.secret() 38 | 39 | def post_action(action), do: post_action(action, nil, get_timestamp(), secret()) 40 | def post_action(action, vault_address), do: post_action(action, vault_address, get_timestamp(), secret()) 41 | def post_action(action, vault_address, nonce), do: post_action(action, vault_address, nonce, secret()) 42 | 43 | def post_action(%{type: "usdSend"} = action, nil, nonce, secret) do 44 | signature = Signer.sign_usd_transfer_action(action, mainnet?(), secret) 45 | payload = %{ 46 | action: action, 47 | nonce: nonce, 48 | signature: signature, 49 | vaultAddress: nil 50 | } 51 | 52 | post_signed(payload) 53 | end 54 | 55 | def post_action(%{type: "spotSend"} = action, nil, nonce, secret) do 56 | signature = Signer.sign_spot_transfer_action(action, mainnet?(), secret) 57 | payload = %{ 58 | action: action, 59 | nonce: nonce, 60 | signature: signature, 61 | vaultAddress: nil 62 | } 63 | 64 | post_signed(payload) 65 | end 66 | 67 | def post_action(%{type: "withdraw3"} = action, nil, nonce, secret) do 68 | signature = Signer.sign_withdraw_from_bridge_action(action, mainnet?(), secret) 69 | payload = %{ 70 | action: action, 71 | nonce: nonce, 72 | signature: signature, 73 | vaultAddress: nil 74 | } 75 | 76 | post_signed(payload) 77 | end 78 | 79 | def post_action(action, vault_address, nonce, secret) do 80 | signature = Signer.sign_l1_action(action, vault_address, nonce, mainnet?(), secret) 81 | payload = %{ 82 | action: action, 83 | nonce: nonce, 84 | signature: signature, 85 | vaultAddress: vault_address 86 | } 87 | 88 | post_signed(payload) 89 | end 90 | 91 | def post(payload) do 92 | HTTPoison.post(endpoint(), Jason.encode!(payload), @headers) 93 | |> handle_response() 94 | end 95 | 96 | def post_signed(payload) do 97 | HTTPoison.post(endpoint(), Jason.encode!(payload), @headers) 98 | |> handle_response() 99 | end 100 | end 101 | end 102 | 103 | def handle_response({:ok, %HTTPoison.Response{status_code: 200, body: body}}) do 104 | {:ok, Jason.decode!(body)} 105 | end 106 | 107 | def handle_response({:ok, %HTTPoison.Response{status_code: status_code, body: body}}) do 108 | {:error, %{status_code: status_code, message: body}} 109 | end 110 | 111 | def handle_response({:error, %HTTPoison.Error{reason: reason}}) do 112 | {:error, %{reason: reason}} 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/hyperliquid/api/explorer.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Api.Explorer do 2 | @moduledoc """ 3 | Module for interacting with the Hyperliquid Explorer API endpoints. 4 | 5 | This module provides functions to query various details from the Hyperliquid blockchain, 6 | including block details, transaction details, and user details. 7 | 8 | It uses the `Hyperliquid.Api` macro to set up the basic API interaction functionality. 9 | 10 | ## Usage 11 | 12 | You can use this module to make queries to the Explorer API: 13 | 14 | Hyperliquid.Api.Explorer.block_details(12345) 15 | Hyperliquid.Api.Explorer.tx_details("0x1234...") 16 | Hyperliquid.Api.Explorer.user_details("0xabcd...") 17 | 18 | ## Functions 19 | 20 | - `block_details/1` - Retrieve details for a specific block 21 | - `tx_details/1` - Retrieve details for a specific transaction 22 | - `user_details/1` - Retrieve details for a specific user address 23 | 24 | All functions return a tuple `{:ok, result}` on success, or `{:error, details}` on failure. 25 | """ 26 | 27 | use Hyperliquid.Api, context: "explorer" 28 | 29 | @doc """ 30 | Retrieves details for a specific block. 31 | 32 | ## Parameters 33 | 34 | - `block`: The block height to query 35 | 36 | ## Example 37 | 38 | iex> Hyperliquid.Api.Explorer.block_details(12345) 39 | {:ok, %{ 40 | "blockDetails" => %{ 41 | "blockTime" => 1677437426106, 42 | "hash" => "0x0b2c0480a44085b1b3206fafd19634e8ed435b02d0c1962de0616838fe13f817", 43 | "height" => 12345, 44 | "numTxs" => 1, 45 | "proposer" => "3BFD93BEAF77A51598FEA7BA084DDD2E798EC37A", 46 | "txs" => [ 47 | %{ 48 | "action" => %{"cancels" => [%{"a" => 3, "o" => 23482}], "type" => "cancel"}, 49 | "block" => 12345, 50 | "error" => nil, 51 | "hash" => "0x5e2593db2fe88b32270f02303900005e650d7594bf1154b1b3b7b30e0137426f", 52 | "time" => 1677437426106, 53 | "user" => "0xb7b6f3cea3f66bf525f5d8f965f6dbf6d9b017b2" 54 | } 55 | ] 56 | }, 57 | "type" => "blockDetails" 58 | }} 59 | """ 60 | def block_details(block) do 61 | post(%{type: "blockDetails", height: block}) 62 | end 63 | 64 | @doc """ 65 | Retrieves details for a specific transaction. 66 | 67 | ## Parameters 68 | 69 | - `hash`: The transaction hash to query 70 | 71 | ## Example 72 | 73 | iex> Hyperliquid.Api.Explorer.tx_details("0xf94afe652b34cc43d688040cb9571100001ca826f770b3adb1358e2c82d59be8") 74 | {:ok, %{ 75 | "tx" => %{ 76 | "action" => %{ 77 | "cancels" => [%{"asset" => 75, "cloid" => "0x00000000000003800076757960710291"}], 78 | "type" => "cancelByCloid" 79 | }, 80 | "block" => 213473041, 81 | "error" => nil, 82 | "hash" => "0xf94afe652b34cc43d688040cb9571100001ca826f770b3adb1358e2c82d59be8", 83 | "time" => 1719979999193, 84 | "user" => "0xaf9f722a676230cc44045efe26fe9a85801ca4fa" 85 | }, 86 | "type" => "txDetails" 87 | }} 88 | """ 89 | def tx_details(hash) do 90 | post(%{type: "txDetails", hash: hash}) 91 | end 92 | 93 | @doc """ 94 | Retrieves details for a specific user address. 95 | 96 | ## Parameters 97 | 98 | - `user_address`: The user address to query 99 | 100 | ## Example 101 | 102 | iex> Hyperliquid.Api.Explorer.user_details("0xabcd...") 103 | {:ok, %{ 104 | "txs" => [ 105 | %{ 106 | "action" => %{"cancels" => [%{"a" => 136, "o" => 26403012371}], "type" => "cancel"}, 107 | "block" => 195937903, 108 | "error" => nil, 109 | "hash" => "0x62307db3f7e254b668fe040badc66f017c00e1fe11758901a25f7a562f7c019b", 110 | "time" => 1718631244781, 111 | "user" => "0x25c32751bc8de15e282919ba3946def63c044dea" 112 | } 113 | ], 114 | "type" => "userDetails" 115 | }} 116 | """ 117 | def user_details(user_address) do 118 | post(%{type: "userDetails", user: user_address}) 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/hyperliquid/api/subscription.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Api.Subscription do 2 | @moduledoc """ 3 | Subscriptions and related helper methods. 4 | """ 5 | alias Hyperliquid.Utils 6 | 7 | @type subscription :: %{ 8 | type: String.t(), 9 | user: String.t() | nil, 10 | coin: String.t() | nil, 11 | interval: String.t() | nil 12 | } 13 | 14 | @type subscription_message :: %{ 15 | method: String.t(), 16 | subscription: subscription() 17 | } 18 | 19 | # topics 20 | def general_topics, do: ["allMids", "explorerBlock", "explorerTxs"] 21 | 22 | def coin_topics, do: ["candle", "l2Book", "trades", "activeAssetCtx"] 23 | 24 | def user_topics, 25 | do: [ 26 | "orderUpdates", 27 | "userFills", 28 | "userEvents", 29 | "userFundings", 30 | "notification", 31 | "webData2", 32 | "userNonFundingLedgerUpdates", 33 | "userHistoricalOrders", 34 | "userTwapHistory", 35 | "userTwapSliceFills", 36 | "activeAssetData" 37 | ] 38 | 39 | def topics, do: Enum.concat([general_topics(), coin_topics(), user_topics()]) 40 | 41 | # GENERAL # 42 | def all_mids, do: %{type: "allMids"} 43 | 44 | def explorer_block, do: %{type: "explorerBlock"} 45 | 46 | def explorer_txs, do: %{type: "explorerTxs"} 47 | 48 | # COIN # 49 | def candle(coin, interval), do: %{type: "candle", coin: coin, interval: interval} 50 | 51 | def l2_book(coin, sig_figs \\ 5, mantissa \\ nil), do: %{type: "l2Book", coin: coin, nSigFigs: sig_figs, mantissa: mantissa} 52 | 53 | def trades(coin), do: %{type: "trades", coin: coin} 54 | 55 | def active_asset_ctx(coin), do: %{type: "activeAssetCtx", coin: coin} 56 | 57 | # USER # 58 | def order_updates(user), do: %{type: "orderUpdates", user: user} 59 | 60 | def user_events(user), do: %{type: "userEvents", user: user} 61 | 62 | def user_fills(user), do: %{type: "userFills", user: user} 63 | 64 | def user_fundings(user), do: %{type: "userFundings", user: user} 65 | 66 | def notification(user), do: %{type: "notification", user: user} 67 | 68 | def web_data(user), do: %{type: "webData2", user: user} 69 | 70 | def user_non_funding_ledger_updates(user), do: %{type: "userNonFundingLedgerUpdates", user: user} 71 | 72 | def user_historical_orders(user), do: %{type: "userHistoricalOrders", user: user} 73 | 74 | def user_twap_history(user), do: %{type: "userTwapHistory", user: user} 75 | 76 | def user_twap_slice_fills(user), do: %{type: "userTwapSliceFills", user: user} 77 | 78 | def active_asset_data(user, coin), do: %{type: "activeAssetData", user: user, coin: coin} 79 | 80 | ###### helpers ######## 81 | 82 | def make_user_subs(user), 83 | do: [ 84 | notification(user), 85 | user_fills(user), 86 | user_non_funding_ledger_updates(user), 87 | user_twap_slice_fills(user), 88 | user_twap_history(user), 89 | user_historical_orders(user), 90 | user_fundings(user) 91 | ] 92 | 93 | def make_user_subs(user, coin), do: make_user_subs(user) ++ [active_asset_data(user, coin)] 94 | 95 | def get_subject(value) when is_map(value), do: sub_to_subject(value) 96 | def get_subject(value), do: topic_to_subject(value) 97 | 98 | defp topic_to_subject(topic) do 99 | cond do 100 | Enum.member?(user_topics(), topic) -> :user 101 | Enum.member?(coin_topics(), topic) -> :coin 102 | true -> :info 103 | end 104 | end 105 | 106 | defp sub_to_subject(sub) do 107 | cond do 108 | Map.has_key?(sub, :user) -> :user 109 | Map.has_key?(sub, :coin) -> :coin 110 | true -> :info 111 | end 112 | end 113 | 114 | def to_key(%{"type" => _} = sub), do: Utils.atomize_keys(sub) |> to_key() 115 | 116 | def to_key(%{type: type} = sub) do 117 | cond do 118 | type == "activeAssetData" -> {sub.user, type, sub.coin} 119 | Map.has_key?(sub, :user) -> {sub.user, type} 120 | Map.has_key?(sub, :interval) -> {sub.coin, type, sub.interval} 121 | Map.has_key?(sub, :coin) -> {sub.coin, type} 122 | true -> type 123 | end 124 | end 125 | 126 | def to_message(sub, sub? \\ true) 127 | def to_message(sub, true), do: %{method: "subscribe", subscription: sub} 128 | def to_message(sub, false), do: %{method: "unsubscribe", subscription: sub} 129 | 130 | def to_encoded_message(sub, sub? \\ true), do: to_message(sub, sub?) |> Jason.encode!() 131 | end 132 | -------------------------------------------------------------------------------- /lib/hyperliquid/api/info.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Api.Info do 2 | @moduledoc """ 3 | Info endpoints. 4 | """ 5 | use Hyperliquid.Api, context: "info" 6 | 7 | def meta do 8 | post(%{type: "meta"}) 9 | end 10 | 11 | def meta_and_asset_ctxs do 12 | post(%{type: "metaAndAssetCtxs"}) 13 | end 14 | 15 | def clearinghouse_state(user_address) do 16 | post(%{type: "clearinghouseState", user: user_address}) 17 | end 18 | 19 | def spot_meta do 20 | post(%{type: "spotMeta"}) 21 | end 22 | 23 | def spot_meta_and_asset_ctxs do 24 | post(%{type: "spotMetaAndAssetCtxs"}) 25 | end 26 | 27 | def spot_clearinghouse_state(user_address) do 28 | post(%{type: "spotClearinghouseState", user: user_address}) 29 | end 30 | 31 | def all_mids do 32 | post(%{type: "allMids"}) 33 | end 34 | 35 | def candle_snapshot(coin, interval, start_time, end_time) do 36 | post(%{ 37 | type: "candleSnapshot", 38 | req: %{coin: coin, interval: interval, startTime: start_time, endTime: end_time} 39 | }) 40 | end 41 | 42 | def l2_book(coin, sig_figs \\ 5, mantissa \\ nil) do 43 | post(%{type: "l2Book", coin: coin, nSigFigs: sig_figs, mantissa: mantissa}) 44 | end 45 | 46 | def user_funding(user_address, start_time, end_time) do 47 | post(%{ 48 | type: "userFunding", 49 | user: user_address, 50 | startTime: start_time, 51 | endTime: end_time 52 | }) 53 | end 54 | 55 | def funding_history(coin, start_time, end_time) do 56 | post(%{type: "fundingHistory", coin: coin, startTime: start_time, endTime: end_time}) 57 | end 58 | 59 | def get_orders(user_address) do 60 | post(%{type: "openOrders", user: user_address}) 61 | end 62 | 63 | def get_orders_fe(user_address) do 64 | post(%{type: "frontendOpenOrders", user: user_address}) 65 | end 66 | 67 | def user_fees(user_address) do 68 | post(%{type: "userFees", user: user_address}) 69 | end 70 | 71 | def order_by_id(user_address, id) do 72 | # id = oid | cloid 73 | post(%{type: "orderStatus", user: user_address, oid: id}) 74 | end 75 | 76 | def user_twap_slice_fills(user_address) do 77 | post(%{type: "userTwapSliceFills", user: user_address}) 78 | end 79 | 80 | def user_web_data(user_address) do 81 | post(%{type: "webData2", user: user_address}) 82 | end 83 | 84 | def user_non_funding_ledger_updates(user_address) do 85 | post(%{type: "userNonFundingLedgerUpdates", user: user_address}) 86 | end 87 | 88 | def user_fills(user_address) do 89 | post(%{type: "userFills", user: user_address}) 90 | end 91 | 92 | @doc """ 93 | Returns at most 2000 fills per response and only the 10000 most recent fills are available 94 | """ 95 | def user_fills_by_time(user_address, startTime) do 96 | post(%{type: "userFillsByTime", user: user_address, startTime: startTime}) 97 | end 98 | 99 | def user_fills_by_time(user_address, startTime, endTime) do 100 | post(%{type: "userFillsByTime", user: user_address, startTime: startTime, endTime: endTime}) 101 | end 102 | 103 | def user_vault_equities(user_address) do 104 | post(%{type: "userVaultEquities", user: user_address}) 105 | end 106 | 107 | def user_rate_limit(user_address) do 108 | post(%{type: "userRateLimit", user: user_address}) 109 | end 110 | 111 | def leaderboard do 112 | post(%{type: "leaderboard"}) 113 | end 114 | 115 | def vaults(user_address) do 116 | post(%{type: "vaults", user: user_address}) 117 | end 118 | 119 | def vault_details(vault_address) do 120 | post(%{type: "vaultDetails", vaultAddress: vault_address}) 121 | end 122 | 123 | def referral_state(user_address) do 124 | post(%{type: "referral", user: user_address}) 125 | end 126 | 127 | def sub_accounts(user_address) do 128 | post(%{type: "subAccounts", user: user_address}) 129 | end 130 | 131 | def agents(user_address) do 132 | post(%{type: "extraAgents", user: user_address}) 133 | end 134 | 135 | def predicted_fundings do 136 | post(%{type: "predictedFundings"}) 137 | end 138 | 139 | def portfolio(user_address) do 140 | post(%{type: "portfolio", user: user_address}) 141 | end 142 | 143 | def is_vip(user_address) do 144 | post(%{type: "isVip", user: user_address}) 145 | end 146 | 147 | def tvl_breakdown do 148 | post(%{type: "tvlBreakdown"}) 149 | end 150 | 151 | # only for testnet 152 | def eth_faucet(user_address) do 153 | post(%{type: "ethFaucet", user: user_address}) 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/hyperliquid/manager.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Manager do 2 | @moduledoc """ 3 | Application manager responsible for handling WebSocket clients and subscriptions. 4 | 5 | This module provides functionality to manage WebSocket connections, user subscriptions, 6 | and stream workers in the Hyperliquid application. It acts as a central point for 7 | managing the state of active connections and subscriptions. 8 | 9 | ## Key Features 10 | 11 | - Initializes the application cache and starts initial streams 12 | - Manages user and non-user subscriptions 13 | - Provides utilities to start and stop stream workers 14 | - Handles automatic user subscription initialization 15 | - Offers functions to query the current state of subscriptions and workers 16 | 17 | ## Usage 18 | 19 | This module is typically used to manage WebSocket connections and subscriptions, 20 | as well as to query the current state of workers. 21 | 22 | Example: 23 | 24 | # Get all active subscriptions 25 | Hyperliquid.Manager.get_all_active_subs() 26 | 27 | # Start a new stream for a specific subscription 28 | Hyperliquid.Manager.maybe_start_stream(%{type: "allMids"}) 29 | 30 | # Automatically start subscriptions for a user 31 | Hyperliquid.Manager.auto_start_user("0x1234...") 32 | """ 33 | use GenServer 34 | require Logger 35 | 36 | alias Hyperliquid.Cache 37 | alias Hyperliquid.Api.Subscription 38 | alias Hyperliquid.Streamer.{Supervisor, Stream} 39 | 40 | @workers :worker_registry 41 | @users :user_registry 42 | 43 | def start_link(_) do 44 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 45 | end 46 | 47 | def init(:ok) do 48 | Cache.init() 49 | Supervisor.start_stream([%{type: "allMids"}]) 50 | {:ok, %{}} 51 | end 52 | 53 | def get_subbed_users, do: Registry.select(@users, [{{:"$1", :_, :_}, [], [:"$1"]}]) 54 | 55 | def get_active_non_user_subs, do: 56 | @workers 57 | |> Registry.select([{{:_, :_, :"$3"}, [], [:"$3"]}]) 58 | |> Enum.flat_map(& &1.subs) 59 | |> Enum.filter(&!Map.has_key?(&1, :user)) 60 | 61 | def get_active_user_subs, do: 62 | @users 63 | |> Registry.select([{{:_, :_, :"$3"}, [], [:"$3"]}]) 64 | |> Enum.flat_map(& &1) 65 | |> Enum.filter(&Map.has_key?(&1, :user)) 66 | 67 | def get_all_active_subs, do: get_active_user_subs() ++ get_active_non_user_subs() 68 | 69 | def get_worker_pids, do: Registry.select(@workers, [{{:_, :"$2", :_}, [], [:"$2"]}]) 70 | 71 | def get_worker_ids, do: get_worker_pids() |> Enum.flat_map(&Registry.keys(@workers, &1)) 72 | 73 | def get_workers, do: 74 | Supervisor 75 | |> DynamicSupervisor.which_children() 76 | |> Enum.map(&elem(&1, 1)) 77 | 78 | def get_worker_pid_by_sub(match_sub), do: get_pid_by_sub(@workers, match_sub) 79 | 80 | def get_pid_by_sub(registry, match_sub) do 81 | results = Registry.select(registry, [ 82 | {{:"$1", :"$2", :"$3"}, [], [{{:"$2", :"$3"}}]} 83 | ]) 84 | 85 | case Enum.find(results, fn {_pid, state} -> 86 | Enum.any?(state.subs, fn sub -> sub == match_sub end) 87 | end) do 88 | {pid, _state} -> pid 89 | nil -> {:error, :not_found} 90 | end 91 | end 92 | 93 | def maybe_start_stream(sub) when is_map(sub) do 94 | subbed? = get_active_non_user_subs() |> Enum.member?(sub) 95 | 96 | if subbed? do 97 | Logger.warning("already subbed to this topic") 98 | else 99 | Supervisor.start_stream([sub]) 100 | end 101 | end 102 | 103 | def kill_worker(pid), do: Supervisor.stop_child(pid) 104 | 105 | def auto_start_user(address, coin \\ nil) do 106 | address = String.downcase(address) 107 | 108 | get_subbed_users() 109 | |> Enum.map(&String.downcase(&1)) 110 | |> Enum.member?(address) 111 | |> case do 112 | true -> Logger.warning("already subbed to user") 113 | _ -> Subscription.make_user_subs(address, coin) |> Supervisor.start_stream() 114 | end 115 | end 116 | 117 | def unsubscribe_all(pid) when is_pid(pid) do 118 | id = Registry.keys(@workers, pid) |> Enum.at(0) 119 | 120 | case id do 121 | nil -> Logger.warning("not a worker pid") 122 | _ -> Registry.values(@workers, id, pid) |> Enum.at(0) 123 | end 124 | |> Map.get(:subs) 125 | |> Enum.map(&Stream.unsubscribe(pid, &1)) 126 | end 127 | 128 | def unsubscribe_all(id) when is_binary(id) do 129 | [{pid, %{subs: subs}}] = Registry.lookup(@workers, id) 130 | 131 | Enum.map(subs, &Stream.unsubscribe(pid, &1)) 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/hyperliquid/orders/orders.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Orders do 2 | @moduledoc """ 3 | Provides helper methods for order creation and management. 4 | 5 | This module offers a set of functions to facilitate various order operations, including: 6 | - Retrieving and calculating mid-prices 7 | - Creating market and limit orders 8 | - Handling order types and triggers 9 | - Managing position closures 10 | 11 | Key features: 12 | - Mid-price retrieval with caching mechanism 13 | - Slippage price calculation for market orders 14 | - Support for various order types (GTC, IOC, ALO) 15 | - Market buy and sell order creation 16 | - Limit order creation with customizable time-in-force 17 | - Position closing functionality 18 | 19 | The module interacts with the Hyperliquid API and cache to ensure efficient 20 | and accurate order processing. It handles both perpetual and spot markets, 21 | and provides flexibility in order parameters such as size, price, and slippage. 22 | 23 | Usage examples: 24 | 25 | # Retrieve mid-price for a coin 26 | mid_price = Hyperliquid.Orders.get_midprice("SOL") 27 | 135.545 28 | 29 | # Place a market buy order 30 | Hyperliquid.Orders.market_buy("ETH", 1.0, "0x123...") 31 | {:ok, 32 | %{ 33 | "response" => %{ 34 | "data" => %{ 35 | "statuses" => [ 36 | %{"filled" => %{"avgPx" => "128.03", "oid" => 17114311614, "totalSz" => "1.0"}} 37 | ] 38 | }, 39 | "type" => "order" 40 | }, 41 | "status" => "ok" 42 | }} 43 | 44 | # Place a limit sell order 45 | Hyperliquid.Orders.limit_order("BTC", 0.5, false, 50000, "gtc", false, "0x123...") 46 | {:ok, 47 | %{ 48 | "response" => %{ 49 | "data" => %{"statuses" => [%{"resting" => %{"oid" => 10030901240}}]}, 50 | "type" => "order" 51 | }, 52 | "status" => "ok" 53 | }} 54 | 55 | # Close list of positions 56 | {:ok, %{"assetPositions" => positions}} = Info.clearinghouse_state("0x123") 57 | Hyperliquid.Orders.market_close(positions) 58 | 59 | # Close single position 60 | positions 61 | |> Enum.at(0) 62 | |> Hyperliquid.Orders.market_close() 63 | 64 | # Close all positions for an address 65 | Hyperliquid.Orders.market_close("0x123...") 66 | [ 67 | ok: %{ 68 | "response" => %{ 69 | "data" => %{ 70 | "statuses" => [ 71 | %{"filled" => %{"avgPx" => "148.07", "oid" => 10934427319, "totalSz" => "1.0"}} 72 | ] 73 | }, 74 | "type" => "order" 75 | }, 76 | "status" => "ok" 77 | } 78 | ] 79 | """ 80 | 81 | alias Hyperliquid.Api.{Info, Exchange} 82 | alias Hyperliquid.Orders.{OrderWire, PriceConverter} 83 | alias Hyperliquid.Cache 84 | 85 | @default_slippage 0.05 86 | 87 | def get_midprice(coin) do 88 | mids = Cache.all_mids() || fetch_mids() 89 | coin = String.to_existing_atom(coin) 90 | 91 | case Map.get(mids, coin) do 92 | nil -> raise "Midprice for #{coin} not found" 93 | price -> String.to_float(price) 94 | end 95 | end 96 | 97 | defp fetch_mids do 98 | case Info.all_mids() do 99 | {:ok, mids} -> mids 100 | _ -> raise "Unable to fetch mids from api" 101 | end 102 | end 103 | 104 | def slippage_price(coin, buy?, slippage \\ @default_slippage, px \\ nil) do 105 | px = px || get_midprice(coin) 106 | px = if buy?, do: px * (1 + slippage), else: px * (1 - slippage) 107 | 108 | case PriceConverter.convert_price(px, :perp) do 109 | {:ok, px} -> px 110 | _ -> px 111 | end 112 | end 113 | 114 | def trigger_from_order_type(type) when is_binary(type) do 115 | type 116 | |> String.downcase() 117 | |> case do 118 | "gtc" -> %{limit: %{tif: "Gtc"}} 119 | "ioc" -> %{limit: %{tif: "Ioc"}} 120 | "alo" -> %{limit: %{tif: "Alo"}} 121 | _ -> %{limit: %{tif: type}} 122 | end 123 | end 124 | 125 | def trigger_from_order_type(type) when is_map(type), do: %{trigger: type} 126 | 127 | def market_buy(coin, sz, vault_address \\ nil), do: 128 | market_order(coin, sz, true, false, vault_address, nil, @default_slippage) 129 | 130 | def market_sell(coin, sz, vault_address \\ nil), do: 131 | market_order(coin, sz, false, false, vault_address, nil, @default_slippage) 132 | 133 | def market_order(coin, sz, buy?, reduce?, vault_address \\ nil, px \\ nil, slippage \\ @default_slippage) do 134 | px = slippage_price(coin, buy?, slippage, px) 135 | trigger = trigger_from_order_type("ioc") 136 | asset = Cache.asset_from_coin(coin) 137 | 138 | OrderWire.new(asset, buy?, px, sz, reduce?, trigger) 139 | |> OrderWire.purify() 140 | |> Exchange.place_order("na", vault_address) 141 | end 142 | 143 | def limit_order(coin, sz, buy?, px, tif \\ "gtc", reduce? \\ false, vault_address \\ nil) do 144 | trigger = trigger_from_order_type(tif) 145 | asset = Cache.asset_from_coin(coin) 146 | 147 | OrderWire.new(asset, buy?, px, sz, reduce?, trigger) 148 | |> OrderWire.purify() 149 | |> Exchange.place_order("na", vault_address) 150 | end 151 | 152 | def market_close(position, slippage \\ @default_slippage, vault_address \\ nil) 153 | def market_close(address, slippage, vault_address) when is_binary(address) do 154 | {:ok, %{"assetPositions" => positions}} = Info.clearinghouse_state(address) 155 | 156 | market_close(positions, slippage, vault_address) 157 | end 158 | 159 | def market_close(positions, slippage, vault_address) when is_list(positions) do 160 | Enum.map(positions, &market_close(&1, slippage, vault_address)) 161 | end 162 | 163 | def market_close(%{"position" => p}, slippage, vault_address) do 164 | szi = String.to_float(p["szi"]) 165 | sz = abs(szi) 166 | buy? = if szi < 0, do: true, else: false 167 | market_order(p["coin"], sz, buy?, true, vault_address, nil, slippage) 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/hyperliquid/cache/cache.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Cache do 2 | @moduledoc """ 3 | Application cache for storing asset lists and exchange meta information. 4 | 5 | This module provides functions to initialize and manage a cache for Hyperliquid-related data, 6 | including asset information, exchange metadata, and utility functions for retrieving and 7 | manipulating cached data. 8 | 9 | The cache is implemented using Cachex and stores various pieces of information such as: 10 | - Exchange metadata 11 | - Spot market metadata 12 | - Asset mappings 13 | - Decimal precision information 14 | - Token information 15 | 16 | It also provides utility functions for working with assets, tokens, and other cached data. 17 | """ 18 | alias __MODULE__ 19 | alias Hyperliquid.{Api.Info, Utils} 20 | 21 | @cache :hyperliquid 22 | 23 | @doc """ 24 | Initializes the cache with api information. 25 | """ 26 | def init do 27 | {:ok, [meta, ctxs]} = Info.meta_and_asset_ctxs() 28 | {:ok, [spot_meta, spot_ctxs]} = Info.spot_meta_and_asset_ctxs() 29 | {:ok, mids} = Info.all_mids() 30 | 31 | all_mids = Utils.atomize_keys(mids) 32 | perps = Map.get(meta, "universe") 33 | spot_pairs = Map.get(spot_meta, "universe") 34 | tokens = Map.get(spot_meta, "tokens") 35 | 36 | asset_map = Map.merge( 37 | create_asset_map(meta), 38 | create_asset_map(spot_meta, 10_000) 39 | ) 40 | 41 | decimal_map = Map.merge( 42 | create_decimal_map(meta), 43 | create_decimal_map(spot_meta, 8) 44 | ) 45 | 46 | Cachex.put!(@cache, :meta, meta) 47 | Cachex.put!(@cache, :spot_meta, spot_meta) 48 | Cachex.put!(@cache, :all_mids, all_mids) 49 | Cachex.put!(@cache, :asset_map, asset_map) 50 | Cachex.put!(@cache, :decimal_map, decimal_map) 51 | Cachex.put!(@cache, :perps, perps) 52 | Cachex.put!(@cache, :spot_pairs, spot_pairs) 53 | Cachex.put!(@cache, :tokens, tokens) 54 | Cachex.put!(@cache, :ctxs, ctxs) 55 | Cachex.put!(@cache, :spot_ctxs, spot_ctxs) 56 | end 57 | 58 | def meta, do: Cache.get(:meta) 59 | def spot_meta, do: Cache.get(:spot_meta) 60 | def all_mids, do: Cache.get(:all_mids) 61 | def asset_map, do: Cache.get(:asset_map) 62 | def decimal_map, do: Cache.get(:decimal_map) 63 | def perps, do: Cache.get(:perps) 64 | def spot_pairs, do: Cache.get(:spot_pairs) 65 | def tokens, do: Cache.get(:tokens) 66 | def ctxs, do: Cache.get(:ctxs) 67 | def spot_ctxs, do: Cache.get(:spot_ctxs) 68 | 69 | ###### Setters ###### 70 | defp create_asset_map(data, buffer \\ 0) do 71 | data 72 | |> Map.get("universe") 73 | |> Enum.with_index(&{&1["name"], &2 + buffer}) 74 | |> Enum.into(%{}) 75 | end 76 | 77 | defp create_decimal_map(data) do 78 | data 79 | |> Map.get("universe") 80 | |> Enum.map(&{&1["name"], &1["szDecimals"]}) 81 | |> Enum.into(%{}) 82 | end 83 | 84 | defp create_decimal_map(data, decimals) do 85 | data 86 | |> Map.get("universe") 87 | |> Enum.map(&{&1["name"], decimals}) 88 | |> Enum.into(%{}) 89 | end 90 | 91 | ###### Helpers ###### 92 | 93 | @doc """ 94 | Retrieves the asset index for a given coin symbol. 95 | 96 | ## Parameters 97 | 98 | - `coin`: The coin symbol (e.g., "BTC", "ETH") 99 | 100 | ## Returns 101 | 102 | The asset index corresponding to the given coin symbol, or nil if not found. 103 | 104 | ## Example 105 | 106 | iex> Hyperliquid.Cache.asset_from_coin("SOL") 107 | 5 108 | """ 109 | def asset_from_coin(coin), do: Cache.get(:asset_map)[coin] 110 | def decimals_from_coin(coin), do: Cache.get(:decimal_map)[coin] 111 | 112 | def get_token_by_index(index), do: 113 | Cache.get(:tokens) 114 | |> Enum.find(& &1["index"] == index) 115 | 116 | def get_token_by_name(name), do: 117 | Cache.get(:tokens) 118 | |> Enum.find(& &1["name"] == name) 119 | 120 | def get_token_by_address(address), do: 121 | Cache.get(:tokens) 122 | |> Enum.find(& &1["tokenId"] == address) 123 | 124 | def get_token_name_by_index(index), do: 125 | get_token_by_index(index) 126 | |> Map.get("name") 127 | 128 | def get_token_key(token) when is_map(token), do: "#{Map.get(token, "name")}:#{Map.get(token, "tokenId")}" 129 | def get_token_key(name), do: 130 | name 131 | |> get_token_by_name() 132 | |> get_token_key() 133 | 134 | def increment, do: Cache.incr(:post_count) 135 | 136 | ###### Wrappers ###### 137 | 138 | @doc """ 139 | Retrieves a value from the cache by key. 140 | """ 141 | def get(key) do 142 | case Cachex.get(@cache, key) do 143 | {:ok, value} -> value 144 | {:error, _reason} -> nil 145 | end 146 | end 147 | 148 | @doc """ 149 | Puts a key-value pair into the cache. 150 | """ 151 | def put(key, value) do 152 | Cachex.put!(@cache, key, value) 153 | end 154 | 155 | @doc """ 156 | Gets a value from the cache and updates it using the provided function. 157 | """ 158 | def get_and_update(key, func) do 159 | Cachex.get_and_update!(@cache, key, func) 160 | end 161 | 162 | def execute(func) do 163 | Cachex.execute!(@cache, func) 164 | end 165 | 166 | @doc """ 167 | Executes a transaction for a set of keys. 168 | """ 169 | def transaction(keys, func) do 170 | Cachex.transaction!(@cache, keys, func) 171 | end 172 | 173 | @doc """ 174 | Removes a key from the cache. 175 | """ 176 | def del(key) do 177 | Cachex.del!(@cache, key) 178 | end 179 | 180 | @doc """ 181 | Checks if a key exists in the cache. 182 | """ 183 | def exists?(key) do 184 | Cachex.exists?(@cache, key) 185 | end 186 | 187 | @doc """ 188 | Increments a key's value in the cache by a given amount. 189 | """ 190 | def incr(key, amount \\ 1) do 191 | Cachex.incr!(@cache, key, amount) 192 | end 193 | 194 | @doc """ 195 | Decrements a key's value in the cache by a given amount. 196 | """ 197 | def decr(key, amount \\ 1) do 198 | Cachex.decr!(@cache, key, amount) 199 | end 200 | 201 | @doc """ 202 | Clears all entries in the cache. 203 | """ 204 | def clear do 205 | Cachex.clear!(@cache) 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/hyperliquid/utils/signer.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Signer do 2 | @moduledoc """ 3 | Hyperliquid.Signer provides methods for signing and constructing payloads for various 4 | Hyperliquid operations. 5 | 6 | This module includes functions for: 7 | - Generating action hashes 8 | - Constructing phantom agents 9 | - Signing L1 actions 10 | - Signing user-signed actions (e.g., spot transfers, USD transfers, withdrawals) 11 | - Signing agent approvals 12 | 13 | It uses EIP-712 for structured data signing and provides utilities for handling 14 | signatures and preparing data for signing. 15 | """ 16 | require Logger 17 | alias Hyperliquid.Encoder 18 | import Hyperliquid.Utils 19 | 20 | @zero_address "0x0000000000000000000000000000000000000000" 21 | 22 | def action_hash(action, nonce, vault_address) do 23 | Encoder.pack_action(action, nonce, vault_address) 24 | |> IO.iodata_to_binary() 25 | |> ExKeccak.hash_256() 26 | |> Base.encode16(case: :lower) 27 | |> then(&("0x" <> &1)) 28 | end 29 | 30 | # Constructs a phantom agent 31 | def construct_phantom_agent(hash, mainnet?) do 32 | source = if(mainnet?, do: "a", else: "b") 33 | %{"source" => source, "connectionId" => hash} 34 | end 35 | 36 | # Signs an L1 action 37 | def sign_l1_action(action, vault_address, nonce, mainnet?, secret) do 38 | hash = action_hash(action, nonce, vault_address) 39 | phantom_agent = construct_phantom_agent(hash, mainnet?) 40 | data = prepare_data(phantom_agent, 1337) 41 | 42 | case EIP712.sign(data, trim_0x(secret)) do 43 | {:ok, hex_signature} -> split_sig(hex_signature) 44 | _resp -> Logger.warning("Unexpected signature") 45 | end 46 | end 47 | 48 | def prepare_data(message, chain_id) do 49 | Jason.encode! %{ 50 | domain: %{ 51 | chainId: to_full_hex(chain_id), 52 | name: "Exchange", 53 | verifyingContract: @zero_address, 54 | version: "1" 55 | }, 56 | types: %{ 57 | Agent: [ 58 | %{name: "source", type: "string"}, 59 | %{name: "connectionId", type: "bytes32"} 60 | ], 61 | EIP712Domain: [ 62 | %{name: "name", type: "string"}, 63 | %{name: "version", type: "string"}, 64 | %{name: "chainId", type: "uint256"}, 65 | %{name: "verifyingContract", type: "address"} 66 | ] 67 | }, 68 | primaryType: "Agent", 69 | message: message 70 | } 71 | end 72 | 73 | def sign_user_signed_action(action, payload_types, primary_type, mainnet?, secret) do 74 | chain_id = if(mainnet?, do: to_hex(42_161), else: to_hex(421_614)) 75 | action = 76 | Map.merge(action, %{ 77 | hyperliquidChain: if(mainnet?, do: "Mainnet", else: "Testnet"), 78 | signatureChainId: chain_id, 79 | time: to_hex(action.time) 80 | }) 81 | 82 | data = Jason.encode! %{ 83 | domain: %{ 84 | name: "HyperliquidSignTransaction", 85 | version: "1", 86 | chainId: chain_id, 87 | verifyingContract: @zero_address 88 | }, 89 | types: %{ 90 | "#{primary_type}": payload_types, 91 | EIP712Domain: [ 92 | %{name: "name", type: "string"}, 93 | %{name: "version", type: "string"}, 94 | %{name: "chainId", type: "uint256"}, 95 | %{name: "verifyingContract", type: "address"} 96 | ] 97 | }, 98 | primaryType: primary_type, 99 | message: action 100 | } 101 | 102 | case EIP712.sign(data, trim_0x(secret)) do 103 | {:ok, hex_signature} -> split_sig(hex_signature) 104 | _resp -> Logger.warning("Unexpected signature") 105 | end 106 | end 107 | 108 | def sign_spot_transfer_action(action, mainnet?, secret) do 109 | sign_user_signed_action( 110 | action, 111 | [ 112 | %{name: "hyperliquidChain", type: "string"}, 113 | %{name: "destination", type: "string"}, 114 | %{name: "token", type: "string"}, 115 | %{name: "amount", type: "string"}, 116 | %{name: "time", type: "uint64"}, 117 | ], 118 | "HyperliquidTransaction:SpotSend", 119 | mainnet?, 120 | secret 121 | ) 122 | end 123 | 124 | def sign_usd_transfer_action(action, mainnet?, secret) do 125 | sign_user_signed_action( 126 | action, 127 | [ 128 | %{name: "hyperliquidChain", type: "string"}, 129 | %{name: "destination", type: "string"}, 130 | %{name: "amount", type: "string"}, 131 | %{name: "time", type: "uint64"}, 132 | ], 133 | "HyperliquidTransaction:UsdSend", 134 | mainnet?, 135 | secret 136 | ) 137 | end 138 | 139 | def sign_withdraw_from_bridge_action(action, mainnet?, secret) do 140 | sign_user_signed_action( 141 | action, 142 | [ 143 | %{name: "hyperliquidChain", type: "string"}, 144 | %{name: "destination", type: "string"}, 145 | %{name: "amount", type: "string"}, 146 | %{name: "time", type: "uint64"}, 147 | ], 148 | "HyperliquidTransaction:Withdraw", 149 | mainnet?, 150 | secret 151 | ) 152 | end 153 | 154 | def sign_agent(action, mainnet?, secret) do 155 | # testnet signaturechainid = to_hex(43114) 156 | sign_user_signed_action( 157 | action, 158 | [ 159 | %{name: "hyperliquidChain", type: "string"}, 160 | %{name: "agentAddress", type: "address"}, 161 | %{name: "agentName", type: "string"}, 162 | %{name: "nonce", type: "uint64"} 163 | ], 164 | "HyperliquidTransaction:ApproveAgent", 165 | mainnet?, 166 | secret 167 | ) 168 | end 169 | 170 | @doc """ 171 | Splits a hexadecimal signature into its components. 172 | 173 | ## Parameters 174 | 175 | - `hex_signature`: The hexadecimal signature to split 176 | 177 | ## Returns 178 | 179 | A map containing the signature components (r, s, v). 180 | 181 | ## Example 182 | 183 | iex> Hyperliquid.Signer.split_sig("0x1234...") 184 | %{r: "0x...", s: "0x...", v: 27} 185 | 186 | ## Raises 187 | 188 | Raises an `ArgumentError` if the signature length is invalid or if the v value is unexpected. 189 | """ 190 | def split_sig(hex_signature) do 191 | hex_signature = trim_0x(hex_signature) 192 | 193 | if String.length(hex_signature) != 130 do 194 | raise ArgumentError, "bad sig length: #{String.length(hex_signature)}" 195 | end 196 | 197 | sig_v = String.slice(hex_signature, -2, 2) 198 | 199 | unless sig_v in ["1c", "1b", "00", "01"] do 200 | raise ArgumentError, "bad sig v #{sig_v}" 201 | end 202 | 203 | v_value = 204 | case sig_v do 205 | "1b" -> 27 206 | "00" -> 27 207 | "1c" -> 28 208 | "01" -> 28 209 | _ -> raise("Unexpected sig_v value") 210 | end 211 | 212 | r = "0x" <> String.slice(hex_signature, 0, 64) 213 | s = "0x" <> String.slice(hex_signature, 64, 64) 214 | 215 | %{r: r, s: s, v: v_value} 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/hyperliquid/streamer/stream.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Streamer.Stream do 2 | @moduledoc """ 3 | WebSocket client for streaming data from the Hyperliquid API. 4 | 5 | This module implements a WebSocket client that connects to the Hyperliquid API, 6 | manages subscriptions, and processes incoming data. It handles connection 7 | management, subscription requests, heartbeats, and message processing. 8 | 9 | ## Key Features 10 | 11 | - Establishes and maintains WebSocket connections 12 | - Manages user and non-user subscriptions 13 | - Handles heartbeats and connection timeouts 14 | - Processes and broadcasts incoming messages 15 | - Supports dynamic subscription management 16 | 17 | ## Usage 18 | 19 | This module is used to subscribe to ws events and can also support post requests. 20 | Typically this module shouldn't be called directly, as it is meant to be managed by the 21 | Manager, errors may occur when used outside that context. 22 | 23 | To subscribe to the broadcasts of ws events, you can refer to the subscription `type` for the 24 | channel value to subscribe to in your own application. 25 | 26 | Example: 27 | 28 | # Start a new stream with initial subscriptions 29 | {:ok, pid} = Hyperliquid.Streamer.Stream.start_link([%{type: "allMids"}]) 30 | 31 | # Add a new subscription 32 | Hyperliquid.Streamer.Stream.subscribe(pid, %{type: "trades", coin: "BTC"}) 33 | 34 | # Remove a subscription 35 | Hyperliquid.Streamer.Stream.unsubscribe(pid, %{type: "trades", coin: "BTC"}) 36 | """ 37 | use WebSockex 38 | require Logger 39 | 40 | import Hyperliquid.Utils 41 | alias Hyperliquid.{Api.Subscription, Cache, Config, PubSub} 42 | 43 | @heartbeat_interval 50_000 44 | @timeout_seconds 60 45 | 46 | @workers :worker_registry 47 | @users :user_registry 48 | 49 | @ping Jason.encode!(%{"method" => "ping"}) 50 | 51 | def start_link(subs \\ []) do 52 | state = %{ 53 | id: make_cloid(), 54 | user: nil, 55 | subs: subs, 56 | req_count: 0, 57 | active_subs: 0, 58 | last_response: System.system_time(:second) 59 | } 60 | 61 | WebSockex.start_link( 62 | Config.ws_url(), 63 | __MODULE__, 64 | state, 65 | name: via(state) 66 | ) 67 | end 68 | 69 | def post(pid, type, payload) do 70 | WebSockex.send_frame(pid, {:text, 71 | Jason.encode!(%{ 72 | method: "post", 73 | id: Cache.increment(), 74 | request: %{ 75 | type: type, 76 | payload: payload 77 | } 78 | }) 79 | }) 80 | end 81 | 82 | def subscribe(pid, sub) do 83 | WebSockex.cast(pid, {:add_sub, sub}) 84 | end 85 | 86 | def unsubscribe(pid, sub) do 87 | WebSockex.cast(pid, {:remove_sub, sub}) 88 | end 89 | 90 | @impl true 91 | def handle_connect(_conn, state) do 92 | :timer.send_interval(@heartbeat_interval, self(), :send_ping) 93 | 94 | Enum.each(state.subs, &WebSockex.cast(self(), {:add_sub, &1})) 95 | 96 | {:ok, %{state | subs: []}} 97 | end 98 | 99 | @impl true 100 | def handle_info(:send_ping, state) do 101 | age = System.system_time(:second) - state.last_response 102 | 103 | if age > @timeout_seconds do 104 | Logger.warning("No response for over #{@timeout_seconds} seconds. Restarting Websocket process.") 105 | {:close, state} 106 | else 107 | {:reply, {:text, @ping}, state} 108 | end 109 | end 110 | 111 | @impl true 112 | def handle_cast({:add_sub, sub}, %{user: user} = state) do 113 | message = Subscription.to_encoded_message(sub) 114 | subject = Subscription.get_subject(sub) 115 | 116 | cond do 117 | subject == :user && is_nil(user) -> 118 | new_user = Map.get(sub, :user) |> String.downcase() 119 | Registry.register(@users, new_user, []) 120 | {:reply, {:text, message}, %{state | user: new_user}} 121 | 122 | subject == :user && user != String.downcase(sub.user) -> 123 | {:ok, state} 124 | 125 | true -> 126 | {:reply, {:text, message}, state} 127 | end 128 | end 129 | 130 | @impl true 131 | def handle_cast({:remove_sub, sub}, state) do 132 | Subscription.to_encoded_message(sub, false) 133 | |> then(&{:reply, {:text, &1}, state}) 134 | end 135 | 136 | @impl true 137 | def handle_disconnect(reason, state) do 138 | IO.puts("Disconnected: #{inspect(reason)}") 139 | {:ok, state} 140 | end 141 | 142 | @impl true 143 | def handle_frame({:text, msg}, %{req_count: req_count, id: id} = state) do 144 | msg = Jason.decode!(msg, keys: :atoms) 145 | event = process_event(msg) 146 | 147 | new_state = 148 | case event.channel do 149 | "subscriptionResponse" -> 150 | update_active_subs(event, state) 151 | 152 | "allMids" -> 153 | Cache.put(:all_mids, event.data.mids) 154 | state 155 | 156 | _ -> 157 | state 158 | end 159 | |> Map.merge(%{ 160 | last_response: System.system_time(:second), 161 | req_count: req_count + 1 162 | }) 163 | 164 | broadcast("ws_event", Map.merge(event, %{ 165 | pid: self(), 166 | wid: id, 167 | subs: state.subs 168 | })) 169 | 170 | Registry.update_value(@workers, id, fn _ -> new_state end) 171 | 172 | {:ok, new_state} 173 | end 174 | 175 | defp update_active_subs(%{data: %{method: method, subscription: sub}}, %{subs: subs} = state) do 176 | new_subs = 177 | case method do 178 | "subscribe" -> [sub | subs] 179 | "unsubscribe" -> Enum.reject(subs, &(&1 == sub)) 180 | _ -> subs 181 | end 182 | 183 | user = 184 | new_subs 185 | |> Enum.any?(&Map.has_key?(&1, :user)) 186 | |> case do 187 | true -> 188 | Registry.update_value(@users, state.user, fn _ -> new_subs end) 189 | state.user 190 | false -> 191 | Registry.unregister(@users, state.user) 192 | nil 193 | end 194 | 195 | new_state = %{state | 196 | user: user, 197 | subs: new_subs, 198 | active_subs: Enum.count(new_subs) 199 | } 200 | 201 | Registry.update_value(@workers, state.id, fn _ -> new_state end) 202 | 203 | new_state 204 | end 205 | 206 | defp update_active_subs(event, state) do 207 | Logger.warning("Update active subs catchall: #{inspect(event)}") 208 | state 209 | end 210 | 211 | def process_event(%{channel: ch, data: %{subscription: sub, method: method} = data}) do 212 | %{ 213 | channel: ch, 214 | subject: Subscription.get_subject(sub), 215 | data: data, 216 | method: method, 217 | sub: sub, 218 | key: Subscription.to_key(sub) 219 | } 220 | end 221 | 222 | def process_event(%{channel: "post", data: %{id: id, response: response}}) do 223 | %{ 224 | id: id, 225 | channel: "post", 226 | subject: Subscription.get_subject(response.payload.type), 227 | data: response 228 | } 229 | end 230 | 231 | def process_event(%{channel: ch, data: data}) do 232 | %{ 233 | channel: ch, 234 | subject: Subscription.get_subject(ch), 235 | data: data 236 | } 237 | end 238 | 239 | def process_event([%{action: _} | _] = msg) do 240 | %{ 241 | channel: "explorerTxs", 242 | subject: :txs, 243 | data: msg 244 | } 245 | end 246 | 247 | def process_event([%{height: _} | _] = msg) do 248 | %{ 249 | channel: "explorerBlock", 250 | subject: :block, 251 | data: msg 252 | } 253 | end 254 | 255 | def process_event(msg) do 256 | %{ 257 | channel: nil, 258 | subject: nil, 259 | data: msg 260 | } 261 | end 262 | 263 | defp broadcast(channel, event) do 264 | Phoenix.PubSub.broadcast(PubSub, channel, event) 265 | end 266 | 267 | @impl true 268 | def terminate(close_reason, _state) do 269 | Logger.warning("Websocket terminated: #{inspect(close_reason)}") 270 | end 271 | 272 | defp via(state) do 273 | {:via, Registry, {@workers, state.id, state}} 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperliquid 2 | 3 | Hyperliquid is an Elixir-based library for interacting with the Hyperliquid decentralized exchange platform. It provides a set of modules and functions to manage WebSocket connections, handle orders, and interact with the Hyperliquid API. 4 | 5 | ## Features 6 | 7 | - Order management (market, limit, and close orders) 8 | - Account operations (transfers, leverage adjustment, etc.) 9 | - Price conversion and formatting 10 | - Caching for efficient data access and improved performance 11 | - Dynamic subscription management 12 | - WebSocket streaming for real-time data 13 | 14 | ## Installation 15 | 16 | Add `hyperliquid` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:hyperliquid, "~> 0.1.6"} 22 | ] 23 | end 24 | ``` 25 | 26 | ## Livebook 27 | 28 | To use in livebook, add the following to the notebook dependencies and setup section: 29 | 30 | ```elixir 31 | Mix.install([ 32 | {:hyperliquid, "~> 0.1.6"} 33 | ], 34 | config: [ 35 | hyperliquid: [private_key: "YOUR_KEY_HERE"] 36 | ] 37 | ) 38 | 39 | # You can override the default ws and http urls to use testnet 40 | Mix.install([ 41 | {:hyperliquid, "~> 0.1.6"} 42 | ], 43 | config: [ 44 | hyperliquid: [ 45 | ws_url: "wss://api.hyperliquid-testnet.xyz/ws", 46 | http_url: "https://api.hyperliquid-testnet.xyz", 47 | private_key: "YOUR_KEY_HERE" 48 | ] 49 | ] 50 | ) 51 | ``` 52 | 53 | ## Configuration 54 | 55 | In `config/config.exs`, add Hyperliquid protocol host params to your config file 56 | 57 | ```elixir 58 | config :hyperliquid, 59 | private_key: "YOUR_KEY_HERE" 60 | ``` 61 | 62 | ## Usage 63 | 64 | ### Placing Orders 65 | ```elixir 66 | # Place a market sell order 67 | Hyperliquid.Orders.market_sell("ETH", 1) 68 | 69 | # Place a market buy order for a sub account (vault address) 70 | Hyperliquid.Orders.market_buy("ETH", 1, "0x123...") 71 | {:ok, 72 | %{ 73 | "response" => %{ 74 | "data" => %{ 75 | "statuses" => [ 76 | %{"filled" => %{"avgPx" => "128.03", "oid" => 17114311614, "totalSz" => "1.0"}} 77 | ] 78 | }, 79 | "type" => "order" 80 | }, 81 | "status" => "ok" 82 | }} 83 | 84 | # Place a limit sell order 85 | Hyperliquid.Orders.limit_order("BTC", 0.5, false, 50000, "gtc", false) 86 | {:ok, 87 | %{ 88 | "response" => %{ 89 | "data" => %{"statuses" => [%{"resting" => %{"oid" => 10030901240}}]}, 90 | "type" => "order" 91 | }, 92 | "status" => "ok" 93 | }} 94 | ``` 95 | 96 | ### Closing Positions 97 | ```elixir 98 | # Close list of positions 99 | {:ok, %{"assetPositions" => positions}} = Info.clearinghouse_state("0x123") 100 | Hyperliquid.Orders.market_close(positions) 101 | 102 | # Close single position 103 | position = Enum.at(positions, 0) 104 | Hyperliquid.Orders.market_close(position) 105 | {:ok, %{ 106 | "response" => %{ 107 | "data" => %{ 108 | "statuses" => [ 109 | %{"filled" => %{"avgPx" => "148.07", "oid" => 10934427319, "totalSz" => "1.0"}} 110 | ] 111 | }, 112 | "type" => "order" 113 | }, 114 | "status" => "ok" 115 | }} 116 | 117 | # Close all positions for an address 118 | Hyperliquid.Orders.market_close("0x123...") 119 | [ 120 | ok: %{ 121 | "response" => %{ 122 | "data" => %{ 123 | "statuses" => [ 124 | %{"filled" => %{"avgPx" => "148.07", "oid" => 10934427319, "totalSz" => "1.0"}} 125 | ] 126 | }, 127 | "type" => "order" 128 | }, 129 | "status" => "ok" 130 | } 131 | ] 132 | ``` 133 | ### Streaming Data 134 | To start a WebSocket stream: 135 | 136 | ```elixir 137 | # Pass in a single sub or list of subs to start a new ws connection. 138 | {:ok, PID<0.408.0>} = Hyperliquid.Manager.maybe_start_stream(%{type: "allMids"}) 139 | {:ok, PID<0.408.0>} = Hyperliquid.Manager.maybe_start_stream([sub_list]) 140 | # The manager will check if the sub is currently already subscribed, and if not, open the connection. 141 | 142 | # To subscribe to a user address, we call auto_start_user 143 | {:ok, PID<0.408.0>} = Hyperliquid.Manager.auto_start_user(user_address) 144 | 145 | # Because we are limited to 10 unique user subscriptions, it is crucial to keep track of which users 146 | # are currently subbed to and that logic is handled internally by the manager but also available to be called externally. 147 | Hyperliquid.Manager.get_subbed_users() 148 | ["0x123..."] 149 | 150 | Hyperliquid.Manager.get_active_non_user_subs() 151 | [%{type: "allMids"}] 152 | 153 | Hyperliquid.Manager.get_active_user_subs() 154 | [ 155 | %{type: "userFundings", user: "0x123..."}, 156 | %{type: "userHistoricalOrders", user: "0x123..."}, 157 | %{type: "userTwapHistory", user: "0x123..."}, 158 | %{type: "userTwapSliceFills", user: "0x123..."}, 159 | %{type: "userNonFundingLedgerUpdates", user: "0x123..."}, 160 | %{type: "userFills", user: "0x123...", aggregateByTime: false}, 161 | %{type: "notification", user: "0x123..."} 162 | ] 163 | 164 | Manager.get_workers() 165 | [PID<0.633.0>, PID<0.692.0>] 166 | ``` 167 | 168 | Once a Manager has started, it will automatically subscribe to allMids and update the cache. 169 | The Stream module is broadcasting each event it receives to the "ws_event" channel, 170 | to subscribe to these events in your own application, simply call subscribe like so: 171 | ```elixir 172 | Phoenix.PubSub.subscribe(Hyperliquid.PubSub, channel) 173 | # also available is the shart hand method, via the Utils module. 174 | Hyperliquid.Utils.subscribe(channel) 175 | ``` 176 | 177 | ### Cache 178 | The application uses Cachex to handle in memory kv storage for fast and efficient lookup of values 179 | we frequently need to place valid orders, one of those key items is the current mid price of each asset. 180 | 181 | When initialized, the Manager will make several requests to get this data, as well as subscribe to the "allMids" channel. 182 | This ensures the latest mid price is always up to date and can be immediately accessable. 183 | 184 | Quick access utility functions. 185 | ```elixir 186 | def meta, do: Cache.get(:meta) 187 | def spot_meta, do: Cache.get(:spot_meta) 188 | def all_mids, do: Cache.get(:all_mids) 189 | def asset_map, do: Cache.get(:asset_map) 190 | def decimal_map, do: Cache.get(:decimal_map) 191 | def perps, do: Cache.get(:perps) 192 | def spot_pairs, do: Cache.get(:spot_pairs) 193 | def tokens, do: Cache.get(:tokens) 194 | def ctxs, do: Cache.get(:ctxs) 195 | def spot_ctxs, do: Cache.get(:spot_ctxs) 196 | ``` 197 | You may also note some commonly used util methods in the Cache which can be used like this: 198 | 199 | ```elixir 200 | Hyperliquid.Cache.asset_from_coin("SOL") 201 | 5 202 | 203 | Hyperliquid.Cache.decimals_from_coin("SOL") 204 | 2 205 | 206 | Hyperliquid.Cache.get_token_by_name("HFUN") 207 | %{ 208 | "evmContract" => nil, 209 | "fullName" => nil, 210 | "index" => 2, 211 | "isCanonical" => false, 212 | "name" => "HFUN", 213 | "szDecimals" => 2, 214 | "tokenId" => "0xbaf265ef389da684513d98d68edf4eae", 215 | "weiDecimals" => 8 216 | } 217 | 218 | Hyperliquid.Cache.get_token_by_address("0xbaf265ef389da684513d98d68edf4eae") 219 | %{ 220 | "evmContract" => nil, 221 | "fullName" => nil, 222 | "index" => 2, 223 | "isCanonical" => false, 224 | "name" => "HFUN", 225 | "szDecimals" => 2, 226 | "tokenId" => "0xbaf265ef389da684513d98d68edf4eae", 227 | "weiDecimals" => 8 228 | } 229 | 230 | Hyperliquid.Cache.get_token_key("PURR") 231 | "PURR:0xc1fb593aeffbeb02f85e0308e9956a90" 232 | 233 | Hyperliquid.Cache.get_token_by_name("PURR") |> Cache.get_token_key() 234 | "PURR:0xc1fb593aeffbeb02f85e0308e9956a90" 235 | 236 | Hyperliquid.Cache.get(:tokens) 237 | [ 238 | %{ 239 | "evmContract" => nil, 240 | "fullName" => nil, 241 | "index" => 0, 242 | "isCanonical" => true, 243 | "name" => "USDC", 244 | "szDecimals" => 8, 245 | "tokenId" => "0x6d1e7cde53ba9467b783cb7c530ce054", 246 | "weiDecimals" => 8 247 | }, 248 | ... 249 | ] 250 | ``` 251 | One great place to look for more insight on how to utilize the cache, is the Orders module. 252 | 253 | ## License 254 | This project is licensed under the MIT License. 255 | 256 | -------------------------------------------------------------------------------- /lib/hyperliquid/utils/encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Encoder do 2 | @moduledoc """ 3 | Provides encoding functionality for Hyperliquid API actions. 4 | 5 | This module contains various functions to encode structured data for different 6 | Hyperliquid API actions. It supports encoding of orders, cancellations, modifications, 7 | and various other action types. 8 | 9 | ## Key Features 10 | 11 | - Encodes different action types (order, cancel, modify, etc.) 12 | - Packs structured data for API requests 13 | - Supports various field encodings 14 | - Handles special cases like triggers and transfers 15 | - Provides utilities for binary data manipulation 16 | 17 | ## Usage 18 | 19 | Most functions in this module are intended for internal use within the Hyperliquid 20 | API client. However, you may use some of the public functions if you need to 21 | manually encode data for custom API interactions. Refer to the Signer module. 22 | 23 | Example: 24 | 25 | iex> action = %{type: "order", orders: [...], grouping: "na"} 26 | iex> nonce = 12345 27 | iex> vault_address = "0x1234..." 28 | iex> Hyperliquid.Encoder.pack_action(action, nonce, vault_address) 29 | 30 | Note: Ensure you understand the Hyperliquid API specifications when using 31 | these encoding functions directly. 32 | """ 33 | alias Hyperliquid.Utils 34 | import Msgpax 35 | 36 | @action_type pack!("type") 37 | 38 | @action_types %{ 39 | order: pack!("order"), 40 | cancel: pack!("cancel"), 41 | cancelByCloid: pack!("cancelByCloid"), 42 | modify: pack!("modify"), 43 | batchModify: pack!("batchModify"), 44 | updateLeverage: pack!("updateLeverage"), 45 | updateIsolatedMargin: pack!("updateIsolatedMargin"), 46 | spotUser: pack!("spotUser"), 47 | setReferrer: pack!("setReferrer"), 48 | vaultTransfer: pack!("vaultTransfer"), 49 | createSubAccount: pack!("createSubAccount"), 50 | subAccountTransfer: pack!("subAccountTransfer"), 51 | subAccountSpotTransfer: pack!("subAccountSpotTransfer") 52 | } 53 | 54 | @orders pack!("orders") 55 | @cancels pack!("cancels") 56 | @modifies pack!("modifies") 57 | @oid pack!("oid") 58 | @order pack!("order") 59 | @class_transfer pack!("classTransfer") 60 | 61 | @fields %{ 62 | a: pack!("a"), 63 | b: pack!("b"), 64 | p: pack!("p"), 65 | s: pack!("s"), 66 | r: pack!("r"), 67 | t: pack!("t"), 68 | o: pack!("o"), 69 | c: pack!("c"), 70 | asset: pack!("asset"), 71 | cloid: pack!("cloid"), 72 | isMarket: pack!("isMarket"), 73 | triggerPx: pack!("triggerPx"), 74 | tpsl: pack!("tpsl"), 75 | trigger: pack!("trigger"), 76 | limit: pack!("limit"), 77 | usdc: pack!("usdc"), 78 | toPerp: pack!("toPerp"), 79 | vaultAddress: pack!("vaultAddress"), 80 | isDeposit: pack!("isDeposit"), 81 | usd: pack!("usd"), 82 | hyperliquidChain: pack!("hyperliquidChain"), 83 | signatureChainId: pack!("signatureChainId"), 84 | token: pack!("token"), 85 | amount: pack!("amount"), 86 | time: pack!("time"), 87 | chain: pack!("chain"), 88 | isBuy: pack!("isBuy"), 89 | ntli: pack!("ntli"), 90 | isCross: pack!("isCross"), 91 | leverage: pack!("leverage"), 92 | subAccountUser: pack!("subAccountUser"), 93 | name: pack!("name"), 94 | code: pack!("code") 95 | } 96 | 97 | @grouping pack!("grouping") 98 | 99 | @groupings %{ 100 | "na" => pack!("na"), 101 | "normalTpsl" => pack!("normalTpsl"), 102 | "positionTpsl" => pack!("positionTpsl") 103 | } 104 | 105 | def type(key), do: [@action_type | @action_types[key]] 106 | 107 | def grouping(key), do: [@grouping | @groupings[key]] 108 | 109 | def field(:t, %{t: %{trigger: _}} = value), do: [@fields[:t] | pack_trigger(value[:t])] 110 | def field(key, value), do: [@fields[key] | pack!(value[key])] 111 | def fields([_|_] = keys, value), do: Enum.map(keys, &field(&1, value)) 112 | 113 | def pack_orders(orders) do 114 | Enum.reduce(orders, [@orders, first_byte(orders)], &(&2 ++ pack_order(&1))) 115 | end 116 | 117 | def pack_order(order) do 118 | bytes = [first_byte(order) | fields([:a, :b, :p, :s, :r, :t], order)] 119 | 120 | case Map.has_key?(order, :c) do 121 | true -> [bytes ++ field(:c, order)] 122 | _ -> bytes 123 | end 124 | end 125 | 126 | def pack_trigger(t) do 127 | [ 128 | first_byte(t), 129 | @fields[:trigger], 130 | first_byte(t[:trigger]), 131 | fields([:isMarket, :triggerPx, :tpsl], t[:trigger]) 132 | ] 133 | end 134 | 135 | def pack_cancel(%{cloid: _} = cancel) do 136 | [first_byte(cancel), fields([:asset, :cloid], cancel)] 137 | end 138 | 139 | def pack_cancel(cancel) do 140 | [first_byte(cancel), fields([:a, :o], cancel)] 141 | end 142 | 143 | def pack_cancels(cancels) do 144 | Enum.reduce(cancels, [@cancels, first_byte(cancels)], &(&2 ++ pack_cancel(&1))) 145 | end 146 | 147 | def pack_modifies(modifies) do 148 | Enum.reduce(modifies, [@modifies, first_byte(modifies)], &(&2 ++ [first_byte(&1) | pack_modify(&1)])) 149 | end 150 | 151 | def pack_modify(mod) do 152 | [@oid, pack!(mod[:oid]), @order] ++ pack_order(mod[:order]) 153 | end 154 | 155 | def pack_transfer(transfer) do 156 | [ 157 | @class_transfer, 158 | first_byte(transfer), 159 | fields([:usdc, :toPerp], transfer) 160 | ] 161 | end 162 | 163 | def pack_action(%{type: "setReferrer"} = action, nonce, vault_address) do 164 | [ 165 | first_byte(action), 166 | type(:setReferrer), 167 | fields([:code], action), 168 | add_additional_bytes(nonce, vault_address) 169 | ] 170 | end 171 | 172 | def pack_action(%{type: "updateLeverage"} = action, nonce, vault_address) do 173 | [ 174 | first_byte(action), 175 | type(:updateLeverage), 176 | fields([:asset, :isCross, :leverage], action), 177 | add_additional_bytes(nonce, vault_address) 178 | ] 179 | end 180 | 181 | def pack_action(%{type: "createSubAccount"} = action, nonce, vault_address) do 182 | [ 183 | first_byte(action), 184 | type(:createSubAccount), 185 | field(:name, action), 186 | add_additional_bytes(nonce, vault_address) 187 | ] 188 | end 189 | 190 | def pack_action(%{type: "subAccountTransfer"} = action, nonce, vault_address) do 191 | [ 192 | first_byte(action), 193 | type(:subAccountTransfer), 194 | fields([:subAccountUser, :isDeposit, :usd], action), 195 | add_additional_bytes(nonce, vault_address) 196 | ] 197 | end 198 | 199 | def pack_action(%{type: "subAccountSpotTransfer"} = action, nonce, vault_address) do 200 | [ 201 | first_byte(action), 202 | type(:subAccountSpotTransfer), 203 | fields([:subAccountUser, :isDeposit, :token, :amount], action), 204 | add_additional_bytes(nonce, vault_address) 205 | ] 206 | end 207 | 208 | def pack_action(%{type: "updateIsolatedMargin"} = action, nonce, vault_address) do 209 | [ 210 | first_byte(action), 211 | type(:updateIsolatedMargin), 212 | fields([:asset, :isBuy, :ntli], action), 213 | add_additional_bytes(nonce, vault_address) 214 | ] 215 | end 216 | 217 | def pack_action(%{type: "vaultTransfer"} = action, nonce, vault_address) do 218 | [ 219 | first_byte(action), 220 | type(:vaultTransfer), 221 | fields([:vaultAddress, :isDeposit, :usd], action), 222 | add_additional_bytes(nonce, vault_address) 223 | ] 224 | end 225 | 226 | def pack_action(%{type: "order"} = action, nonce, vault_address) do 227 | [ 228 | first_byte(action), 229 | type(:order), 230 | pack_orders(action[:orders]), 231 | grouping(action[:grouping]), 232 | add_additional_bytes(nonce, vault_address) 233 | ] 234 | end 235 | 236 | def pack_action(%{type: "cancel"} = action, nonce, vault_address) do 237 | [ 238 | first_byte(action), 239 | type(:cancel), 240 | pack_cancels(action[:cancels]), 241 | add_additional_bytes(nonce, vault_address) 242 | ] 243 | end 244 | 245 | def pack_action(%{type: "cancelByCloid"} = action, nonce, vault_address) do 246 | [ 247 | first_byte(action), 248 | type(:cancelByCloid), 249 | pack_cancels(action[:cancels]), 250 | add_additional_bytes(nonce, vault_address) 251 | ] 252 | end 253 | 254 | def pack_action(%{type: "modify"} = action, nonce, vault_address) do 255 | [ 256 | first_byte(action), 257 | type(:modify), 258 | pack_modify(action), 259 | add_additional_bytes(nonce, vault_address) 260 | ] 261 | end 262 | 263 | def pack_action(%{type: "batchModify"} = action, nonce, vault_address) do 264 | [ 265 | first_byte(action), 266 | type(:batchModify), 267 | pack_modifies(action[:modifies]), 268 | add_additional_bytes(nonce, vault_address) 269 | ] 270 | end 271 | 272 | def pack_action(%{type: "spotUser"} = action, nonce, vault_address) do 273 | [ 274 | first_byte(action), 275 | type(:spotUser), 276 | pack_transfer(action[:classTransfer]), 277 | add_additional_bytes(nonce, vault_address) 278 | ] 279 | end 280 | 281 | def first_byte(action), do: pack!(action) |> Enum.take(1) 282 | def first_byte(action, false), do: first_byte(action) |> to_binary() 283 | 284 | def to_binary(data), do: IO.iodata_to_binary(data) 285 | 286 | def address_to_bytes(address) do 287 | Utils.trim_0x(address) 288 | |> Base.decode16(case: :lower) 289 | |> case do 290 | {:ok, binary} -> binary 291 | {:error, _reason} -> raise "Invalid hexadecimal string" 292 | end 293 | end 294 | 295 | def add_additional_bytes(nonce, nil) do 296 | nonce_position = byte_size(<<>>) 297 | 298 | <<>> <> <<0::size(8 * 9)>> 299 | |> put_big_uint64(nonce, nonce_position) 300 | |> put_uint8(0, nonce_position + 8) 301 | end 302 | 303 | def add_additional_bytes(nonce, vault_address) do 304 | address_bytes = address_to_bytes(vault_address) 305 | nonce_position = byte_size(<<>>) 306 | 307 | <<>> <> <<0::size(8 * 29)>> 308 | |> put_big_uint64(nonce, nonce_position) 309 | |> put_uint8(1, nonce_position + 8) 310 | |> put_bytes(address_bytes, nonce_position + 9) 311 | end 312 | 313 | def put_big_uint64(data, value, position) do 314 | <> = data 315 | <> 316 | end 317 | 318 | def put_uint8(data, value, position) do 319 | <> = data 320 | <> 321 | end 322 | 323 | def put_bytes(data, bytes, position) do 324 | <> = data 325 | <> 326 | end 327 | end 328 | -------------------------------------------------------------------------------- /lib/hyperliquid/api/exchange.ex: -------------------------------------------------------------------------------- 1 | defmodule Hyperliquid.Api.Exchange do 2 | @moduledoc """ 3 | Module for interacting with the Hyperliquid Exchange API endpoints. 4 | 5 | This module provides functions to perform various operations on the Hyperliquid exchange, 6 | including placing and canceling orders, modifying orders, updating leverage, transferring 7 | funds, and managing sub-accounts. 8 | 9 | It uses the `Hyperliquid.Api` macro to set up the basic API interaction functionality. 10 | 11 | ## Functions 12 | 13 | ### Order Management 14 | - `place_order/1`, `place_order/2`, `place_order/3` - Place one or multiple orders 15 | - `cancel_orders/1`, `cancel_orders/2` - Cancel multiple orders 16 | - `cancel_order/2`, `cancel_order/3` - Cancel a single order 17 | - `cancel_order_by_cloid/2`, `cancel_order_by_cloid/3` - Cancel an order by client order ID 18 | - `cancel_orders_by_cloid/1`, `cancel_orders_by_cloid/2` - Cancel multiple orders by client order IDs 19 | - `modify_order/2`, `modify_order/3` - Modify an existing order 20 | - `modify_multiple_orders/1`, `modify_multiple_orders/2` - Modify multiple orders 21 | 22 | ### Account Management 23 | - `update_leverage/3` - Update leverage for an asset 24 | - `update_isolated_margin/3` - Update isolated margin for an asset 25 | - `spot_perp_transfer/2` - Transfer between spot and perpetual accounts 26 | - `vault_transfer/3` - Transfer to/from a vault 27 | - `create_sub_account/1` - Create a sub-account 28 | - `sub_account_transfer/3` - Transfer funds to/from a sub-account 29 | - `sub_account_spot_transfer/4` - Transfer spot tokens to/from a sub-account 30 | 31 | ### Withdrawal and Transfers 32 | - `usd_send/3` - Send USD to another address 33 | - `spot_send/4` - Send spot tokens to another address 34 | - `withdraw_from_bridge/3` - Withdraw funds from the bridge 35 | 36 | All functions return a tuple `{:ok, result}` on success, or `{:error, details}` on failure. 37 | """ 38 | use Hyperliquid.Api, context: "exchange" 39 | 40 | @doc """ 41 | Places one or multiple orders. 42 | 43 | ## Parameters 44 | 45 | - `order`: A single order or a list of orders 46 | - `grouping`: Order grouping (default: "na") 47 | - `vault_address`: Optional vault address 48 | 49 | ## Examples 50 | 51 | iex> Hyperliquid.Api.Exchange.place_order(order) 52 | {:ok, 53 | %{ 54 | "response" => %{ 55 | "data" => %{ 56 | "statuses" => [ 57 | %{"filled" => %{"avgPx" => "115.17", "oid" => 18422439200, "totalSz" => "1.0"}} 58 | ] 59 | }, 60 | "type" => "order" 61 | }, 62 | "status" => "ok" 63 | }} 64 | """ 65 | def place_order(order, grouping \\ "na", vault_address \\ nil) 66 | 67 | def place_order([_|_] = orders, grouping, vault_address) do 68 | post_action(%{type: "order", grouping: grouping, orders: orders}, vault_address) 69 | end 70 | 71 | def place_order(order, grouping, vault_address) do 72 | post_action(%{type: "order", grouping: grouping, orders: [order]}, vault_address) 73 | end 74 | 75 | @doc """ 76 | Cancels multiple orders. 77 | 78 | ## Parameters 79 | 80 | - `cancels`: List of orders to cancel 81 | - `vault_address`: Optional vault address 82 | 83 | ## Example 84 | 85 | iex> Hyperliquid.Api.Exchange.cancel_orders([%{a: 5, o: 123}, %{a: 5, o: 456}]) 86 | {:ok, %{...}} 87 | """ 88 | def cancel_orders([_|_] = cancels, vault_address \\ nil) do 89 | post_action(%{type: "cancel", cancels: cancels}, vault_address) 90 | end 91 | 92 | @doc """ 93 | Cancels a single order. 94 | 95 | ## Parameters 96 | 97 | - `asset`: Integer representing the asset's index in the coin list 98 | - `oid`: The order ID to cancel 99 | - `vault_address`: Optional vault address 100 | 101 | ## Example 102 | 103 | iex> Hyperliquid.Api.Exchange.cancel_order(5, 123) 104 | {:ok, %{...}} 105 | """ 106 | def cancel_order(asset, oid, vault_address \\ nil) do 107 | post_action(%{type: "cancel", cancels: [%{a: asset, o: oid}]}, vault_address) 108 | end 109 | 110 | @doc """ 111 | Cancels order by cloid. 112 | 113 | ## Parameters 114 | 115 | - `asset`: Integer representing the asset's index in the coin list 116 | - `cloid`: The cloid to cancel 117 | - `vault_address`: Optional vault address 118 | 119 | ## Example 120 | 121 | iex> Hyperliquid.Api.Exchange.cancel_order_by_cloid(5, "0x123") 122 | {:ok, %{...}} 123 | """ 124 | def cancel_order_by_cloid(asset, cloid, vault_address \\ nil) do 125 | post_action(%{type: "cancelByCloid", cancels: [%{asset: asset, cloid: cloid}]}, vault_address) 126 | end 127 | 128 | def cancel_orders_by_cloid([_|_] = cancels, vault_address \\ nil) do 129 | post_action(%{type: "cancelByCloid", cancels: cancels}, vault_address) 130 | end 131 | 132 | @doc """ 133 | Modifies an existing order. 134 | 135 | ## Parameters 136 | 137 | - `oid`: The order ID to modify 138 | - `order`: A map containing the new order details 139 | - `vault_address`: Optional vault address 140 | 141 | ## Example 142 | 143 | iex> Hyperliquid.Api.Exchange.modify_order(123, order) 144 | {:ok, %{...}} 145 | """ 146 | def modify_order(oid, order, vault_address \\ nil) do 147 | post_action(%{type: "modify", oid: oid, order: order}, vault_address) 148 | end 149 | 150 | def modify_multiple_orders(modifies, vault_address \\ nil) do 151 | post_action(%{type: "batchModify", modifies: modifies}, vault_address) 152 | end 153 | 154 | @doc """ 155 | Updates the leverage for a specific asset. 156 | 157 | ## Parameters 158 | 159 | - `asset`: Integer representing the asset's index in the coin list 160 | - `is_cross`: Boolean indicating whether to use cross margin 161 | - `leverage`: The new leverage value 162 | 163 | ## Example 164 | 165 | iex> Hyperliquid.Api.Exchange.update_leverage(1, true, 10) 166 | {:ok, %{...}} 167 | """ 168 | def update_leverage(asset, is_cross, leverage) do 169 | post_action(%{ 170 | type: "updateLeverage", 171 | asset: asset, 172 | isCross: is_cross, 173 | leverage: leverage 174 | }) 175 | end 176 | 177 | @doc """ 178 | Updates the isolated margin for a specific asset. 179 | 180 | ## Parameters 181 | 182 | - `asset`: Integer representing the asset's index in the coin list 183 | - `is_buy`: Boolean indicating whether it's a buy position 184 | - `ntli`: The new notional total liability increase 185 | 186 | ## Example 187 | 188 | iex> Hyperliquid.Api.Exchange.update_isolated_margin(1, true, 1000) 189 | {:ok, %{...}} 190 | """ 191 | def update_isolated_margin(asset, is_buy, ntli) do 192 | post_action(%{ 193 | type: "updateIsolatedMargin", 194 | asset: asset, 195 | isBuy: is_buy, 196 | ntli: ntli 197 | }) 198 | end 199 | 200 | @doc """ 201 | Transfers funds between spot and perpetual accounts. 202 | 203 | ## Parameters 204 | 205 | - `amount`: The amount to transfer (in USDC) 206 | - `to_perp`: Boolean indicating the direction of transfer (true for spot to perp, false for perp to spot) 207 | 208 | ## Example 209 | 210 | iex> Hyperliquid.Api.Exchange.spot_perp_transfer(1000, true) 211 | {:ok, %{...}} 212 | """ 213 | def spot_perp_transfer(amount, to_perp) do 214 | post_action(%{ 215 | type: "spotUser", 216 | classTransfer: %{ 217 | usdc: amount, 218 | toPerp: to_perp 219 | } 220 | }) 221 | end 222 | 223 | @doc """ 224 | Transfers funds to or from a vault. 225 | 226 | ## Parameters 227 | 228 | - `vault_address`: The address of the vault 229 | - `is_deposit`: Boolean indicating whether it's a deposit (true) or withdrawal (false) 230 | - `amount_usd`: The amount to transfer in USD (positive for transfer, negative for withdraw) 231 | 232 | ## Example 233 | 234 | iex> Hyperliquid.Api.Exchange.vault_transfer("0x1234...", true, 1000) 235 | {:ok, %{...}} 236 | """ 237 | def vault_transfer(vault_address, is_deposit, amount_usd) do 238 | post_action(%{ 239 | type: "vaultTransfer", 240 | vaultAddress: vault_address, 241 | isDeposit: is_deposit, 242 | usd: amount_usd 243 | }) 244 | end 245 | 246 | @doc """ 247 | Creates a new sub-account. 248 | 249 | ## Parameters 250 | 251 | - `name`: The name for the new sub-account 252 | 253 | ## Example 254 | 255 | iex> Hyperliquid.Api.Exchange.create_sub_account("trading_bot_1") 256 | {:ok, %{...}} 257 | """ 258 | def create_sub_account(name) do 259 | post_action(%{ 260 | type: "createSubAccount", 261 | name: name 262 | }) 263 | end 264 | 265 | @doc """ 266 | Transfers funds to or from a sub-account. 267 | 268 | ## Parameters 269 | 270 | - `user`: The address or identifier of the sub-account 271 | - `is_deposit`: Boolean indicating whether it's a deposit (true) or withdrawal (false) 272 | - `amount_usd`: The amount to transfer in USD cents (e.g., 1_000_000 = $1) 273 | 274 | ## Example 275 | 276 | iex> Hyperliquid.Api.Exchange.sub_account_transfer("0x5678...", true, 1_000_000) 277 | {:ok, %{...}} 278 | """ 279 | def sub_account_transfer(user, is_deposit, amount_usd) do 280 | post_action(%{ 281 | type: "subAccountTransfer", 282 | subAccountUser: user, 283 | isDeposit: is_deposit, 284 | usd: amount_usd # MUST BE INT VALUE - 1_000_000 = $1 285 | }) 286 | end 287 | 288 | @doc """ 289 | Transfers spot tokens to or from a sub-account. 290 | 291 | ## Parameters 292 | 293 | - `user`: The address or identifier of the sub-account 294 | - `is_deposit`: Boolean indicating whether it's a deposit (true) or withdrawal (false) 295 | - `token`: The token to transfer (e.g., "BTC", "ETH") 296 | - `amount`: The amount of the token to transfer 297 | 298 | ## Example 299 | 300 | iex> Hyperliquid.Api.Exchange.sub_account_spot_transfer("0x9876...", true, "BTC", 0.1) 301 | {:ok, %{...}} 302 | """ 303 | def sub_account_spot_transfer(user, is_deposit, token, amount) do 304 | post_action(%{ 305 | type: "subAccountSpotTransfer", 306 | subAccountUser: user, 307 | isDeposit: is_deposit, 308 | token: token, 309 | amount: amount 310 | }) 311 | end 312 | 313 | def set_referrer(code) do 314 | post_action(%{ 315 | type: "setReferrer", 316 | code: code 317 | }) 318 | end 319 | 320 | ####### non l1 actions with different signer ########## 321 | 322 | def usd_send(destination, amount, time) do 323 | %{ 324 | type: "usdSend", 325 | destination: Ethers.Utils.to_checksum_address(destination), 326 | amount: amount, 327 | time: time 328 | } 329 | |> set_action_chains(mainnet?()) 330 | |> post_action(time) 331 | end 332 | 333 | def spot_send(destination, token, amount, time) do 334 | # use Cache.get_token_key(name) to get the proper token key 335 | # tokenName:tokenId, e.g. "PURR:0xc1fb593aeffbeb02f85e0308e9956a90" 336 | post_action(%{ 337 | type: "spotSend", 338 | hyperliquidChain: if(mainnet?(), do: "Mainnet", else: "Testnet"), 339 | signatureChainId: if(mainnet?(), do: to_hex(42_161), else: to_hex(421_614)), 340 | destination: Ethers.Utils.to_checksum_address(destination), 341 | token: token, 342 | amount: amount, 343 | time: time 344 | }, nil, time) 345 | end 346 | 347 | @doc """ 348 | Withdraws funds from the bridge. 349 | 350 | ## Parameters 351 | 352 | - `destination`: The destination address 353 | - `amount`: The amount to withdraw 354 | - `time`: The timestamp for the withdrawal 355 | 356 | ## Returns 357 | 358 | `{:ok, result}` on success, where `result` contains the response from the API. 359 | `{:error, details}` on failure. 360 | 361 | ## Example 362 | 363 | iex> Hyperliquid.Api.Exchange.withdraw_from_bridge("0x1234...", 1000000, 1625097600) 364 | {:ok, %{...}} 365 | """ 366 | def withdraw_from_bridge(destination, amount, time) do 367 | post_action(%{ 368 | type: "withdraw3", 369 | hyperliquidChain: if(mainnet?(), do: "Mainnet", else: "Testnet"), 370 | signatureChainId: if(mainnet?(), do: to_hex(42_161), else: to_hex(421_614)), 371 | amount: amount, 372 | time: time, 373 | destination: Ethers.Utils.to_checksum_address(destination) 374 | }, time) 375 | end 376 | 377 | defp set_action_chains(action, true) do 378 | Map.merge(action, %{ 379 | hyperliquidChain: "Mainnet", 380 | signatureChainId: to_hex(42_161) 381 | }) 382 | end 383 | 384 | defp set_action_chains(action, false) do 385 | Map.merge(action, %{ 386 | hyperliquidChain: "Testnet", 387 | signatureChainId: to_hex(421_614) 388 | }) 389 | end 390 | end 391 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, 4 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, 5 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 6 | "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"}, 8 | "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, 9 | "ethereumex": {:hex, :ethereumex, "0.10.6", "6d75cac39b5b7a720b064fe48563f205d3d9784e5bde25f983dd07cf306c2a6d", [:make, :mix], [{:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "58cf926239dabf8bd1fc6cf50a37b926274240b7f58ba5b235a20b5500a9a7e1"}, 10 | "ethers": {:hex, :ethers, "0.4.5", "5af10678317ff183617072fc5970ee25d7145846e600db769f2e7a53641e6131", [:mix], [{:ethereumex, "~> 0.10.6", [hex: :ethereumex, repo: "hexpm", optional: false]}, {:ex_abi, "~> 0.7.0", [hex: :ex_abi, repo: "hexpm", optional: false]}, {:ex_rlp, "~> 0.6.0", [hex: :ex_rlp, repo: "hexpm", optional: false]}, {:ex_secp256k1, "~> 0.7.2", [hex: :ex_secp256k1, repo: "hexpm", optional: true]}, {:idna, "~> 6.1", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "885de526ae5a37bb0cefc882caac2e3b5dd06eff35a9fee43a58523556dda009"}, 11 | "ex_abi": {:hex, :ex_abi, "0.7.2", "9950d8aa764c74b8c89cc279c72ac786675aca315c08bc06b4a387407fe67873", [:mix], [{:ex_keccak, "~> 0.7.5", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1ca991d6eb497959aab4b3aaeb7d0f71b67d4b617d5689113da82d6e4cedc408"}, 12 | "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, 13 | "ex_eip712": {:hex, :ex_eip712, "0.3.0", "cfab1f6c038948d49dba899fdf07eb427242f6fefeeda91de87808f89754c9ed", [:mix], [{:rustler, "~> 0.26", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "42ccc0dc0cd664522834208428d79befe6ca506b279cca0b4a9f2d722f5e8036"}, 14 | "ex_keccak": {:hex, :ex_keccak, "0.7.5", "f3b733173510d48ae9a1ea1de415e694b2651f35c787e63f33b5ed0013fbfd35", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8a5e1cb7f96fff5e480ff6a121477b90c4fd8c150984086dffd98819f5d83763"}, 15 | "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, 16 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 17 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 18 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 19 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 20 | "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, 21 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 22 | "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, 23 | "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, 24 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 25 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 26 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 27 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 28 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 29 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 30 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, 31 | "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, 32 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 33 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 34 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 35 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 36 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 37 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, 38 | "rustler": {:hex, :rustler, "0.33.0", "4a5b0a7a7b0b51549bea49947beff6fae9bc5d5326104dcd4531261e876b5619", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "7c4752728fee59a815ffd20c3429c55b644041f25129b29cdeb5c470b80ec5fd"}, 39 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.7.2", "097f657e401f02e7bc1cab808cfc6abdc1f7b9dc5e5adee46bf2fd8fdcce9ecf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "7663faaeadc9e93e605164dcf9e69168e35f2f8b7f2b9eb4e400d1a8e0fe2999"}, 40 | "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, 41 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 42 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 43 | "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, 44 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 45 | "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, 46 | "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, 47 | } 48 | --------------------------------------------------------------------------------