# `DpExchange.Core.AdapterContract`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.76/lib/dp_exchange/core/adapter_contract.ex#L1)

The conformance suite every venue package runs in its own CI.

**This is the mechanism.** Prose in six `CLAUDE.md` files drifts; a suite that runs in
five CI pipelines cannot.

## Why this lives in `lib/` and not `test/support/`

It was in `test/support/` first, following the reference repo, and **that does not
work for a suite consumers run.** `elixirc_paths(:test)` governs *this* package's own
build; a dependency is not compiled in the `:test` environment, so the file shipped
inside the tarball and was never compiled into the consumer. A venue package's
`use DpExchange.Core.AdapterContract` failed with *module not loaded and could not be
found* — the suite present on disk and absent from the code path.

Shipping a file is not the same as shipping a module. A public testing API belongs in
`lib/`, which is why `Ecto.Adapters.SQL.Sandbox` and `Phoenix.ConnTest` live there.

`use ExUnit.Case` appears only inside the generated block, so nothing here needs
ExUnit at compile time and this compiles cleanly as a dependency.

    defmodule DpExchange.Coinbase.ContractTest do
      use DpExchange.Core.AdapterContract,
        venue: DpExchange.Coinbase,
        symbol_format: DpExchange.Coinbase.SymbolFormat,
        sample_pairs: ~w(BTC-USDC ETH-USDC),
        credentials: %{api_key: "test", api_secret: "test"}
    end

## What it asserts, and what it deliberately does not

Thirty-one groups, listed in `assertions/0`. The load-bearing one is **capabilities and
behaviour agreeing in both directions**: over-declaring fails in a caller's hands at
runtime, and under-declaring hides working functionality. Holding both is what makes
`capabilities/0` trustworthy enough for a consumer to branch on instead of branching on
venue identity.

**Maturity is asserted for presence, not for truth.** Every active endpoint must declare
`:proven` or `:experimental`, because an absent value is the failure this prevents.
Whether a `:proven` claim is *true* is not machine-checkable, and a suite that pretended
otherwise would be worse than one that admits the limit.

**No assertion may name a socket, a channel string, a transport module or a polling
interval.** If one does, mechanism has leaked through the facade and the assertion is the
bug, not the venue.

## The fake satisfies the same suite as the real adapter

That is the ratchet. The suite's job is not to anticipate every way a fake can diverge —
it is to make each discovered divergence permanent. The reference implementation this
pattern comes from went from nothing to over a thousand lines by absorbing thirteen gaps
its first consumer found, and none reached an external user across thousands of downloads.

**Every gap found becomes a new assertion here.** A gap fixed only in one venue's fake is
a gap the next venue will reintroduce.

# `active_endpoint_fault`

```elixir
@spec active_endpoint_fault(term()) :: nil | :not_supported | :raised
```

What is wrong with a fake's answer to an endpoint its `capabilities/0` declares active,
or `nil` when nothing is.

  * `:not_supported` — the fake answered `{:error, :not_supported}`: the declaration and the
    behaviour disagree (assertion 12, over-declaring).
  * `:raised` — the call raised. `{:raised, error}` is how the suite's `call_on/3` reports
    it. A raise is not an answer, and treating it as one let a fake that crashed on every
    endpoint pass "does not answer :not_supported".

Any other answer, refusals and errors included, is not this check's business.

# `arg_shape`

```elixir
@spec arg_shape(atom(), non_neg_integer(), boolean()) :: [atom()]
```

How a call to `name/arity` is shaped, position by position: `:credentials`, `:symbol`,
`:timeframe`, `:opts`, or one of the order arguments.

`credentialed?` is whether the callback takes its credential positionally — see
`credentialed_callbacks/0`. A per-callback override wins over the shape the arity implies.
Both the call assertions and assertion 17's credential-stripped call are built from this one
table; the stripped call used to rebuild its own and ignore the overrides, so for
`place_order/3` and `replace_order/4` it sent a malformed order, the fake raised, and
"no `{:ok, _}` with credentials stripped" held for a reason that had nothing to do with
credentials.

# `assertions`

```elixir
@spec assertions() :: [{pos_integer(), String.t()}]
```

The assertion groups, for documentation and for a venue package to report against.

# `credential_gated_endpoints`

```elixir
@spec credential_gated_endpoints(DpExchange.Core.Capabilities.t(), [atom()]) :: [
  {atom(), arity()}
]
```

The endpoints assertion 17 asks a `credential_benefit: :required` venue's fake to refuse
when called with credentials stripped.

Every facade callback that is **answerable** (not `child_spec/1` or `start_link/1`, which
start something), **active** in `caps`, not one of the two whose own doc types the
credential `credentials() | nil`, not declared in `Capabilities.no_venue_contact`, and not
`market_status/1` on a venue whose `asset_classes` are exactly `[:crypto]` (crypto trades
continuously, so there is no session for a credential to gate).

This is public, and the assertion calls it, so that the selection can be shown to fail:
the previous tests of this decision rebuilt it by hand beside the fixtures and asserted
what the fixtures did, so no change to the decision could break them. The assertion also
refuses an **empty** result, because a loop over no endpoints reports green having
examined none.

# `credentialed_callbacks`

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

The names of the `Core.Venue` callbacks whose first argument is `credentials()`, read from
the behaviour's own `@callback` specs.

The suite shapes a call's arguments by whether the callback takes its credential
positionally. That used to be a hand-written list of eleven names, and it rotted in the
way a list does: `list_payment_methods/2`, `get_payment_method/3`, `get_notional_balances/3`,
`list_custody_fees/2`, `get_transactions/2`, `place_orders/3`, `cancel_all_orders/2`,
`preview_order/3`, `preview_replace/4`, `close_position/3` and `get_trade_volume/2` all take
`credentials()` first and none was on it. Each was therefore called with a symbol where the
credential belongs, a fake that matched on its credentials raised, and the raise was taken
for an answer by every assertion that tolerates one. A union such as
`credentials() | nil` (`test_connection/2`) counts.

# `foreign_modules`

```elixir
@spec foreign_modules([module()], MapSet.t(String.t()), Path.t()) :: [module()]
```

The modules in `modules` that belong to an application outside `permitted`.

`build_lib_dir` is `Path.join(Mix.Project.build_path(), "lib")`. A module found under it
belongs to the application named by its first path segment. A module found anywhere else is
OTP or the Elixir standard library and is permitted, and a module found nowhere is a
host-application module this package depends on without saying so, which is foreign.

**The previous check could not fire for a dependency.** It looked for `/deps/<name>/` in
the module's path, but Mix compiles a dependency into `_build/<env>/lib/<name>/ebin` and
loads it from there, never from `deps/`. Every dependency module therefore matched nothing
and was permitted, so a package linking an undeclared dependency passed. Only the
`:non_existing` branch ever rejected anything.

# `permitted_apps`

```elixir
@spec permitted_apps(atom(), [tuple()]) :: MapSet.t(String.t())
```

The applications a package may link against: its own, every declared dependency, and what
those depend on at runtime, as strings.

`deps` is `Mix.Project.config()[:deps]` in any of Mix's three shapes. The purity assertion
read it with `for {dep, _rest} <- deps`, which matches a two-element tuple only, so every
dependency declared with a requirement AND options — `{:jason, "~> 1.4", optional: true}`,
`{:foo, "~> 1.0", only: :test}` — was silently dropped from the declared set.

# `promised_types`

```elixir
@spec promised_types() :: %{required({atom(), non_neg_integer()}) =&gt; [module()]}
```

The `Core.Types` struct(s) each `Core.Venue` callback's success is typed as, read from the
behaviour's own `@callback` specs.

Returns `%{{name, arity} => [module]}` for every callback whose success type names a
`DpExchange.Core.Types.*` struct, and omits the rest. Read from the compiled specs rather
than written as a table, so it cannot drift from the contract it restates. Used by
assertion 5.

# `struct_answer?`

```elixir
@spec struct_answer?(term()) :: boolean()
```

Whether `result` is a success that gave assertion 5 a struct to check: `{:ok, struct}` or
`{:ok, [struct | _]}`.

Assertion 5 takes a refusal, an error and an empty list as "not a success, so nothing to
compare", which is right for each answer alone. Across a whole fake it is not: a fake that
refused every endpoint (a missing `endpoint_opts:`) or returned `{:ok, []}` everywhere
passed the group having compared nothing. The group requires at least one answer for which
this is `true`.

# `validate_order_args!`

```elixir
@spec validate_order_args!(term()) :: %{optional(atom()) =&gt; term()}
```

Checks the `order_args:` option of `use DpExchange.Core.AdapterContract`. It must be a map
whose keys are drawn from `:order_request`, `:order_requests` and `:order_changes`.

An unknown key raises at compile time instead of being ignored. A misspelt key that was
silently dropped would leave the suite's default order in place, and the assertion it
was meant to reach would go on failing, or passing, for a reason nobody declared.

---

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