Add OpenAPI v3.0 Docs for API (#243)

* Add initial api docs using drf-spectacular

This commit adds the inital and basic docs using drf-spectacular.
It also adds some serializers to be used for automatically
generating response objects by drf-spectacular

* Update api docs with correct request and response objects

- TODO: /order route

* Fix typo in api docs

* Separate OpenAPI schemas into it's own file

* Update drf-spectacular and add API intro and logo

* Update API docs for GET /order

* Add api docs for POST /order route

* Update serializers.py
This commit is contained in:
redphix 2022-10-02 23:32:35 +05:30 committed by GitHub
parent 4d6f8067bb
commit 9a6d3d33a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1406 additions and 8 deletions

874
api/oas_schemas.py Normal file
View File

@ -0,0 +1,874 @@
import textwrap
from decouple import config
from drf_spectacular.utils import OpenApiExample, OpenApiParameter
from api.serializers import (
ListOrderSerializer,
OrderDetailSerializer,
StealthSerializer,
)
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME"))
PUBLIC_DURATION = 60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1
ESCROW_DURATION = 60 * int(config("INVOICE_AND_ESCROW_DURATION"))
BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
class MakerViewSchema:
post = {
"summary": "Create a maker order",
"description": textwrap.dedent(
f"""
Create a new order as a maker.
Default values for the following fields if not specified:
- `public_duration` - **{PUBLIC_DURATION}**
- `escrow_duration` - **{ESCROW_DURATION}**
- `bond_size` - **{BOND_SIZE}**
- `bondless_taker` - **false**
- `has_range` - **false**
- `premium` - **0**
"""
),
"responses": {
201: ListOrderSerializer,
400: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
409: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
},
}
class OrderViewSchema:
get = {
"summary": "Get order details",
"description": textwrap.dedent(
"""
Get the order details. Details include/exclude attributes according to what is the status of the order
The following fields are available irrespective of whether you are a participant or not (A participant is either a taker or a maker of an order)
All the other fields are only available when you are either the taker or the maker of the order:
- `id`
- `status`
- `created_at`
- `expires_at`
- `type`
- `currency`
- `amount`
- `has_range`
- `min_amount`
- `max_amount`
- `payment_method`
- `is_explicit`
- `premium`
- `satoshis`
- `bondless_taker`
- `maker`
- `taker`
- `escrow_duration`
- `total_secs_exp`
- `penalty`
- `is_maker`
- `is_taker`
- `is_participant`
- `maker_status`
- `taker_status`
- `price_now`
### Order Status
The response of this route changes according to the status of the order. Some fields are documented below (check the 'Responses' section)
with the status code of when they are available and some or not. With v1 API we aim to simplify this
route to make it easier to understand which fields are available on which order status codes.
`status` specifies the status of the order. Below is a list of possible values (status codes) and what they mean:
- `0` "Waiting for maker bond"
- `1` "Public"
- `2` "Paused"
- `3` "Waiting for taker bond"
- `4` "Cancelled"
- `5` "Expired"
- `6` "Waiting for trade collateral and buyer invoice"
- `7` "Waiting only for seller trade collateral"
- `8` "Waiting only for buyer invoice"
- `9` "Sending fiat - In chatroom"
- `10` "Fiat sent - In chatroom"
- `11` "In dispute"
- `12` "Collaboratively cancelled"
- `13` "Sending satoshis to buyer"
- `14` "Sucessful trade"
- `15` "Failed lightning network routing"
- `16` "Wait for dispute resolution"
- `17` "Maker lost dispute"
- `18` "Taker lost dispute"
Notes:
- both `price_now` and `premium_now` are always calculated irrespective of whether `is_explicit` = true or false
"""
),
"parameters": [
OpenApiParameter(
name="order_id",
location=OpenApiParameter.QUERY,
required=True,
type=int,
),
],
"responses": {
200: OrderDetailSerializer,
400: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
403: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
"default": "This order is not available",
},
},
},
404: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
"default": "Invalid order Id",
},
},
},
},
"examples": [
OpenApiExample(
"Order cancelled",
value={"bad_request": "This order has been cancelled by the maker"},
status_codes=[400],
),
OpenApiExample(
"When the order is not public and you neither the taker nor maker",
value={"bad_request": "This order is not available"},
status_codes=[400],
),
OpenApiExample(
"Order cancelled",
value={"bad_request": "This order has been cancelled collaborativelly"},
status_codes=[400],
),
OpenApiExample(
"When maker bond expires (as maker)",
value={
"bad_request": "Invoice expired. You did not confirm publishing the order in time. Make a new order."
},
status_codes=[400],
),
OpenApiExample(
"When Robosats node is down",
value={
"bad_request": "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware."
},
status_codes=[400],
),
],
}
take_update_confirm_dispute_cancel = {
"summary": "Update order",
"description": textwrap.dedent(
"""
Update an order
`action` field is required and determines what is to be done. Below
is an explaination of what each action does:
- `take`
- If the order has not expired and is still public, on a
successful take, you get the same response as if `GET /order`
was called and the status of the order was `3` (waiting for
taker bond) which means `bond_satoshis` and `bond_invoice` are
present in the response as well. Once the `bond_invoice` is
paid, you successfully become the taker of the order and the
status of the order changes.
- `pause`
- Toggle the status of an order from `1` to `2` and vice versa. Allowed only if status is `1` (Public) or `2` (Paused)
- `update_invoice`
- This action only is valid if you are the buyer. The `invoice`
field needs to be present in the body and the value must be a
valid LN invoice. Make sure to perform this action only when
both the bonds are locked. i.e The status of your order is
atleast `6` (Waiting for trade collateral and buyer invoice)
- `update_address`
- This action is only valid if you are the buyer. This action is
used to set an on-chain payout address if you wish to have your
payout be recieved on-chain. This enables on-chain swap for the
order, so even if you earlier had submitted a LN invoice, it
will be ignored. You get to choose the `mining_fee_rate` as
well. Mining fee rate is specified in sats/vbyte.
- `cancel`
- This action is used to cancel an existing order. You cannot cancel an order if it's in one of the following states:
- `1` - Cancelled
- `5` - Expired
- `11` - In dispute
- `12` - Collaboratively cancelled
- `13` - Sending satoshis to buyer
- `14` - Sucessful trade
- `15` - Failed lightning network routing
- `17` - Maker lost dispute
- `18` - Taker lost dispute
Note that there are penalties involved for cancelling a order
mid-trade so use this action carefully:
- As a maker if you cancel an order after you have locked your
maker bond, you are returend your bond. This may change in
the future to prevent DDoSing the LN node and you won't be
returend the maker bond.
- As a taker there is a time penalty involved if you `take` an
order and cancel it without locking the taker bond.
- For both taker or maker, if you cancel the order when both
have locked thier bonds (status = `6` or `7`), you loose your
bond and a percent of it goes as "rewards" to your
counterparty and some of it the platform keeps. This is to
discourage wasting time and DDoSing the platform.
- For both taker or maker, if you cancel the order when the
escrow is locked (status = `8` or `9`), you trigger a
collaborative cancel request. This sets
`(m|t)aker_asked_cancel` field to `true` depending on whether
you are the maker or the taker respectively, so that your
counterparty is informed that you asked for a cancel.
- For both taker or maker, and your counterparty asked for a
cancel (i.e `(m|t)aker_asked_cancel` is true), and you cancel
as well, a collaborative cancel takes place which returns
both the bonds and escrow to the respective parties. Note
that in the future there will be a cost for even
collaborativelly cancelling orders for both parties.
- `confirm`
- This is a **crucial** action. This confirms the sending and
recieving of fiat depending on whether you are a buyer or
seller. There is not much RoboSats can do to actually confirm
and verify the fiat payment channel. It is up to you to make
sure of the correct amount was recieved before you confirm.
This action is only allowed when status is either `9` (Sending
fiat - In chatroom) or `10` (Fiat sent - In chatroom)
- If you are the buyer, it simply sets `fiat_sent` to `true`
which means that you have sent the fiat using the payment
method selected by the seller and signals the seller that the
fiat payment was done.
- If you are the seller, be very careful and double check
before perorming this action. Check that your fiat payment
method was successful in recieving the funds and whether it
was the correct amount. This action settles the escrow and
pays the buyer and sets the the order status to `13` (Sending
satohis to buyer) and eventually to `14` (successful trade).
- `dispute`
- This action is allowed only if status is `9` or `10`. It sets
the order status to `11` (In dispute) and sets `is_disputed` to
`true`. Both the bonds and the escrow are settled (i.e RoboSats
takes custody of the funds). Disputes can take long to resolve,
it might trigger force closure for unresolved HTLCs). Dispute
winner will have to submit a new invoice for value of escrow +
bond.
- `submit_statement`
- This action updates the dispute statement. Allowed only when
status is `11` (In dispute). `satement` must be sent in the
request body and should be a string. 100 chars < length of
`statement` < 5000 chars. You need to discribe the reason for
raising a dispute. The `(m|t)aker_statement` field is set
respectively. Only when both parties have submitted thier
dispute statement, the order status changes to `16` (Waiting
for dispute resolution)
- `rate_user`
- You can rate your counterparty using this action. You can rate
your user from `1-5` using the `rate` field in the request
body. Only allowed in the following states:
- `13` - Sending satoshis to buyer
- `14` - Sucessful trade
- `15` - Failed lightning network routing
- `17` - Maker lost dispute
- `18` - Taker lost dispute
- `rate_platform`
- Let us know how much you love (or hate 😢) RoboSats.
You can rate the platform from `1-5` using the `rate` field in the request body
"""
),
"parameters": [
OpenApiParameter(
name="order_id",
location=OpenApiParameter.QUERY,
required=True,
type=int,
),
],
"responses": {
200: {
"type": "object",
"additionalProperties": {
"oneOf": [
{"type": "str"},
{"type": "number"},
{"type": "object"},
{"type": "boolean"},
],
},
},
400: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
},
"examples": [
OpenApiExample(
"User not authenticated",
value={
"bad_request": "Woops! It seems you do not have a robot avatar",
},
status_codes=[400],
),
],
}
class UserViewSchema:
post = {
"summary": "Create user",
"description": textwrap.dedent(
"""
Create a new Robot 🤖
`token_sha256` is the SHA256 hash of your token. Make sure you generate your token
using cryptographically secure methods. [Here's]() the function the Javascript
client uses to generate the tokens. Since the server only recieves the hash of the
token, it trusts the client with computing `length`, `counts` and `unique_values`
correctly. Check [here](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/utils/token.js#L13)
to see how the Javascript client copmutes these values. These values are optional,
but if provided, the api computes the entropy of the token adds two additional
fields to the response JSON - `token_shannon_entropy` and `token_bits_entropy`.
**Note: It is entirely the clients responsibilty to generate high entropy tokens, and the optional
parameters are provided to act as an aid to help determine sufficient entropy, but the server is happy
with just any sha256 hash you provide it**
`public_key` - PGP key associated with the user (Armored ASCII format)
`encrypted_private_key` - Private PGP key. This is only stored on the backend for later fetching by
the frontend and the key can't really be used by the server since it's protected by the token
that only the client knows. Will be made an optional parameter in a future release.
On the Javascript client, It's passphrase is set to be the secret token generated.
A gpg key can be created by:
```shell
gpg --full-gen-key
```
it's public key can be exported in ascii armored format with:
```shell
gpg --export --armor <key-id | email | name>
```
and it's private key can be exported in ascii armored format with:
```shell
gpg --export-secret-keys --armor <key-id | email | name>
```
"""
),
"responses": {
201: {
"type": "object",
"properties": {
"encrypted_private_key": {
"type": "string",
"description": "Armored ASCII PGP private key block",
},
"nickname": {
"type": "string",
"description": "Username generated (Robot name)",
},
"public_key": {
"type": "string",
"description": "Armored ASCII PGP public key block",
},
"referral_code": {
"type": "string",
"description": "User's referral code",
},
"token_bits_entropy": {"type": "integer"},
"token_shannon_entropy": {"type": "integer"},
"wants_stealth": {
"type": "boolean",
"default": False,
"description": "Whether the user prefers stealth invoices",
},
},
},
202: {
"type": "object",
"properties": {
"encrypted_private_key": {
"type": "string",
"description": "Armored ASCII PGP private key block",
},
"nickname": {
"type": "string",
"description": "Username generated (Robot name)",
},
"public_key": {
"type": "string",
"description": "Armored ASCII PGP public key block",
},
"referral_code": {
"type": "string",
"description": "User's referral code",
},
"token_bits_entropy": {"type": "integer"},
"token_shannon_entropy": {"type": "integer"},
"wants_stealth": {
"type": "boolean",
"default": False,
"description": "Whether the user prefers stealth invoices",
},
"found": {"type": "string", "description": "Welcome back message"},
"active_order_id": {
"type": "integer",
"description": "Active order id if present",
},
"last_order_id": {
"type": "integer",
"description": "Last order id if present",
},
},
},
400: {
"oneOf": [
{
"type": "object",
"properties": {
"active_order_id": {
"type": "string",
"description": "Order id the robot is a maker/taker of",
},
"nickname": {
"type": "string",
"description": "Username (Robot name)",
},
"bad_request": {
"type": "string",
"description": "Reason for the failure",
"default": "You are already logged in as {nickname} and have an active order",
},
},
"description": "Response when you already authenticated and have an order",
},
{
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
]
},
403: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
"default": "Enter a different token",
},
"found": {
"type": "string",
"default": "Bad luck, this nickname is taken",
},
},
},
},
"examples": [
OpenApiExample(
"Successfuly created user",
value={
"token_shannon_entropy": 0.7714559798089662,
"token_bits_entropy": 169.21582985307933,
"nickname": "StackerMan420",
"referral_code": "lfvv4-ppNi1",
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n......\n......",
"encrypted_private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n......\n......",
"wants_stealth": False,
},
status_codes=[201],
),
OpenApiExample(
"Already authenticated and have an order",
value={
"active_order_id": "42069",
"nickname": "StackerMan210",
"bad_request": "You are already logged in as {nickname} and have an active order",
},
status_codes=[400],
),
OpenApiExample(
"When required token entropy not met",
value={"bad_request": "The token does not have enough entropy"},
status_codes=[400],
),
OpenApiExample(
"Invalid PGP public key provided",
value={"bad_request": "Your PGP public key does not seem valid"},
status_codes=[400],
),
],
}
delete = {
"summary": "Delete user",
"description": textwrap.dedent(
"""
Delete a Robot. Deleting a robot is not allowed if the robot has an active order, has had completed trades or was created more than 30 mins ago.
Mainly used on the frontend to "Generate new Robot" without flooding the DB with discarded robots.
"""
),
"responses": {
403: {},
400: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
301: {
"type": "object",
"properties": {
"user_deleted": {
"type": "string",
"default": "User deleted permanently",
},
},
},
},
}
class BookViewSchema:
get = {
"summary": "Get public orders",
"description": "Get public orders in the book.",
"parameters": [
OpenApiParameter(
name="currency",
location=OpenApiParameter.QUERY,
description=(
"The currency id to filter by. Currency IDs can be found [here]"
"(https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/static/assets/currencies.json). "
"Value of `0` means ANY currency"
),
type=int,
),
OpenApiParameter(
name="type",
location=OpenApiParameter.QUERY,
description=(
"Order type to filter by\n"
"- `0` - BUY\n"
"- `1` - SELL\n"
"- `2` - ALL"
),
type=int,
enum=[0, 1, 2],
),
],
}
class InfoViewSchema:
get = {
"summary": "Get info",
"description": textwrap.dedent(
"""
Get general info (overview) about the exchange.
**Info**:
- Current market data
- num. of orders
- book liquidity
- 24h active robots
- 24h non-KYC premium
- 24h volume
- all time volume
- Node info
- lnd version
- node id
- node alias
- network
- Fees
- maker and taker fees
- on-chain swap fees
- Robot (If autheticated)
- nickname
- referral code
- earned rewards
"""
),
}
class RewardViewSchema:
post = {
"summary": "Withdraw reward",
"description": "Withdraw user reward by submitting an invoice",
"responses": {
200: {
"type": "object",
"properties": {
"successful_withdrawal": {"type": "boolean", "default": True}
},
},
400: {
"oneOf": [
{
"type": "object",
"properties": {
"successful_withdrawal": {
"type": "boolean",
"default": False,
},
"bad_invoice": {
"type": "string",
"description": "More context for the reason of the failure",
},
},
},
{
"type": "object",
"properties": {
"successful_withdrawal": {
"type": "boolean",
"default": False,
},
"bad_request": {
"type": "string",
"description": "More context for the reason of the failure",
},
},
},
]
},
},
"examples": [
OpenApiExample(
"User not authenticated",
value={
"bad_request": "Woops! It seems you do not have a robot avatar",
},
status_codes=[400],
),
OpenApiExample(
"When no rewards earned",
value={
"successful_withdrawal": False,
"bad_invoice": "You have not earned rewards",
},
status_codes=[400],
),
OpenApiExample(
"Bad invoice or in case of payment failure",
value={
"successful_withdrawal": False,
"bad_invoice": "Does not look like a valid lightning invoice",
},
status_codes=[400],
),
],
}
class PriceViewSchema:
get = {
"summary": "Get last market prices",
"description": "Get the last market price for each currency. Also, returns some more info about the last trade in each currency.",
"responses": {
200: {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"price": {"type": "integer"},
"volume": {"type": "integer"},
"premium": {"type": "integer"},
"timestamp": {"type": "string", "format": "date-time"},
},
},
},
},
"examples": [
OpenApiExample(
"Truncated example. Real response contains all the currencies",
value={
"<currency symbol>": {
"price": 21948.89,
"volume": 0.01366812,
"premium": 3.5,
"timestamp": "2022-09-13T14:32:40.591774Z",
},
},
status_codes=[200],
)
],
}
class TickViewSchema:
get = {
"summary": "Get market ticks",
"description": "Get all market ticks. Returns a list of all the market ticks since inception.\n"
"CEX price is also recorded for useful insight on the historical premium of Non-KYC BTC. "
"Price is set when taker bond is locked.",
}
class LimitViewSchema:
get = {
"summary": "List order limits",
"description": "Get a list of order limits for every currency pair available.",
"responses": {
200: {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Three letter currency symbol",
},
"price": {"type": "integer"},
"min_amount": {
"type": "integer",
"description": "Minimum amount allowed in an order in the particular currency",
},
"max_amount": {
"type": "integer",
"description": "Maximum amount allowed in an order in the particular currency",
},
"max_bondless_amount": {
"type": "integer",
"description": "Maximum amount allowed in a bondless order",
},
},
},
},
},
"examples": [
OpenApiExample(
"Truncated example. Real response contains all the currencies",
value={
"<currency number>": {
"code": "USD",
"price": "42069.69",
"min_amount": "4.2",
"max_amount": "420.69",
"max_bondless_amount": "10.1",
},
},
status_codes=[200],
)
],
}
class HistoricalViewSchema:
get = {
"summary": "Get historical exchange activity",
"description": "Get historical exchange activity. Currently, it lists each day's total contracts and their volume in BTC since inception.",
"responses": {
200: {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"volume": {
"type": "integer",
"description": "Total Volume traded on that particular date",
},
"num_contracts": {
"type": "number",
"description": "Number of successful trades on that particular date",
},
},
},
},
},
"examples": [
OpenApiExample(
"Truncated example",
value={
"<date>": {
"code": "USD",
"price": "42069.69",
"min_amount": "4.2",
"max_amount": "420.69",
"max_bondless_amount": "10.1",
},
},
status_codes=[200],
)
],
}
class StealthViewSchema:
put = {
"summary": "Update stealth option",
"description": "Update stealth invoice option for the user",
"responses": {
200: StealthSerializer,
400: {
"type": "object",
"properties": {
"bad_request": {
"type": "string",
"description": "Reason for the failure",
},
},
},
},
}

View File

@ -1,5 +1,34 @@
from django.template.defaultfilters import default
from rest_framework import serializers
from .models import MarketTick, Order
from decouple import config
RETRY_TIME = int(config("RETRY_TIME"))
MIN_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))
MAX_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))
class InfoSerializer(serializers.Serializer):
num_public_buy_orders = serializers.IntegerField()
num_public_sell_orders = serializers.IntegerField()
book_liquidity = serializers.IntegerField(help_text='Total amount of BTC in the order book')
active_robots_today = serializers.CharField()
last_day_nonkyc_btc_premium = serializers.FloatField(help_text='Average premium (weighted by volume) of the orders in the last 24h')
last_day_volume = serializers.FloatField(help_text='Total volume in BTC in the last 24h')
lifetime_volume = serializers.FloatField(help_text='Total volume in BTC since exchange\'s inception')
lnd_version = serializers.CharField()
robosats_running_commit_hash = serializers.CharField()
alternative_site = serializers.CharField()
alternative_name = serializers.CharField()
node_alias = serializers.CharField()
node_id = serializers.CharField()
network = serializers.CharField()
maker_fee = serializers.FloatField(help_text='Exchange\'s set maker fee')
taker_fee = serializers.FloatField(help_text='Exchange\'s set taker fee ')
bond_size = serializers.FloatField(help_text='Default bond size (percent)')
current_swap_fee_rate = serializers.FloatField(help_text='Swap fees to perform on-chain transaction (percent)')
nickname = serializers.CharField(help_text='Currenlty logged in Robot name')
referral_code = serializers.CharField(help_text='Logged in users\'s referral code')
earned_rewards = serializers.IntegerField(help_text='Logged in user\'s earned rewards in satoshis')
class ListOrderSerializer(serializers.ModelSerializer):
@ -28,7 +57,421 @@ class ListOrderSerializer(serializers.ModelSerializer):
)
# Only used in oas_schemas
class SummarySerializer(serializers.Serializer):
sent_fiat = serializers.IntegerField(
required=False,
help_text="same as `amount` (only for buyer)"
)
received_sats = serializers.IntegerField(
required=False,
help_text="same as `trade_satoshis` (only for buyer)"
)
is_swap = serializers.BooleanField(
required=False,
help_text="True if the payout was on-chain (only for buyer)"
)
received_onchain_sats = serializers.IntegerField(
required=False,
help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)"
)
mining_fee_sats = serializers.IntegerField(
required=False,
help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)"
)
swap_fee_sats = serializers.IntegerField(
required=False,
help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)"
)
swap_fee_percent = serializers.FloatField(
required=False,
help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`"
)
sent_sats = serializers.IntegerField(
required=False,
help_text="The total sats you sent (only for seller)"
)
received_fiat = serializers.IntegerField(
required=False,
help_text="same as `amount` (only for seller)"
)
trade_fee_sats = serializers.IntegerField(
required=False,
help_text="Exchange fees in sats (Does not include swap fee and miner fee)"
)
# Only used in oas_schemas
class PlatformSummarySerializer(serializers.Serializer):
contract_timestamp = serializers.DateTimeField(
required=False,
help_text="Timestamp of when the contract was finalized (price and sats fixed)"
)
contract_total_time = serializers.FloatField(
required=False,
help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds"
)
routing_fee_sats = serializers.IntegerField(
required=False,
help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout"
)
trade_revenue_sats = serializers.IntegerField(
required=False,
help_text="The sats the exchange earned from the trade"
)
# Only used in oas_schemas
class OrderDetailSerializer(serializers.ModelSerializer):
total_secs_exp = serializers.IntegerField(
required=False,
help_text="Duration of time (in seconds) to expire, according to the current status of order."
"This is duration of time after `created_at` (in seconds) that the order will automatically expire."
"This value changes according to which stage the order is in"
)
penalty = serializers.DateTimeField(
required=False,
help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond"
)
is_maker = serializers.BooleanField(
required=False,
help_text="Whether you are the maker or not"
)
is_taker = serializers.BooleanField(
required=False,
help_text="Whether you are the taker or not"
)
is_participant = serializers.BooleanField(
required=False,
help_text="True if you are either a taker or maker, False otherwise"
)
maker_status = serializers.CharField(
required=False,
help_text="Status of the maker:\n"
"- **'Active'** (seen within last 2 min)\n"
"- **'Seen Recently'** (seen within last 10 min)\n"
"- **'Inactive'** (seen more than 10 min ago)\n\n"
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty"
)
taker_status = serializers.BooleanField(
required=False,
help_text="True if you are either a taker or maker, False otherwise"
)
price_now = serializers.IntegerField(
required=False,
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)"
)
premium = serializers.IntegerField(
required=False,
help_text="Premium over the CEX price at the current time"
)
premium_percentile = serializers.IntegerField(
required=False,
help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book"
)
num_similar_orders = serializers.IntegerField(
required=False,
help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book"
)
tg_enabled = serializers.BooleanField(
required=False,
help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not"
)
tg_token = serializers.CharField(
required=False,
help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications."
)
tg_bot_name = serializers.CharField(
required=False,
help_text="(Only if `is_maker`) The Telegram username of the bot"
)
is_buyer = serializers.BooleanField(
required=False,
help_text="Whether you are a buyer of sats (you will be receiving sats)"
)
is_seller = serializers.BooleanField(
required=False,
help_text="Whether you are a seller of sats or not (you will be sending sats)"
)
maker_nick = serializers.CharField(
required=False,
help_text="Nickname (Robot name) of the maker"
)
taker_nick = serializers.CharField(
required=False,
help_text="Nickname (Robot name) of the taker"
)
status_message = serializers.CharField(
required=False,
help_text="The current status of the order corresponding to the `status`"
)
is_fiat_sent = serializers.BooleanField(
required=False,
help_text="Whether or not the fiat amount is sent by the buyer"
)
is_disputed = serializers.BooleanField(
required=False,
help_text="Whether or not the counterparty raised a dispute"
)
ur_nick = serializers.CharField(
required=False,
help_text="Your Nickname"
)
ur_nick = serializers.CharField(
required=False,
help_text="Your Nick"
)
maker_locked = serializers.BooleanField(
required=False,
help_text="True if maker bond is locked, False otherwise"
)
taker_locked = serializers.BooleanField(
required=False,
help_text="True if taker bond is locked, False otherwise"
)
escrow_locked = serializers.BooleanField(
required=False,
help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised."
)
trade_satoshis = serializers.IntegerField(
required=False,
help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive "
)
bond_invoice = serializers.CharField(
required=False,
help_text="When `status` = `0`, `3`. Bond invoice to be paid"
)
bond_satoshis = serializers.IntegerField(
required=False,
help_text="The bond amount in satoshis"
)
escrow_invoice = serializers.CharField(
required=False,
help_text="For the seller, the escrow invoice to be held by RoboSats"
)
escrow_satoshis = serializers.IntegerField(
required=False,
help_text="The escrow amount in satoshis"
)
invoice_amount = serializers.IntegerField(
required=False,
help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount"
)
swap_allowed = serializers.BooleanField(
required=False,
help_text="Whether on-chain swap is allowed"
)
swap_failure_reason = serializers.CharField(
required=False,
help_text="Reason for why on-chain swap is not available"
)
suggested_mining_fee_rate = serializers.IntegerField(
required=False,
help_text="fee in sats/vbyte for the on-chain swap"
)
swap_fee_rate = serializers.FloatField(
required=False,
help_text="in percentage, the swap fee rate the platform charges"
)
pending_cancel = serializers.BooleanField(
required=False,
help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`"
)
asked_for_cancel = serializers.BooleanField(
required=False,
help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`"
)
statement_submitted = serializers.BooleanField(
required=False,
help_text="True if you have submitted a statement. Available when `status` is `11`"
)
retries = serializers.IntegerField(
required=False,
help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)"
)
next_retry_time = serializers.DateTimeField(
required=False,
help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec"
)
failure_reason = serializers.CharField(
required=False,
help_text="The reason the payout failed"
)
invoice_expired = serializers.BooleanField(
required=False,
help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed"
)
trade_fee_percent = serializers.IntegerField(
required=False,
help_text="The fee for the trade (fees differ for maker and taker)"
)
bond_size_sats = serializers.IntegerField(
required=False,
help_text="The size of the bond in sats"
)
bond_size_percent = serializers.IntegerField(
required=False,
help_text="same as `bond_size`"
)
maker_summary = SummarySerializer(required=False)
taker_summary = SummarySerializer(required=False)
platform_summary = PlatformSummarySerializer(required=True)
expiry_message = serializers.CharField(
required=False,
help_text="The reason the order expired (message associated with the `expiry_reason`)"
)
num_satoshis = serializers.IntegerField(
required=False,
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`"
)
sent_satoshis = serializers.IntegerField(
required=False,
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`"
)
txid = serializers.CharField(
required=False,
help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`"
)
network = serializers.CharField(
required=False,
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`"
)
class Meta:
model = Order
fields = (
"id",
"status",
"created_at",
"expires_at",
"type",
"currency",
"amount",
"has_range",
"min_amount",
"max_amount",
"payment_method",
"is_explicit",
"premium",
"satoshis",
"bondless_taker",
"maker",
"taker",
"escrow_duration",
"total_secs_exp",
"penalty",
"is_maker",
"is_taker",
"is_participant",
"maker_status",
"taker_status",
"price_now",
"premium",
"premium_percentile",
"num_similar_orders",
"tg_enabled",
"tg_token",
"tg_bot_name",
"is_buyer",
"is_seller",
"maker_nick",
"taker_nick",
"status_message",
"is_fiat_sent",
"is_disputed",
"ur_nick",
"ur_nick",
"maker_locked",
"taker_locked",
"escrow_locked",
"trade_satoshis",
"bond_invoice",
"bond_satoshis",
"escrow_invoice",
"escrow_satoshis",
"invoice_amount",
"swap_allowed",
'swap_failure_reason',
"suggested_mining_fee_rate",
"swap_fee_rate",
"pending_cancel",
"asked_for_cancel",
"statement_submitted",
"retries",
"next_retry_time",
"failure_reason",
"invoice_expired",
"public_duration",
"bond_size",
"trade_fee_percent",
"bond_size_sats",
"bond_size_percent",
"maker_summary",
"taker_summary",
"platform_summary",
"expiry_reason",
"expiry_message",
"num_satoshis",
"sent_satoshis",
"txid",
"network",
)
class OrderPublicSerializer(serializers.ModelSerializer):
maker_nick = serializers.CharField(required=False)
maker_status = serializers.CharField(help_text='Status of the nick - "Active" or "Inactive"', required=False)
price = serializers.IntegerField(help_text="Price in order's fiat currency", required=False)
class Meta:
model = Order
fields = (
"id",
"created_at",
"expires_at",
"type",
"currency",
"amount",
"has_range",
"min_amount",
"max_amount",
"payment_method",
"is_explicit",
"premium",
"satoshis",
"bondless_taker",
"maker",
"maker_nick",
"maker_status",
"price",
"escrow_duration",
)
class MakeOrderSerializer(serializers.ModelSerializer):
currency = serializers.IntegerField(
required=True,
help_text="Currency id. See [here](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/static/assets/currencies.json) for a list of all IDs",
)
payment_method = serializers.CharField(
max_length=70,
default="not specified",
required=False,
help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo."
)
is_explicit = serializers.BooleanField(
default=False,
help_text='Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified'
)
has_range = serializers.BooleanField(
default=False,
help_text='Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**',
)
bondless_taker = serializers.BooleanField(
default=False,
help_text='Whether bondless takers are allowed for this order or not',
)
class Meta:
model = Order
@ -111,25 +554,30 @@ class UserGenSerializer(serializers.Serializer):
allow_null=True,
allow_blank=True,
required=False,
default=None)
default=None,
help_text="Referal code")
counts = serializers.ListField(child=serializers.IntegerField(),
allow_null=True,
required=False,
default=None)
default=None,
help_text="Counts of the unique characters in the token")
length = serializers.IntegerField(allow_null=True,
default=None,
required=False,
min_value=1)
min_value=1,
help_text="Length of the token")
unique_values = serializers.IntegerField(allow_null=True,
default=None,
required=False,
min_value=1)
min_value=1,
help_text="Number of unique values in the token")
class ClaimRewardSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000,
allow_null=True,
allow_blank=True,
default=None)
default=None,
help_text="A valid LN invoice with the reward amount to withdraw")
class PriceSerializer(serializers.Serializer):
pass

View File

@ -1,7 +1,10 @@
from django.urls import path
from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView, LimitView, HistoricalView, TickView, StealthView
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [
path('schema/', SpectacularAPIView.as_view(), name='schema'),
path('', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
path("make/", MakerView.as_view()),
path("order/",OrderView.as_view({
"get": "get",

View File

@ -1,17 +1,22 @@
import os
from re import T
from django.db.models import Sum, Q
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema
from rest_framework import status, viewsets
from rest_framework.exceptions import bad_request
from rest_framework.generics import CreateAPIView, ListAPIView, UpdateAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
import textwrap
from django.contrib.auth import authenticate, login, logout
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from api.oas_schemas import BookViewSchema, HistoricalViewSchema, InfoViewSchema, LimitViewSchema, MakerViewSchema, OrderViewSchema, PriceViewSchema, RewardViewSchema, StealthViewSchema, TickViewSchema, UserViewSchema
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer, TickSerializer, StealthSerializer
from api.serializers import InfoSerializer, ListOrderSerializer, MakeOrderSerializer, OrderPublicSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer, TickSerializer, StealthSerializer
from api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile
from control.models import AccountingDay, BalanceLog
from api.logics import Logics
@ -46,6 +51,7 @@ avatar_path.mkdir(parents=True, exist_ok=True)
class MakerView(CreateAPIView):
serializer_class = MakeOrderSerializer
@extend_schema(**MakerViewSchema.post)
def post(self, request):
serializer = self.serializer_class(data=request.data)
@ -97,6 +103,8 @@ class MakerView(CreateAPIView):
if bondless_taker == None: bondless_taker = False
if has_range == None: has_range = False
# TODO add a check - if `is_explicit` is true then `satoshis` need to be specified
# An order can either have an amount or a range (min_amount and max_amount)
if has_range:
amount = None
@ -122,6 +130,7 @@ class MakerView(CreateAPIView):
status.HTTP_400_BAD_REQUEST,
)
# Creates a new order
order = Order(
type=type,
@ -158,6 +167,7 @@ class OrderView(viewsets.ViewSet):
serializer_class = UpdateOrderSerializer
lookup_url_kwarg = "order_id"
@extend_schema(**OrderViewSchema.get)
def get(self, request, format=None):
"""
Full trade pipeline takes place while looking/refreshing the order page.
@ -419,6 +429,7 @@ class OrderView(viewsets.ViewSet):
return Response(data, status.HTTP_200_OK)
@extend_schema(**OrderViewSchema.take_update_confirm_dispute_cancel)
def take_update_confirm_dispute_cancel(self, request, format=None):
"""
Here takes place all of the updates to the order object.
@ -548,6 +559,7 @@ class OrderView(viewsets.ViewSet):
return self.get(request)
class UserView(APIView):
NickGen = NickGenerator(lang="English",
use_adv=False,
@ -557,6 +569,7 @@ class UserView(APIView):
serializer_class = UserGenSerializer
def post(self, request, format=None):
"""
Get a new user derived from a high entropy token
@ -711,6 +724,7 @@ class UserView(APIView):
context["bad_request"] = "Enter a different token"
return Response(context, status.HTTP_403_FORBIDDEN)
@extend_schema(**UserViewSchema.delete)
def delete(self, request):
"""Pressing "give me another" deletes the logged in user"""
user = request.user
@ -750,9 +764,10 @@ class UserView(APIView):
class BookView(ListAPIView):
serializer_class = ListOrderSerializer
serializer_class = OrderPublicSerializer
queryset = Order.objects.filter(status=Order.Status.PUB)
@extend_schema(**BookViewSchema.get)
def get(self, request, format=None):
currency = request.GET.get("currency", 0)
type = request.GET.get("type", 2)
@ -794,8 +809,12 @@ class BookView(ListAPIView):
return Response(book_data, status=status.HTTP_200_OK)
class InfoView(ListAPIView):
serializer_class = InfoSerializer
@extend_schema(**InfoViewSchema.get)
def get(self, request):
context = {}
@ -872,6 +891,7 @@ class InfoView(ListAPIView):
class RewardView(CreateAPIView):
serializer_class = ClaimRewardSerializer
@extend_schema(**RewardViewSchema.post)
def post(self, request):
serializer = self.serializer_class(data=request.data)
@ -897,10 +917,12 @@ class RewardView(CreateAPIView):
return Response({"successful_withdrawal": True}, status.HTTP_200_OK)
class PriceView(ListAPIView):
serializer_class = PriceSerializer
@extend_schema(**PriceViewSchema.get)
def get(self, request):
payload = {}
@ -921,17 +943,21 @@ class PriceView(ListAPIView):
return Response(payload, status.HTTP_200_OK)
class TickView(ListAPIView):
queryset = MarketTick.objects.all()
serializer_class = TickSerializer
@extend_schema(**TickViewSchema.get)
def get(self, request):
data = self.serializer_class(self.queryset.all(), many=True, read_only=True).data
return Response(data, status=status.HTTP_200_OK)
class LimitView(ListAPIView):
@extend_schema(**LimitViewSchema.get)
def get(self, request):
# Trade limits as BTC
@ -955,7 +981,10 @@ class LimitView(ListAPIView):
return Response(payload, status.HTTP_200_OK)
class HistoricalView(ListAPIView):
@extend_schema(**HistoricalViewSchema.get)
def get(self, request):
payload = {}
queryset = AccountingDay.objects.all().order_by('day')
@ -968,10 +997,11 @@ class HistoricalView(ListAPIView):
return Response(payload, status.HTTP_200_OK)
class StealthView(UpdateAPIView):
serializer_class = StealthSerializer
@extend_schema(**StealthViewSchema.put)
def put(self, request):
serializer = self.serializer_class(data=request.data)

View File

@ -27,3 +27,5 @@ django-import-export==2.7.1
requests[socks]
python-gnupg==0.4.9
daphne==3.0.2
drf-spectacular==0.24.0
drf-spectacular-sidecar==2022.9.1

View File

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
import os
from pathlib import Path
import textwrap
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -89,7 +90,47 @@ INSTALLED_APPS = [
"chat",
"control",
"frontend.apps.FrontendConfig",
"drf_spectacular",
"drf_spectacular_sidecar", # required for Django collectstatic discovery
]
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'RoboSats REST API v0',
'DESCRIPTION': textwrap.dedent(
"""
REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange
<p style='background-color:#fff0f0;padding:16px;border-radius:6px;border:2px solid #ffd3d3'>
<span style='color:#f31f1f;font-weight:bold'>Note:</span>
The RoboSats REST API is on v0, which in other words, is beta.
We recommend that if you don't have time to actively maintain
your project, do not build it with v0 of the API. A refactored, simpler
and more stable version - v1 will be released soon.
</p>
"""
),
'VERSION': '0.1.0',
'SERVE_INCLUDE_SCHEMA': False,
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_UI_SETTINGS': {
'expandResponses': '200,201',
},
'EXTENSIONS_INFO': {
'x-logo': {
'url': 'https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png',
'backgroundColor': '#FFFFFF',
'altText': 'RoboSats logo'
}
},
'REDOC_DIST': 'SIDECAR',
}
from .celery.conf import *
MIDDLEWARE = [