defmodule CCXT.Credentials do @moduledoc """ API credentials for exchange authentication. Requires `api_key` and `secret` at minimum. Some exchanges also need `password` (OKX, KuCoin) or `uid`. Credentials are always passed explicitly — this module never reads from environment variables or application config. ## Examples {:ok, creds} = CCXT.Credentials.new(api_key: "abc", secret: "xyz") creds = CCXT.Credentials.new!(api_key: "abc", secret: "xyz", sandbox: true) """ @type t :: %__MODULE__{ api_key: String.t(), secret: String.t(), password: String.t() | nil, uid: String.t() | nil, sandbox: boolean() } @enforce_keys [:api_key, :secret] defstruct [:api_key, :secret, :password, :uid, sandbox: false] @doc """ Creates credentials from a keyword list. Returns `{:ok, credentials}` or `{:error, reason}`. ## Examples {:ok, creds} = CCXT.Credentials.new(api_key: "abc", secret: "xyz") {:error, :missing_api_key} = CCXT.Credentials.new(secret: "xyz") """ @allowed_keys [:api_key, :secret, :password, :uid, :sandbox] @spec new(keyword()) :: {:ok, t()} | {:error, :missing_api_key | :missing_secret | {:invalid_type, atom()} | {:unknown_key, atom()}} def new(opts) when is_list(opts) do with :ok <- validate_keys(opts), :ok <- validate_required(opts), :ok <- validate_types(opts) do {:ok, struct!(__MODULE__, opts)} end end @string_fields [:api_key, :secret, :password, :uid] # Validates no unknown keys are present defp validate_keys(opts) do case Enum.find(Keyword.keys(opts), &(&1 not in @allowed_keys)) do nil -> :ok key -> {:error, {:unknown_key, key}} end end # Validates required fields are present defp validate_required(opts) do cond do is_nil(Keyword.get(opts, :api_key)) -> {:error, :missing_api_key} is_nil(Keyword.get(opts, :secret)) -> {:error, :missing_secret} true -> :ok end end # Validates string fields are strings when present defp validate_types(opts) do case Enum.find(@string_fields, fn key -> value = Keyword.get(opts, key) not is_nil(value) and not is_binary(value) end) do nil -> :ok key -> {:error, {:invalid_type, key}} end end @doc """ Creates credentials from a keyword list, raising on invalid input. ## Examples creds = CCXT.Credentials.new!(api_key: "abc", secret: "xyz") """ @spec new!(keyword()) :: t() def new!(opts) when is_list(opts) do case new(opts) do {:ok, credentials} -> credentials {:error, :missing_api_key} -> raise ArgumentError, "api_key is required" {:error, :missing_secret} -> raise ArgumentError, "secret is required" {:error, {:unknown_key, key}} -> raise ArgumentError, "unknown key: #{inspect(key)}" {:error, {:invalid_type, key}} -> raise ArgumentError, "#{key} must be a string, got: #{inspect(Keyword.get(opts, key))}" end end end