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.
Summary
Types
@type status() :: :quoted | :committed | :settled | :expired | :failed | :cancelled
@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 }
Functions
@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.
Builds a 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).