# `DpExchange.Core.Types.Conversion`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.120/lib/dp_exchange/core/types/conversion.ex#L1)

A conversion between two assets — quoted first, committed second.

## This is the facade's only two-step operation, and the gap between the steps is the risk

Every other write in this contract is one call: place an order, cancel it, stake. A
conversion is two. The venue quotes a rate, hands back an identifier, and holds that rate
for a short window; the caller then commits against the identifier or lets it lapse.

**`:expires_at` is the whole reason this type exists.** A caller that commits an expired
quote does not get the rate it was shown. Depending on the venue it gets an error — or a
fill at the current rate, which is the dangerous case, because the operation appears to
have succeeded and the numbers are all real. The window is typically seconds.

`nil` means the venue did not state an expiry, **not** that the quote is open-ended. A
caller treating an unstated expiry as unlimited is making the same mistake as reading a
missing liquidation price as safety.

## `:status` distinguishes a quote from a trade

The same identifier addresses both stages, so the struct carries which stage it is in:

  * `:quoted` — a rate is held; nothing has moved
  * `:committed` — the caller accepted; the venue is executing
  * `:settled` — the assets have moved
  * `:expired` — the window closed unaccepted
  * `:failed` — the venue rejected or could not complete it
  * `:cancelled` — withdrawn before it settled. Not `:expired`: a window that closed and a
    quote someone cancelled are different events, and Coinbase reports them as two

**`:quoted` is not a conversion that happened.** A package that reported a quote as
complete would be reporting an intention as a fact.

# `status`

```elixir
@type status() :: :quoted | :committed | :settled | :expired | :failed | :cancelled
```

# `t`

```elixir
@type t() :: %DpExchange.Core.Types.Conversion{
  expires_at: DateTime.t() | nil,
  fee: Decimal.t() | nil,
  from_amount: Decimal.t() | nil,
  from_asset: String.t(),
  id: String.t(),
  provider: atom(),
  rate: Decimal.t() | nil,
  status: status(),
  to_amount: Decimal.t() | nil,
  to_asset: String.t(),
  venue_time: DateTime.t() | nil
}
```

# `expired?`

```elixir
@spec expired?(t(), DateTime.t()) :: boolean() | nil
```

Whether a quote has reached its stated expiry, as at `now`.

**The instant of `:expires_at` itself is expired.** A rate held "until 12:00:00" is not one
to commit at 12:00:00: the venue's clock is not this caller's, and a commit that lands on the
boundary is the one that is refused or filled at the current rate. So `now == expires_at` is
`true`, and a quote is open only strictly before it. The caller's clock skew is the caller's
to allow for: pass a `now` ahead of its own clock by that margin.

Returns `nil` when the venue stated no expiry — **unknown, not "still valid"**. A caller
that cannot establish the window should re-quote rather than assume it has one.

This is advisory. The venue decides whether a commit succeeds, and a quote can be refused
inside its stated window; asking here is how a caller avoids the round trip, not how it
learns the outcome.

# `new`

```elixir
@spec new(keyword() | map()) :: t()
```

Builds a `t:t/0`, failing closed if a required field is absent or `nil`.

`@enforce_keys` guards presence, not `nil` — see `DpExchange.Core.Types.Validate`.

A `:rate` of zero or less raises (a rate a caller multiplies by is above zero), and so does a
`:from_amount` or `:to_amount` below zero: a conversion moves money, and a negative amount is
a sign flipped in a decode that every other field stays plausible around. Zero amounts stay
legal (a quote is held before an amount settles).

---

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