Packages

Elixir client library for cryptocurrency exchanges — generated from CCXT specs via compile-time macros.

Current section

Files

Jump to
ccxt_client lib ccxt ws url_routing.ex
Raw

lib/ccxt/ws/url_routing.ex

defmodule CCXT.WS.UrlRouting do
@moduledoc """
URL-based account type detection for WebSocket channel selection.
Some exchanges (notably Bybit) use different channel names depending on the
WebSocket connection type. The connection URL indicates what type of account
is being used (spot, unified, usdc, etc.), and each watch method may need
a different topic/channel for each account type.
## URL Routing Types
| Type | Exchange | Pattern |
|------|----------|---------|
| `:dictionary` | Bybit | URL → account type via getPrivateType() |
| `:dictionary` | Gate | URL → market type via getMarketTypeByUrl() |
| `:binary` | Binance | URL → spot/swap via isSpotUrl() |
## Usage
For URL-routed channels, the subscription builder needs the WebSocket URL
to determine which topic to subscribe to:
# Get account type from URL
account_type = UrlRouting.get_account_type(url, url_patterns)
#=> "unified"
# Look up topic for this method and account type
topic = UrlRouting.get_topic(account_type, topic_dict)
#=> "wallet"
## Example: Bybit watchBalance
Bybit's watchBalance uses different topics based on connection type:
- spot URL → "outboundAccountInfo"
- unified URL → "wallet"
- usdc URL → no topic (unsupported)
The URL patterns and topic dictionaries are extracted from CCXT Pro and
stored in the exchange spec.
"""
@typedoc """
URL pattern with account type mapping.
- `pattern` - String to search for in URL (nil = default)
- `account_type` - Account type string when pattern matches
"""
@type url_pattern :: %{
pattern: String.t() | nil,
account_type: String.t()
}
@typedoc """
Topic dictionary mapping account types to channel names.
Keys are account type strings (e.g., "spot", "unified", "usdc").
Values are topic strings or lists of topics.
"""
@type topic_dict :: %{String.t() => String.t() | [String.t()]}
@doc """
Determines account type from URL using exchange's URL patterns.
Checks each pattern against the URL in order. The first match wins.
A pattern with `nil` acts as the default case.
## Examples
iex> patterns = [
...> %{pattern: "spot", account_type: "spot"},
...> %{pattern: "v5/private", account_type: "unified"},
...> %{pattern: nil, account_type: "usdc"}
...> ]
iex> CCXT.WS.UrlRouting.get_account_type("wss://stream.bybit.com/v5/private", patterns)
"unified"
iex> patterns = [
...> %{pattern: "spot", account_type: "spot"},
...> %{pattern: nil, account_type: "usdc"}
...> ]
iex> CCXT.WS.UrlRouting.get_account_type("wss://stream.bybit.com/contract", patterns)
"usdc"
"""
@spec get_account_type(String.t(), [url_pattern()]) :: String.t() | nil
def get_account_type(url, patterns) when is_binary(url) and is_list(patterns) do
find_matching_pattern(url, patterns)
end
@doc false
# Recursively searches URL patterns for first match. Returns the account_type
# of the first pattern whose string is found in the URL. A pattern with nil
# acts as default (matches everything). Handles both atom-keyed and string-keyed
# maps since JSON extraction produces string keys.
@spec find_matching_pattern(String.t(), [url_pattern()]) :: String.t() | nil
defp find_matching_pattern(_url, []), do: nil
defp find_matching_pattern(_url, [%{pattern: nil, account_type: default} | _rest]) do
# Default pattern (nil) matches everything
default
end
defp find_matching_pattern(url, [%{pattern: pattern, account_type: type} | rest]) do
if String.contains?(url, pattern) do
type
else
find_matching_pattern(url, rest)
end
end
# Handle string-keyed maps from JSON extraction
defp find_matching_pattern(_url, [%{"pattern" => nil, "account_type" => default} | _rest]) do
default
end
defp find_matching_pattern(url, [%{"pattern" => pattern, "account_type" => type} | rest]) do
if String.contains?(url, pattern) do
type
else
find_matching_pattern(url, rest)
end
end
@doc """
Gets topic from dictionary based on account type.
Returns the topic(s) for a given account type, or nil if not found.
## Examples
iex> topic_dict = %{"spot" => "outboundAccountInfo", "unified" => "wallet"}
iex> CCXT.WS.UrlRouting.get_topic("spot", topic_dict)
"outboundAccountInfo"
iex> topic_dict = %{"spot" => ["order", "stopOrder"], "unified" => ["order"]}
iex> CCXT.WS.UrlRouting.get_topic("spot", topic_dict)
["order", "stopOrder"]
iex> topic_dict = %{"spot" => "ticker"}
iex> CCXT.WS.UrlRouting.get_topic("usdc", topic_dict)
nil
"""
@spec get_topic(String.t(), topic_dict()) :: String.t() | [String.t()] | nil
def get_topic(account_type, topic_dict) when is_binary(account_type) and is_map(topic_dict) do
Map.get(topic_dict, account_type)
end
@doc """
Resolves the channel/topic for a URL-routed method.
Combines `get_account_type/2` and `get_topic/2` into a single function.
Returns `{:ok, topic}` if found, or `{:error, reason}` if not.
## Examples
iex> url_patterns = [
...> %{pattern: "spot", account_type: "spot"},
...> %{pattern: "v5/private", account_type: "unified"},
...> %{pattern: nil, account_type: "usdc"}
...> ]
iex> topic_dict = %{"spot" => "outboundAccountInfo", "unified" => "wallet"}
iex> CCXT.WS.UrlRouting.resolve_topic("wss://stream.bybit.com/spot/v3/ws", url_patterns, topic_dict)
{:ok, "outboundAccountInfo"}
iex> url_patterns = [%{pattern: nil, account_type: "usdc"}]
iex> topic_dict = %{"spot" => "ticker"}
iex> CCXT.WS.UrlRouting.resolve_topic("wss://stream.bybit.com/usdc", url_patterns, topic_dict)
{:error, {:no_topic_for_account_type, "usdc"}}
"""
@spec resolve_topic(String.t(), [url_pattern()], topic_dict()) ::
{:ok, String.t() | [String.t()]} | {:error, term()}
def resolve_topic(url, url_patterns, topic_dict) do
case get_account_type(url, url_patterns) do
nil ->
{:error, :no_matching_url_pattern}
account_type ->
case get_topic(account_type, topic_dict) do
nil -> {:error, {:no_topic_for_account_type, account_type}}
topic -> {:ok, topic}
end
end
end
end