# `CCXT.Testnet`
[🔗](https://github.com/ZenHive/ccxt_client/blob/main/lib/ccxt/testnet.ex#L1)

ETS-backed credential registry for integration testing.

Supports multiple credential sets per exchange for multi-API exchanges
(e.g., Binance spot vs futures testnets require separate credentials).

Credentials are registered once at test startup (in `test_helper.exs`),
then retrieved by generated tests at runtime. ETS with `read_concurrency: true`
allows lock-free reads from any process.

## Usage

    # In test_helper.exs - register credentials for each sandbox
    CCXT.Testnet.register_all_from_env([
      {:bybit, testnet: true},
      {:binance, testnet: true},
      {:binance, :futures, testnet: true},
      {:okx, testnet: true, passphrase: true}
    ])

    # In tests - retrieve credentials for specific sandbox
    creds = CCXT.Testnet.creds(:bybit)             # default sandbox
    creds = CCXT.Testnet.creds(:binance, :futures) # futures sandbox

## Sandbox Keys

Multi-API exchanges have different testnets per API section:

| Sandbox Key | Env Var Infix | Example Hostname           |
|-------------|---------------|----------------------------|
| `:default`  | (none)        | testnet.binance.vision     |
| `:futures`  | `_FUTURES`    | testnet.binancefuture.com  |
| `:coinm`    | `_COINM`      | testnet.binancefuture.com/dapi |

## Env Var Convention

- `{EXCHANGE}[_{SANDBOX}]_TESTNET_API_KEY`
- `{EXCHANGE}[_{SANDBOX}]_TESTNET_API_SECRET`
- `{EXCHANGE}_PASSPHRASE` (if `passphrase: true`)

When `:testnet` is true, the `_TEST_` infix is accepted as a silent fallback
(e.g. `BINANCE_FUTURES_TEST_API_KEY` is treated as an alias for
`BINANCE_FUTURES_TESTNET_API_KEY`). The canonical `_TESTNET_` name remains
preferred and is the one surfaced in `creds!/2` error messages.

# `config_entry`

```elixir
@type config_entry() :: {atom(), register_opts()} | {atom(), atom(), register_opts()}
```

Config entry for register_all_from_env/1

# `register_opts`

```elixir
@type register_opts() :: [
  testnet: boolean(),
  passphrase: boolean(),
  sandbox: boolean(),
  secret_suffix: String.t()
]
```

Options for register_from_env/2,3

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear`

```elixir
@spec clear() :: :ok
```

Clears all registered credentials (for test isolation).

# `creds`

```elixir
@spec creds(atom(), atom()) :: CCXT.Credentials.t() | nil
```

Get credentials for an exchange/sandbox. Returns `nil` if unregistered.

# `creds!`

```elixir
@spec creds!(atom(), atom()) :: CCXT.Credentials.t()
```

Get credentials or raise `ArgumentError` with a helpful message.

# `env_var_prefix`

```elixir
@spec env_var_prefix(atom(), atom()) :: String.t()
```

Env var prefix for a given exchange + sandbox combination.

## Examples

    iex> CCXT.Testnet.env_var_prefix(:binance, :default)
    "BINANCE_TESTNET"

    iex> CCXT.Testnet.env_var_prefix(:binance, :futures)
    "BINANCE_FUTURES_TESTNET"

# `exchanges_with_creds`

```elixir
@spec exchanges_with_creds() :: [atom()]
```

List unique exchange atoms with any registered credentials.

# `register`

```elixir
@spec register(atom(), atom() | keyword(), keyword()) :: :ok | :skipped
```

Register credentials directly.

Returns `:ok` if credentials validate, `:skipped` if required fields are missing.

# `register_all_from_env`

```elixir
@spec register_all_from_env([config_entry()]) :: [{atom(), atom()}]
```

Register credentials for multiple exchanges from environment variables.

Returns the list of successfully registered `{exchange, sandbox_key}` tuples.

# `register_from_env`

```elixir
@spec register_from_env(atom(), atom() | register_opts(), register_opts()) ::
  :ok | :skipped
```

Register credentials from environment variables.

Env var pattern: `{EXCHANGE}[_{SANDBOX}][_TESTNET]_API_KEY|_API_SECRET`.

## Options

- `:testnet` - Include `_TESTNET` in env var names (default: `false`)
- `:passphrase` - Also load passphrase from `{EXCHANGE}_PASSPHRASE` (default: `false`)
- `:sandbox` - Value for `credentials.sandbox` (default: value of `:testnet`)
- `:secret_suffix` - Override secret env var suffix (default: `"API_SECRET"`)

Returns `:ok` on success, `:skipped` when required env vars are absent.

# `registered?`

```elixir
@spec registered?(atom(), atom()) :: boolean()
```

Returns `true` when credentials are registered for the given exchange/sandbox.

# `registered_exchanges`

```elixir
@spec registered_exchanges() :: [{atom(), atom()}]
```

List all registered `{exchange, sandbox_key}` tuples.

# `sandbox_key_from_url`

```elixir
@spec sandbox_key_from_url(String.t() | nil) :: atom()
```

Derive sandbox_key from a sandbox URL's hostname/path.

## Examples

    iex> CCXT.Testnet.sandbox_key_from_url("https://testnet.binance.vision/api/v3")
    :default

    iex> CCXT.Testnet.sandbox_key_from_url("https://testnet.binancefuture.com/fapi/v1")
    :futures

    iex> CCXT.Testnet.sandbox_key_from_url("https://testnet.binancefuture.com/dapi/v1")
    :coinm

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts the Testnet registry GenServer (owns the ETS table).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
