defmodule CCXT.Signing.Custom do @moduledoc """ Custom signing escape hatch for exchanges with non-standard authentication. Delegates to a user-provided module that implements `CCXT.Signing.Behaviour`. ## Configuration signing: %{ pattern: :custom, custom_module: MyApp.CustomSigners.MyExchange } ## Validation {:ok, _} = CCXT.Signing.Custom.validate_module(MyApp.CustomSigners.MyExchange) See `CCXT.Signing.Behaviour` for the full contract and available helpers. """ @behaviour CCXT.Signing.Behaviour alias CCXT.Credentials alias CCXT.Signing @doc """ Validates that a module implements the signing contract (`sign/3`). Returns `{:ok, module}` if the module exports `sign/3`, or `{:error, reason}` with an actionable message. """ @spec validate_module(module()) :: {:ok, module()} | {:error, String.t()} def validate_module(module) when is_atom(module) do case Code.ensure_loaded(module) do {:module, _} -> if function_exported?(module, :sign, 3) do {:ok, module} else {:error, "#{inspect(module)} must implement sign/3 (see CCXT.Signing.Behaviour)"} end {:error, reason} -> {:error, "Could not load #{inspect(module)}: #{reason}"} end end @doc """ Delegates signing to the custom module specified in config. Raises `ArgumentError` if `:custom_module` is not specified or invalid. """ @impl true @spec sign(Signing.request(), Credentials.t(), Signing.config()) :: Signing.signed_request() def sign(request, credentials, config) do case Map.fetch(config, :custom_module) do {:ok, module} when is_atom(module) -> case validate_module(module) do {:ok, _} -> module.sign(request, credentials, config) {:error, reason} -> raise ArgumentError, reason end _ -> raise ArgumentError, """ Custom signing pattern requires :custom_module in config. Example: signing: %{ pattern: :custom, custom_module: MyApp.CustomSigners.MyExchange } """ end end end