defmodule Dwarves.BinanceSpot do
alias Binance.Rest.HTTPClient
@endpoint Application.compile_env(
:dwarves_binancex,
:spot_url,
"https://api.binance.com"
)
@testnet_endpoint Application.compile_env(
:dwarves_binancex,
:spot_testnet_url,
"https://testnet.binance.vision"
)
require Logger
def get_endpoint(is_testnet) do
case is_testnet do
true -> @testnet_endpoint
false -> @endpoint
end
end
# Server
@doc """
Pings binance API. Returns `{:ok, %{}}` if successful, `{:error, reason}` otherwise
"""
def ping(is_testnet \\ false) do
endpoint = get_endpoint(is_testnet)
HTTPClient.get_binance("#{endpoint}/sapi/v1/ping", [])
end
# Wallet
@doc """
Get Daily Account Snapshot.
Returns `{:ok, %{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://binance-docs.github.io/apidocs/spot/en/#daily-account-snapshot-user_data to understand all the parameters
## Examples
```
account_snapshot(%{"type" => "FUTURES", "start_time" => 1642032000000, "end_time" => 1642032000000}, "api_key", "api_secret")
```
Result:
```
{:ok,
%{
code: 200,
msg: "",
snapshotVos: [...]
}
or
{:error, {:binance_error, %{
code: -3026,
msg: "request param 'type' wrong, should be in ('SPOT', 'MARGIN', 'FUTURES')"
}
}
}
```
"""
def account_snapshot(
%{"type" => type} = params,
api_key,
api_secret,
is_testnet \\ false
) do
arguments =
%{
type: type,
recvWindow: get_receiving_window(params["receiving_window"]),
timestamp: get_timestamp(params["timestamp"])
}
|> Map.merge(
unless(
is_nil(params["start_time"]),
do: %{startTime: params["start_time"]},
else: %{}
)
)
|> Map.merge(
unless(
is_nil(params["end_time"]),
do: %{endTime: params["end_time"]},
else: %{}
)
)
|> Map.merge(
unless(
is_nil(params["limit"]),
do: %{limit: params["limit"]},
else: %{}
)
)
endpoint = get_endpoint(is_testnet)
case HTTPClient.get_binance(
"#{endpoint}/sapi/v1/accountSnapshot",
arguments,
api_secret,
api_key
) do
{:error, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
data ->
data
end
end
# Corporate (sub-account)
@doc """
Universal Transfer (For Master Account).
Returns `{:ok, %{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://binance-docs.github.io/apidocs/spot/en/#universal-transfer-for-master-account to understand all the parameters
## Examples
```
universal_transfer(%{"from_email" => "email@gmail.com", "to_email" => "email@gmail.com", "from_account_type" => "USDT_FUTURE", "to_account_type" => "SPOT", "asset" => "USDT", "amount" => 100},"api_key", "secret_key")
```
Result:
```
{:ok,
%{
tranId: "tranId"
}
or
{:error, {:binance_error, %{
code: -1000,
msg: "No enum constant com.binance.accountsubuser.enums.TranferWay.FUTURE_TO_FUTURE"
}
}
}
```
"""
def universal_transfer(
%{
"from_account_type" => from_account_type,
"to_account_type" => to_account_type,
"asset" => asset,
"amount" => amount
} = params,
api_key,
api_secret
) do
arguments =
%{
fromAccountType: from_account_type,
toAccountType: to_account_type,
asset: asset,
amount: amount,
recvWindow: get_receiving_window(params["receiving_window"]),
timestamp: get_timestamp(params["timestamp"])
}
|> Map.merge(
unless(
is_nil(params["from_email"]),
do: %{fromEmail: params["from_email"]},
else: %{}
)
)
|> Map.merge(
unless(
is_nil(params["to_email"]),
do: %{toEmail: params["to_email"]},
else: %{}
)
)
|> Map.merge(
unless(
is_nil(params["client_tran_id"]),
do: %{clientTranId: params["client_tran_id"]},
else: %{}
)
)
endpoint = get_endpoint(false)
case HTTPClient.signed_request_binance(
"#{endpoint}/sapi/v1/sub-account/universalTransfer",
arguments,
:post,
api_secret,
api_key
) do
{:ok, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
data ->
data
end
end
@doc """
Swap token
Returns `{:ok, %{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://binance-docs.github.io/apidocs/spot/en/#swap-trade to understand all the parameters
## Examples
```
swap(%{"quote_asset" => "BUSD", "base_asset" => "USDT", "quote_qty" => 1000.53}, "api_key", "api_secret")
```
Result:
```
{:ok,
%{
swapId: 2314
}
or
{:error, {:binance_error, %{
code: -1002,
msg: "You are not authorized to execute this request."
}
}
}
```
"""
def swap(
%{"quote_asset" => quote_asset, "base_asset" => base_asset, "quote_qty" => quote_qty} = params,
api_key,
api_secret,
is_testnet \\ false
) do
arguments =
%{
quoteAsset: quote_asset,
baseAsset: base_asset,
quoteQty: quote_qty,
recvWindow: get_receiving_window(params["receiving_window"]),
timestamp: get_timestamp(params["timestamp"])
}
endpoint = get_endpoint(is_testnet)
case HTTPClient.signed_request_binance(
"#{endpoint}/sapi/v1/bswap/swap",
arguments,
:post,
api_secret,
api_key,
Map.get(params, "request_opts", [])
) do
{:ok, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
data ->
data
end
end
@doc """
Get swap histories on binance by params
Returns `{:ok, []}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://binance-docs.github.io/apidocs/spot/en/#get-swap-history-user_data to understand all the parameters
## Examples
```
get_swap_histories(
"api_key",
"api_secret",
%{"swapId" => 227709205},
false
)
```
Result:
```
{:ok,
[
%Binance.SwapHistory{
base_asset: "BUSD",
base_qty: "9.9845313",
fee: "0.015",
price: "1.00004694",
quote_asset: "USDT",
quote_qty: "10",
status: 1,
swap_id: 227709205,
swap_time: 1661230512920
}
]}
or
{:error, {:binance_error, %{code: -1, msg: ""}}}
```
"""
def get_swap_histories(api_key, secret_key, params, is_testnet \\ false) do
endpoint = get_endpoint(is_testnet)
case HTTPClient.get_binance(
"#{endpoint}/sapi/v1/bswap/swap",
params,
secret_key,
api_key
) do
{:error, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
data ->
parse_swap_history_response(data)
end
end
@doc """
Get swap quote
Returns `{:ok, %{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://binance-docs.github.io/apidocs/spot/en/#request-quote-user_data to understand all the parameters
## Examples
```
quote(%{"quote_asset" => "BUSD", "base_asset" => "USDT", "quote_qty" => 300000}, "api_key", "api_secret")
```
Result:
```
{:ok,
%Binance.SwapQuote{
quote_asset: "USDT",
base_asset: "BUSD",
quote_qty: 300000,
base_qty: 299975,
price: 1.00008334,
slippage: 0.00007245,
fee: 120
}
or
{:error, {:binance_error, %{
code: -1002,
msg: "You are not authorized to execute this request."
}
}
}
```
"""
def quote(
%{"quote_asset" => quote_asset, "base_asset" => base_asset, "quote_qty" => quote_qty} = params,
api_key,
api_secret,
is_testnet \\ false
) do
arguments =
%{
quoteAsset: quote_asset,
baseAsset: base_asset,
quoteQty: quote_qty,
recvWindow: get_receiving_window(params["receiving_window"]),
timestamp: get_timestamp(params["timestamp"])
}
endpoint = get_endpoint(is_testnet)
case HTTPClient.get_binance(
"#{endpoint}/sapi/v1/bswap/quote",
arguments,
api_secret,
api_key
) do
{:ok, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
data ->
parse_swap_quote_response(data)
end
end
defp parse_swap_quote_response({:ok, res}) do
{:ok,
case res do
%{"code" => _code, "msg" => _msg} = error -> error
_ -> Binance.SwapQuote.new(res)
end}
end
defp parse_swap_history_response({:ok, responses}) do
{:ok,
Enum.map(responses, fn res ->
case res do
%{"code" => _code, "msg" => _msg} = error -> error
_ -> Binance.SwapHistory.new(res)
end
end)}
end
# Misc
defp get_timestamp(timestamp) do
case timestamp do
# timestamp needs to be in milliseconds
nil ->
:os.system_time(:millisecond)
val ->
val
end
end
defp get_receiving_window(receiving_window) do
case receiving_window do
nil ->
5000
val ->
val
end
end
@doc """
get spot account info from binance
Returns `{:ok, %{}}` or `{:error, reason}`.
In the case of a error on binance, for example with invalid parameters, `{:error, {:binance_error, %{code: code, msg: msg}}}` will be returned.
Please read https://binance-docs.github.io/apidocs/spot/en/#account-information-user_data to understand all the parameters
## Examples
```
get_account_info("api_key", "api_secret", true)
```
Result:
```
{:ok,
%Binance.SpotAccount{
maker_commission: 15,
taker_commission: 15,
buyer_commission: 0,
seller_commission: 0,
can_trade: true,
can_withdraw: true,
can_deposit: true,
brokered: false,
update_time: 123456789,
account_type: "SPOT",
"balances": [
%Binance.SpotBalance{
"asset": "BTC",
"free": "4723846.89208129",
"locked": "0.00000000"
},
%Binance.SpotBalance{
"asset": "LTC",
"free": "4763368.68006011",
"locked": "0.00000000"
}
],
"permissions": [
"SPOT"
]
}
}
or
{:error, {:binance_error, %{code: -2019, msg: "Margin is insufficient."}}}
```
"""
def get_account_info(api_key, secret_key, is_testnet \\ false) do
endpoint = get_endpoint(is_testnet)
case HTTPClient.get_binance(
"#{endpoint}/api/v3/account",
%{},
secret_key,
api_key
) do
{:error, %{"code" => code, "msg" => msg}} ->
{:error, {:binance_error, %{code: code, msg: msg}}}
{:error, error} ->
{:error, {:binance_error, error}}
data ->
parse_account_info(data)
end
end
def parse_account_info({:ok, response}) do
account_info =
response
|> Binance.SpotAccount.new()
balances =
account_info.balances
|> Enum.map(fn itm ->
itm |> Binance.SpotBalance.new()
end)
account_info =
account_info
|> Map.put(:balances, balances)
{:ok, account_info}
end
end