defmodule Rujira.Fin.Order do
@moduledoc """
Trading order for the FIN protocol.
Struct, construction, and queries. Use `Rujira.Fin` as the public API.
"""
alias Rujira.Amount
alias Rujira.Assets
alias Rujira.Contracts
alias Rujira.Math
alias Rujira.Prices
use Memoize
defstruct id: nil,
pair: nil,
owner: nil,
side: nil,
rate: 0,
updated_at: nil,
offer: 0,
offer_value: 0,
remaining: 0,
remaining_value: 0,
filled: 0,
filled_value: 0,
filled_fee: 0,
value_usd: 0,
type: nil,
deviation: nil
@type side :: :base | :quote
@type deviation :: nil | integer()
@type type_order :: :fixed | :oracle
@type t :: %__MODULE__{
id: String.t(),
pair: String.t(),
owner: String.t(),
side: side,
rate: Decimal.t(),
updated_at: DateTime.t(),
offer: Amount.t(),
offer_value: Amount.t(),
remaining: Amount.t(),
remaining_value: Amount.t(),
filled: Amount.t(),
filled_value: Amount.t(),
filled_fee: Amount.t(),
value_usd: Amount.t(),
type: type_order,
deviation: deviation
}
# --- Construction ---
@spec new(Rujira.Fin.Pair.t(), map()) :: {:ok, t()} | {:error, term()}
def new(
%{
address: address,
fee_taker: fee_taker,
token_quote: token_quote,
token_base: token_base
},
%{
"owner" => owner,
"side" => side,
"price" => price,
"rate" => rate,
"updated_at" => updated_at,
"offer" => offer,
"remaining" => remaining,
"filled" => filled
}
) do
with {type, deviation, price_id} <- parse_price(price),
{:ok, rate} <- Math.to_decimal(rate),
{:ok, updated_at} <- Math.to_integer(updated_at),
{:ok, updated_at} <- DateTime.from_unix(updated_at, :nanosecond),
{:ok, offer} <- Amount.new(offer),
{:ok, remaining} <- Amount.new(remaining),
{:ok, filled} <- Amount.new(filled),
{:ok, fee_taker} <- Math.to_decimal(fee_taker),
{:ok, asset_quote} <- Assets.from_denom(token_quote),
{:ok, asset_base} <- Assets.from_denom(token_base) do
side = String.to_existing_atom(side)
filled_fee = Math.mul_floor(filled, fee_taker)
remaining_value = value(remaining, rate, side)
filled_value = value(filled, Decimal.div(Decimal.new(1), rate), side)
{:ok,
%__MODULE__{
id: "#{address}/#{side}/#{price_id}/#{owner}",
pair: address,
owner: owner,
side: side,
rate: rate,
updated_at: updated_at,
offer: offer,
offer_value: value(offer, rate, side),
remaining: remaining,
remaining_value: remaining_value,
filled: filled,
filled_value: filled_value,
filled_fee: filled_fee,
type: type,
deviation: deviation,
value_usd:
case side do
:quote ->
Prices.value_usd(asset_quote.symbol, remaining) +
Prices.value_usd(asset_base.symbol, filled)
:base ->
Prices.value_usd(asset_base.symbol, remaining) +
Prices.value_usd(asset_quote.symbol, filled)
end
}}
end
end
defp placeholder(address, side, price, owner) do
[type | _] = String.split(price, ":")
%__MODULE__{
id: "#{address}/#{side}/#{price}/#{owner}",
pair: address,
owner: owner,
side: String.to_existing_atom(side),
rate: Decimal.new(0),
updated_at: DateTime.utc_now(),
offer: 0,
remaining: 0,
filled: 0,
type: String.to_existing_atom(type),
deviation: nil
}
end
# --- Queries ---
@spec list(Rujira.Fin.Pair.t(), String.t(), integer()) ::
{:ok, [t()]} | {:error, term()}
def list(pair, address, limit \\ 30)
def list(pair, address, limit) do
case query_list(pair.address, address, limit) do
{:ok, %{"orders" => orders}} ->
Rujira.Enum.reduce_while_ok(orders, &new(pair, &1))
err ->
err
end
end
@spec list_all(Rujira.Fin.Pair.t(), keyword()) :: {:ok, [t()]} | {:error, term()}
def list_all(%{address: address} = pair, opts) do
with {:ok, raw_orders} <- query_all(address, opts) do
Rujira.Enum.reduce_while_ok(raw_orders, &new(pair, &1))
end
end
@spec load(Rujira.Fin.Pair.t(), String.t(), String.t(), String.t()) ::
{:ok, t()} | {:error, term()}
def load(%{address: address} = pair, side, price, owner) do
case query(address, owner, side, price) do
{:ok, order} ->
new(pair, order)
{:error, %GRPC.RPCError{status: 2, message: "NotFound: query wasm contract failed"}} ->
{:ok, placeholder(address, side, price, owner)}
err ->
err
end
end
@spec list_all_pairs(String.t()) :: {:ok, [t()]} | {:error, term()}
def list_all_pairs(address) do
with {:ok, pairs} <- Rujira.Fin.Pair.list(),
{:ok, orders} <-
Rujira.Enum.reduce_async_while_ok(pairs, &list(&1, address), timeout: 15_000) do
{:ok, List.flatten(orders)}
end
end
@spec from_id(String.t()) :: {:ok, t()} | {:error, term()}
def from_id(id) do
with [pair_address, side, price, owner] <- String.split(id, "/"),
{:ok, pair} <- Rujira.Fin.Pair.get(pair_address) do
load(pair, side, price, owner)
else
{:error, err} -> {:error, err}
_ -> {:error, :invalid_id}
end
end
# --- Private ---
defp parse_price(%{"fixed" => v}), do: {:fixed, nil, "fixed:#{v}"}
defp parse_price(%{"oracle" => v}), do: {:oracle, v, "oracle:#{v}"}
defp decode_price("fixed:" <> v), do: %{fixed: v}
defp decode_price("oracle:" <> v) do
{:ok, val} = Math.to_integer(v)
%{oracle: val}
end
defmemop query(address, owner, side, price) do
Contracts.query_state_smart(
address,
%{order: [owner, side, decode_price(price)]}
)
end
defmemo query_list(contract, address, limit \\ 30) do
Contracts.query_state_smart_with_retry(contract, %{
orders: %{owner: address, limit: limit}
})
end
defp query_all(contract, opts, cursor \\ nil, limit \\ 30) do
Contracts.query_state_smart(
contract,
%{orders: %{owner: nil, start_after: to_cursor(cursor), limit: limit}},
opts
)
|> Contracts.paginate("orders", limit, fn orders ->
query_all(contract, opts, List.last(orders), limit)
end)
end
defp to_cursor(nil), do: nil
defp to_cursor(%{"owner" => o, "side" => s, "price" => p}), do: [o, s, p]
defp value(amount, rate, :base), do: Math.mul_floor(amount, rate)
defp value(amount, rate, :quote), do: Math.div_floor(amount, rate)
end