diff --git a/.env-sample b/.env-sample index 84dce51b..49249da7 100644 --- a/.env-sample +++ b/.env-sample @@ -30,7 +30,7 @@ POSTGRES_HOST='127.0.0.1' POSTGRES_PORT='5432' # Tor proxy for remote calls (e.g. fetching prices or sending Telegram messages) -USE_TOR='True' +USE_TOR=True TOR_PROXY='127.0.0.1:9050' # Auto unlock LND password. Only used in development docker-compose environment. @@ -166,4 +166,4 @@ MINIMUM_TARGET_CONF = 24 SLASHED_BOND_REWARD_SPLIT = 0.5 # Username for HTLCs escrows -ESCROW_USERNAME = 'admin' +ESCROW_USERNAME = 'admin' \ No newline at end of file diff --git a/api/models/order.py b/api/models/order.py index 14762134..a8964997 100644 --- a/api/models/order.py +++ b/api/models/order.py @@ -1,3 +1,4 @@ +# We use custom seeded UUID generation during testing import uuid from decouple import config @@ -9,6 +10,19 @@ from django.db.models.signals import pre_delete from django.dispatch import receiver from django.utils import timezone +if config("COORDINATOR_TESTING", cast=bool, default=False): + import random + import string + + random.seed(1) + chars = string.ascii_lowercase + string.digits + + def custom_uuid(): + return uuid.uuid5(uuid.NAMESPACE_DNS, "".join(random.choices(chars, k=20))) + +else: + custom_uuid = uuid.uuid4 + class Order(models.Model): class Types(models.IntegerChoices): @@ -44,7 +58,7 @@ class Order(models.Model): NESINV = 4, "Neither escrow locked or invoice submitted" # order info - reference = models.UUIDField(default=uuid.uuid4, editable=False) + reference = models.UUIDField(default=custom_uuid, editable=False) status = models.PositiveSmallIntegerField( choices=Status.choices, null=False, default=Status.WFB ) diff --git a/api/oas_schemas.py b/api/oas_schemas.py index ed59bcd5..569e9658 100644 --- a/api/oas_schemas.py +++ b/api/oas_schemas.py @@ -5,6 +5,7 @@ from django.conf import settings from drf_spectacular.utils import OpenApiExample, OpenApiParameter from api.serializers import ( + InfoSerializer, ListOrderSerializer, OrderDetailSerializer, StealthSerializer, @@ -322,17 +323,7 @@ class OrderViewSchema: ), ], "responses": { - 200: { - "type": "object", - "additionalProperties": { - "oneOf": [ - {"type": "str"}, - {"type": "number"}, - {"type": "object"}, - {"type": "boolean"}, - ], - }, - }, + 200: OrderDetailSerializer, 400: { "type": "object", "properties": { @@ -474,6 +465,16 @@ class RobotViewSchema: "type": "integer", "description": "Last order id if present", }, + "earned_rewards": { + "type": "integer", + "description": "Satoshis available to be claimed", + }, + "last_login": { + "type": "string", + "format": "date-time", + "nullable": True, + "description": "Last time the coordinator saw this robot", + }, }, }, }, @@ -517,6 +518,9 @@ class InfoViewSchema: - on-chain swap fees """ ), + "responses": { + 200: InfoSerializer, + }, } diff --git a/api/serializers.py b/api/serializers.py index d2b0db2a..7da7c2bc 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -6,13 +6,19 @@ from .models import MarketTick, Order RETRY_TIME = int(config("RETRY_TIME")) +class VersionSerializer(serializers.Serializer): + major = serializers.IntegerField() + minor = serializers.IntegerField() + patch = serializers.IntegerField() + + 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() + active_robots_today = serializers.IntegerField() last_day_nonkyc_btc_premium = serializers.FloatField( help_text="Average premium (weighted by volume) of the orders in the last 24h" ) @@ -23,6 +29,7 @@ class InfoSerializer(serializers.Serializer): help_text="Total volume in BTC since exchange's inception" ) lnd_version = serializers.CharField() + cln_version = serializers.CharField() robosats_running_commit_hash = serializers.CharField() alternative_site = serializers.CharField() alternative_name = serializers.CharField() @@ -35,6 +42,17 @@ class InfoSerializer(serializers.Serializer): current_swap_fee_rate = serializers.FloatField( help_text="Swap fees to perform on-chain transaction (percent)" ) + version = VersionSerializer() + notice_severity = serializers.ChoiceField( + choices=[ + ("none", "none"), + ("warning", "warning"), + ("success", "success"), + ("error", "error"), + ("info", "info"), + ] + ) + notice_message = serializers.CharField() class ListOrderSerializer(serializers.ModelSerializer): @@ -60,7 +78,7 @@ class ListOrderSerializer(serializers.ModelSerializer): "escrow_duration", "bond_size", "latitude", - "longitude" + "longitude", ) @@ -160,10 +178,13 @@ class OrderDetailSerializer(serializers.ModelSerializer): 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( + premium = serializers.CharField( + required=False, help_text="Premium over the CEX price set by the maker" + ) + premium_now = serializers.FloatField( required=False, help_text="Premium over the CEX price at the current time" ) - premium_percentile = serializers.IntegerField( + premium_percentile = serializers.FloatField( 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", ) @@ -253,11 +274,11 @@ class OrderDetailSerializer(serializers.ModelSerializer): required=False, help_text="in percentage, the swap fee rate the platform charges", ) - latitude = serializers.FloatField( + latitude = serializers.CharField( required=False, help_text="Latitude of the order for F2F payments", ) - longitude = serializers.FloatField( + longitude = serializers.CharField( required=False, help_text="Longitude of the order for F2F payments", ) @@ -300,7 +321,11 @@ class OrderDetailSerializer(serializers.ModelSerializer): ) maker_summary = SummarySerializer(required=False) taker_summary = SummarySerializer(required=False) - platform_summary = PlatformSummarySerializer(required=True) + satoshis_now = serializers.IntegerField( + required=False, + help_text="Maximum size of the order right now in Satoshis", + ) + platform_summary = PlatformSummarySerializer(required=False) expiry_message = serializers.CharField( required=False, help_text="The reason the order expired (message associated with the `expiry_reason`)", @@ -338,7 +363,9 @@ class OrderDetailSerializer(serializers.ModelSerializer): "payment_method", "is_explicit", "premium", + "premium_now", "satoshis", + "satoshis_now", "maker", "taker", "escrow_duration", @@ -350,7 +377,6 @@ class OrderDetailSerializer(serializers.ModelSerializer): "maker_status", "taker_status", "price_now", - "premium", "premium_percentile", "num_similar_orders", "tg_enabled", @@ -441,7 +467,7 @@ class OrderPublicSerializer(serializers.ModelSerializer): "satoshis_now", "bond_size", "latitude", - "longitude" + "longitude", ) @@ -482,7 +508,7 @@ class MakeOrderSerializer(serializers.ModelSerializer): "escrow_duration", "bond_size", "latitude", - "longitude" + "longitude", ) diff --git a/api/urls.py b/api/urls.py index 407dc2d0..7d8b19c6 100644 --- a/api/urls.py +++ b/api/urls.py @@ -20,19 +20,20 @@ from .views import ( urlpatterns = [ path("schema/", SpectacularAPIView.as_view(), name="schema"), path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), - path("make/", MakerView.as_view()), + path("make/", MakerView.as_view(), name="make"), path( "order/", OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}), + name="order", ), - path("robot/", RobotView.as_view()), - path("book/", BookView.as_view()), - path("info/", InfoView.as_view()), - path("price/", PriceView.as_view()), - path("limits/", LimitView.as_view()), - path("reward/", RewardView.as_view()), - path("historical/", HistoricalView.as_view()), - path("ticks/", TickView.as_view()), - path("stealth/", StealthView.as_view()), - path("chat/", ChatView.as_view({"get": "get", "post": "post"})), + path("robot/", RobotView.as_view(), name="robot"), + path("book/", BookView.as_view(), name="book"), + path("info/", InfoView.as_view({"get": "get"}), name="info"), + path("price/", PriceView.as_view(), name="price"), + path("limits/", LimitView.as_view(), name="limits"), + path("reward/", RewardView.as_view(), name="reward"), + path("historical/", HistoricalView.as_view(), name="historical"), + path("ticks/", TickView.as_view(), name="ticks"), + path("stealth/", StealthView.as_view(), name="stealth"), + path("chat/", ChatView.as_view({"get": "get", "post": "post"}), name="chat"), ] diff --git a/api/views.py b/api/views.py index 2239d18a..ae4bf9ac 100644 --- a/api/views.py +++ b/api/views.py @@ -724,7 +724,7 @@ class BookView(ListAPIView): return Response(book_data, status=status.HTTP_200_OK) -class InfoView(ListAPIView): +class InfoView(viewsets.ViewSet): serializer_class = InfoSerializer @extend_schema(**InfoViewSchema.get) diff --git a/docs/assets/schemas/api-v0.5.3.yaml b/docs/assets/schemas/api-v0.5.3.yaml index 376333b4..51d53a67 100644 --- a/docs/assets/schemas/api-v0.5.3.yaml +++ b/docs/assets/schemas/api-v0.5.3.yaml @@ -140,7 +140,7 @@ paths: description: '' /api/info/: get: - operationId: info_list + operationId: info_retrieve description: |2 Get general info (overview) about the exchange. @@ -172,9 +172,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Info' + $ref: '#/components/schemas/Info' description: '' /api/limits/: get: @@ -563,13 +561,7 @@ paths: content: application/json: schema: - type: object - additionalProperties: - oneOf: - - type: str - - type: number - - type: object - - type: boolean + $ref: '#/components/schemas/OrderDetail' description: '' '400': content: @@ -1103,7 +1095,7 @@ components: type: integer description: Total amount of BTC in the order book active_robots_today: - type: string + type: integer last_day_nonkyc_btc_premium: type: number format: double @@ -1119,6 +1111,8 @@ components: description: Total volume in BTC since exchange's inception lnd_version: type: string + cln_version: + type: string robosats_running_commit_hash: type: string alternative_site: @@ -1147,12 +1141,19 @@ components: type: number format: double description: Swap fees to perform on-chain transaction (percent) + version: + $ref: '#/components/schemas/Version' + notice_severity: + $ref: '#/components/schemas/NoticeSeverityEnum' + notice_message: + type: string required: - active_robots_today - alternative_name - alternative_site - bond_size - book_liquidity + - cln_version - current_swap_fee_rate - last_day_nonkyc_btc_premium - last_day_volume @@ -1162,10 +1163,13 @@ components: - network - node_alias - node_id + - notice_message + - notice_severity - num_public_buy_orders - num_public_sell_orders - robosats_running_commit_hash - taker_fee + - version ListOrder: type: object properties: @@ -1355,6 +1359,20 @@ components: required: - currency - id + NoticeSeverityEnum: + enum: + - none + - warning + - success + - error + - info + type: string + description: |- + * `none` - none + * `warning` - warning + * `success` - success + * `error` - error + * `info` - info NullEnum: enum: - null @@ -1952,6 +1970,19 @@ components: nullable: true required: - action + Version: + type: object + properties: + major: + type: integer + minor: + type: integer + patch: + type: integer + required: + - major + - minor + - patch securitySchemes: tokenAuth: type: apiKey diff --git a/robosats/settings.py b/robosats/settings.py index 492ac9cb..e497364a 100644 --- a/robosats/settings.py +++ b/robosats/settings.py @@ -275,9 +275,9 @@ MAX_PUBLIC_ORDER_DURATION = 24 MIN_PUBLIC_ORDER_DURATION = 0.166 # Bond size as percentage (%) -DEFAULT_BOND_SIZE = 3 -MIN_BOND_SIZE = 2 -MAX_BOND_SIZE = 15 +DEFAULT_BOND_SIZE = float(3) +MIN_BOND_SIZE = float(2) +MAX_BOND_SIZE = float(15) # Default time to provide a valid invoice and the trade escrow MINUTES INVOICE_AND_ESCROW_DURATION = 180 diff --git a/tests/api_specs.yaml b/tests/api_specs.yaml new file mode 100644 index 00000000..b2768cd1 --- /dev/null +++ b/tests/api_specs.yaml @@ -0,0 +1,2004 @@ +openapi: 3.0.3 +info: + title: RoboSats REST API + version: 0.5.3 + 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 + description: |2+ + + REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange + +

+ Note: + 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™. +

+ +paths: + /api/book/: + get: + operationId: book_list + description: Get public orders in the book. + summary: Get public orders + parameters: + - in: query + name: currency + schema: + type: integer + description: The currency id to filter by. Currency IDs can be found [here](https://github.com/RoboSats/robosats/blob/main/frontend/static/assets/currencies.json). + Value of `0` means ANY currency + - in: query + name: type + schema: + type: integer + enum: + - 0 + - 1 + - 2 + description: |- + Order type to filter by + - `0` - BUY + - `1` - SELL + - `2` - ALL + tags: + - book + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OrderPublic' + description: '' + /api/chat/: + get: + operationId: chat_retrieve + description: Returns chat messages for an order with an index higher than `offset`. + tags: + - chat + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PostMessage' + description: '' + post: + operationId: chat_create + description: Adds one new message to the chatroom. + tags: + - chat + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PostMessage' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PostMessage' + multipart/form-data: + schema: + $ref: '#/components/schemas/PostMessage' + required: true + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PostMessage' + description: '' + /api/historical/: + get: + operationId: historical_list + description: Get historical exchange activity. Currently, it lists each day's + total contracts and their volume in BTC since inception. + summary: Get historical exchange activity + tags: + - historical + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + 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: + TruncatedExample: + value: + - : + code: USD + price: '42069.69' + min_amount: '4.2' + max_amount: '420.69' + summary: Truncated example + description: '' + /api/info/: + get: + operationId: info_retrieve + description: |2 + + 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 + summary: Get info + tags: + - info + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Info' + description: '' + /api/limits/: + get: + operationId: limits_list + description: Get a list of order limits for every currency pair available. + summary: List order limits + tags: + - limits + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + 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 + examples: + TruncatedExample.RealResponseContainsAllTheCurrencies: + value: + - : + code: USD + price: '42069.69' + min_amount: '4.2' + max_amount: '420.69' + summary: Truncated example. Real response contains all the currencies + description: '' + /api/make/: + post: + operationId: make_create + description: |2 + + Create a new order as a maker. + + + Default values for the following fields if not specified: + - `public_duration` - **24** + - `escrow_duration` - **180** + - `bond_size` - **3.0** + - `has_range` - **false** + - `premium` - **0** + summary: Create a maker order + tags: + - make + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MakeOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/MakeOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/MakeOrder' + required: true + security: + - tokenAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/ListOrder' + description: '' + '400': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + description: '' + '409': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + description: '' + /api/order/: + get: + operationId: order_retrieve + description: |2+ + + 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` + - `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 + + summary: Get order details + parameters: + - in: query + name: order_id + schema: + type: integer + required: true + tags: + - order + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OrderDetail' + description: '' + '400': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + examples: + OrderCancelled: + value: + bad_request: This order has been cancelled collaborativelly + summary: Order cancelled + WhenTheOrderIsNotPublicAndYouNeitherTheTakerNorMaker: + value: + bad_request: This order is not available + summary: When the order is not public and you neither the taker + nor maker + WhenMakerBondExpires(asMaker): + value: + bad_request: Invoice expired. You did not confirm publishing the + order in time. Make a new order. + summary: When maker bond expires (as maker) + WhenRobosatsNodeIsDown: + value: + bad_request: The Lightning Network Daemon (LND) is down. Write + in the Telegram group to make sure the staff is aware. + summary: When Robosats node is down + description: '' + '403': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + default: This order is not available + description: '' + '404': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + default: Invalid order Id + description: '' + post: + operationId: order_create + description: |2+ + + Update an order + + `action` field is required and determines what is to be done. Below + is an explanation 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 as cleartext PGP message signed with the robot key. Make sure to perform this action only when + both the bonds are locked. i.e The status of your order is + at least `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 received on-chain. Only valid if there is an address in the body as + cleartext PGP message signed with the robot key. 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` - Successful 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 returned your bond. This may change in + the future to prevent DDoSing the LN node and you won't be + returned 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 their 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 + receiving 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 received 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 performing this action. Check that your fiat payment + method was successful in receiving 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). + - `undo_confirm` + - This action will undo the fiat_sent confirmation by the buyer + it is allowed only once the fiat is confirmed as sent and can + enable the collaborative cancellation option if an off-robosats + payment cannot be completed or is blocked. + - `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). `statement` must be sent in the + request body and should be a string. 100 chars < length of + `statement` < 5000 chars. You need to describe the reason for + raising a dispute. The `(m|t)aker_statement` field is set + respectively. Only when both parties have submitted their + dispute statement, the order status changes to `16` (Waiting + for dispute resolution) + - `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 + + summary: Update order + parameters: + - in: query + name: order_id + schema: + type: integer + required: true + tags: + - order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateOrder' + examples: + UserNotAuthenticated: + value: + bad_request: Woops! It seems you do not have a robot avatar + summary: User not authenticated + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UpdateOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/UpdateOrder' + required: true + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OrderDetail' + description: '' + '400': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + examples: + UserNotAuthenticated: + value: + bad_request: Woops! It seems you do not have a robot avatar + summary: User not authenticated + description: '' + /api/price/: + get: + operationId: price_list + description: Get the last market price for each currency. Also, returns some + more info about the last trade in each currency. + summary: Get last market prices + tags: + - price + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + type: object + additionalProperties: + type: object + properties: + price: + type: integer + volume: + type: integer + premium: + type: integer + timestamp: + type: string + format: date-time + examples: + TruncatedExample.RealResponseContainsAllTheCurrencies: + value: + - : + price: 21948.89 + volume: 0.01366812 + premium: 3.5 + timestamp: '2022-09-13T14:32:40.591774Z' + summary: Truncated example. Real response contains all the currencies + description: '' + /api/reward/: + post: + operationId: reward_create + description: Withdraw user reward by submitting an invoice. The invoice must + be send as cleartext PGP message signed with the robot key + summary: Withdraw reward + tags: + - reward + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ClaimReward' + examples: + UserNotAuthenticated: + value: + bad_request: Woops! It seems you do not have a robot avatar + summary: User not authenticated + WhenNoRewardsEarned: + value: + successful_withdrawal: false + bad_invoice: You have not earned rewards + summary: When no rewards earned + BadInvoiceOrInCaseOfPaymentFailure: + value: + successful_withdrawal: false + bad_invoice: Does not look like a valid lightning invoice + summary: Bad invoice or in case of payment failure + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ClaimReward' + multipart/form-data: + schema: + $ref: '#/components/schemas/ClaimReward' + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + type: object + properties: + successful_withdrawal: + type: boolean + default: true + description: '' + '400': + content: + application/json: + schema: + 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: + UserNotAuthenticated: + value: + bad_request: Woops! It seems you do not have a robot avatar + summary: User not authenticated + WhenNoRewardsEarned: + value: + successful_withdrawal: false + bad_invoice: You have not earned rewards + summary: When no rewards earned + BadInvoiceOrInCaseOfPaymentFailure: + value: + successful_withdrawal: false + bad_invoice: Does not look like a valid lightning invoice + summary: Bad invoice or in case of payment failure + description: '' + /api/robot/: + get: + operationId: robot_retrieve + description: |2+ + + Get robot info 🤖 + + An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be + returned the information about the state of a robot. + + 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 receives the hash of the + token, it is responsibility of the client to create a strong token. Check + [here](https://github.com/RoboSats/robosats/blob/main/frontend/src/utils/token.js) + to see how the Javascript client creates a random strong token and how it validates entropy is optimal for tokens + created by the user at will. + + `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 + ``` + + and it's private key can be exported in ascii armored format with: + + ```shell + gpg --export-secret-keys --armor + ``` + + summary: Get robot info + tags: + - robot + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + 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 + wants_stealth: + type: boolean + default: false + description: Whether the user prefers stealth invoices + found: + type: boolean + description: Robot had been created in the past. Only if the robot + was created +5 mins ago. + tg_enabled: + type: boolean + description: The robot has telegram notifications enabled + tg_token: + type: string + description: Token to enable telegram with /start + tg_bot_name: + type: string + description: Name of the coordinator's telegram bot + active_order_id: + type: integer + description: Active order id if present + last_order_id: + type: integer + description: Last order id if present + earned_rewards: + type: integer + description: Satoshis available to be claimed + last_login: + type: string + format: date-time + nullable: true + description: Last time the coordinator saw this robot + examples: + SuccessfullyRetrievedRobot: + value: + nickname: SatoshiNakamoto21 + public_key: |- + -----BEGIN PGP PUBLIC KEY BLOCK----- + + ...... + ...... + encrypted_private_key: |- + -----BEGIN PGP PRIVATE KEY BLOCK----- + + ...... + ...... + wants_stealth: true + summary: Successfully retrieved robot + description: '' + /api/stealth/: + post: + operationId: stealth_create + description: Update stealth invoice option for the user + summary: Update stealth option + tags: + - stealth + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Stealth' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Stealth' + multipart/form-data: + schema: + $ref: '#/components/schemas/Stealth' + required: true + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Stealth' + description: '' + '400': + content: + application/json: + schema: + type: object + properties: + bad_request: + type: string + description: Reason for the failure + description: '' + /api/ticks/: + get: + operationId: ticks_list + description: |- + Get all market ticks. Returns a list of all the market ticks since inception. + CEX price is also recorded for useful insight on the historical premium of Non-KYC BTC. Price is set when taker bond is locked. + summary: Get market ticks + parameters: + - in: query + name: end + schema: + type: string + description: End date formatted as DD-MM-YYYY + - in: query + name: start + schema: + type: string + description: Start date formatted as DD-MM-YYYY + tags: + - ticks + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Tick' + description: '' +components: + schemas: + ActionEnum: + enum: + - pause + - take + - update_invoice + - update_address + - submit_statement + - dispute + - cancel + - confirm + - undo_confirm + - rate_platform + type: string + description: |- + * `pause` - pause + * `take` - take + * `update_invoice` - update_invoice + * `update_address` - update_address + * `submit_statement` - submit_statement + * `dispute` - dispute + * `cancel` - cancel + * `confirm` - confirm + * `undo_confirm` - undo_confirm + * `rate_platform` - rate_platform + BlankEnum: + enum: + - '' + ClaimReward: + type: object + properties: + invoice: + type: string + nullable: true + description: A valid LN invoice with the reward amount to withdraw + maxLength: 2000 + CurrencyEnum: + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + - 23 + - 24 + - 25 + - 26 + - 27 + - 28 + - 29 + - 30 + - 31 + - 32 + - 33 + - 34 + - 35 + - 36 + - 37 + - 38 + - 39 + - 40 + - 41 + - 42 + - 43 + - 44 + - 45 + - 46 + - 47 + - 48 + - 49 + - 50 + - 51 + - 52 + - 53 + - 54 + - 55 + - 56 + - 57 + - 58 + - 59 + - 60 + - 61 + - 62 + - 63 + - 64 + - 65 + - 66 + - 67 + - 68 + - 69 + - 70 + - 71 + - 72 + - 73 + - 74 + - 75 + - 300 + - 1000 + type: integer + description: |- + * `1` - USD + * `2` - EUR + * `3` - JPY + * `4` - GBP + * `5` - AUD + * `6` - CAD + * `7` - CHF + * `8` - CNY + * `9` - HKD + * `10` - NZD + * `11` - SEK + * `12` - KRW + * `13` - SGD + * `14` - NOK + * `15` - MXN + * `16` - BYN + * `17` - RUB + * `18` - ZAR + * `19` - TRY + * `20` - BRL + * `21` - CLP + * `22` - CZK + * `23` - DKK + * `24` - HRK + * `25` - HUF + * `26` - INR + * `27` - ISK + * `28` - PLN + * `29` - RON + * `30` - ARS + * `31` - VES + * `32` - COP + * `33` - PEN + * `34` - UYU + * `35` - PYG + * `36` - BOB + * `37` - IDR + * `38` - ANG + * `39` - CRC + * `40` - CUP + * `41` - DOP + * `42` - GHS + * `43` - GTQ + * `44` - ILS + * `45` - JMD + * `46` - KES + * `47` - KZT + * `48` - MYR + * `49` - NAD + * `50` - NGN + * `51` - AZN + * `52` - PAB + * `53` - PHP + * `54` - PKR + * `55` - QAR + * `56` - SAR + * `57` - THB + * `58` - TTD + * `59` - VND + * `60` - XOF + * `61` - TWD + * `62` - TZS + * `63` - XAF + * `64` - UAH + * `65` - EGP + * `66` - LKR + * `67` - MAD + * `68` - AED + * `69` - TND + * `70` - ETB + * `71` - GEL + * `72` - UGX + * `73` - RSD + * `74` - IRT + * `75` - BDT + * `300` - XAU + * `1000` - BTC + ExpiryReasonEnum: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + type: integer + description: |- + * `0` - Expired not taken + * `1` - Maker bond not locked + * `2` - Escrow not locked + * `3` - Invoice not submitted + * `4` - Neither escrow locked or invoice submitted + Info: + type: object + properties: + num_public_buy_orders: + type: integer + num_public_sell_orders: + type: integer + book_liquidity: + type: integer + description: Total amount of BTC in the order book + active_robots_today: + type: integer + last_day_nonkyc_btc_premium: + type: number + format: double + description: Average premium (weighted by volume) of the orders in the last + 24h + last_day_volume: + type: number + format: double + description: Total volume in BTC in the last 24h + lifetime_volume: + type: number + format: double + description: Total volume in BTC since exchange's inception + lnd_version: + type: string + cln_version: + type: string + robosats_running_commit_hash: + type: string + alternative_site: + type: string + alternative_name: + type: string + node_alias: + type: string + node_id: + type: string + network: + type: string + maker_fee: + type: number + format: double + description: Exchange's set maker fee + taker_fee: + type: number + format: double + description: 'Exchange''s set taker fee ' + bond_size: + type: number + format: double + description: Default bond size (percent) + current_swap_fee_rate: + type: number + format: double + description: Swap fees to perform on-chain transaction (percent) + version: + $ref: '#/components/schemas/Version' + notice_severity: + $ref: '#/components/schemas/NoticeSeverityEnum' + notice_message: + type: string + required: + - active_robots_today + - alternative_name + - alternative_site + - bond_size + - book_liquidity + - cln_version + - current_swap_fee_rate + - last_day_nonkyc_btc_premium + - last_day_volume + - lifetime_volume + - lnd_version + - maker_fee + - network + - node_alias + - node_id + - notice_message + - notice_severity + - num_public_buy_orders + - num_public_sell_orders + - robosats_running_commit_hash + - taker_fee + - version + ListOrder: + type: object + properties: + id: + type: integer + readOnly: true + status: + allOf: + - $ref: '#/components/schemas/StatusEnum' + minimum: 0 + maximum: 32767 + created_at: + type: string + format: date-time + expires_at: + type: string + format: date-time + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + minimum: 0 + maximum: 32767 + currency: + type: integer + nullable: true + amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + has_range: + type: boolean + min_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + max_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + payment_method: + type: string + maxLength: 70 + is_explicit: + type: boolean + premium: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + nullable: true + satoshis: + type: integer + maximum: 5000000 + minimum: 20000 + nullable: true + maker: + type: integer + nullable: true + taker: + type: integer + nullable: true + escrow_duration: + type: integer + maximum: 28800 + minimum: 1800 + bond_size: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,2})?$ + latitude: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,6})?$ + nullable: true + longitude: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,6})?$ + nullable: true + required: + - expires_at + - id + - type + MakeOrder: + type: object + properties: + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + minimum: 0 + maximum: 32767 + currency: + type: integer + description: Currency id. See [here](https://github.com/RoboSats/robosats/blob/main/frontend/static/assets/currencies.json) + for a list of all IDs + amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + has_range: + type: boolean + default: false + description: |- + Whether the order specifies a range of amount or a fixed amount. + + If `true`, then `min_amount` and `max_amount` fields are **required**. + + If `false` then `amount` is **required** + min_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + max_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + payment_method: + type: string + default: not specified + description: Can be any string. The UI recognizes [these payment methods](https://github.com/RoboSats/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) + and displays them with a logo. + maxLength: 70 + is_explicit: + type: boolean + default: false + description: Whether the order is explicitly priced or not. If set to `true` + then `satoshis` need to be specified + premium: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + nullable: true + satoshis: + type: integer + maximum: 5000000 + minimum: 20000 + nullable: true + public_duration: + type: integer + maximum: 86400 + minimum: 597.6 + escrow_duration: + type: integer + maximum: 28800 + minimum: 1800 + bond_size: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,2})?$ + latitude: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,6})?$ + nullable: true + longitude: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,6})?$ + nullable: true + required: + - currency + - type + Nested: + type: object + properties: + id: + type: integer + readOnly: true + currency: + allOf: + - $ref: '#/components/schemas/CurrencyEnum' + minimum: 0 + maximum: 32767 + exchange_rate: + type: string + format: decimal + pattern: ^-?\d{0,14}(?:\.\d{0,4})?$ + nullable: true + timestamp: + type: string + format: date-time + required: + - currency + - id + NoticeSeverityEnum: + enum: + - none + - warning + - success + - error + - info + type: string + description: |- + * `none` - none + * `warning` - warning + * `success` - success + * `error` - error + * `info` - info + NullEnum: + enum: + - null + OrderDetail: + type: object + properties: + id: + type: integer + readOnly: true + status: + allOf: + - $ref: '#/components/schemas/StatusEnum' + minimum: 0 + maximum: 32767 + created_at: + type: string + format: date-time + expires_at: + type: string + format: date-time + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + minimum: 0 + maximum: 32767 + currency: + type: integer + nullable: true + amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + has_range: + type: boolean + min_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + max_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + payment_method: + type: string + maxLength: 70 + is_explicit: + type: boolean + premium: + type: string + description: Premium over the CEX price set by the maker + premium_now: + type: number + format: double + description: Premium over the CEX price at the current time + satoshis: + type: integer + maximum: 5000000 + minimum: 20000 + nullable: true + satoshis_now: + type: integer + description: Maximum size of the order right now in Satoshis + maker: + type: integer + nullable: true + taker: + type: integer + nullable: true + escrow_duration: + type: integer + maximum: 28800 + minimum: 1800 + total_secs_exp: + type: integer + description: 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: + type: string + format: date-time + description: Time when the user penalty will expire. Penalty applies when + you create orders repeatedly without commiting a bond + is_maker: + type: boolean + description: Whether you are the maker or not + is_taker: + type: boolean + description: Whether you are the taker or not + is_participant: + type: boolean + description: True if you are either a taker or maker, False otherwise + maker_status: + type: string + description: |- + Status of the maker: + - **'Active'** (seen within last 2 min) + - **'Seen Recently'** (seen within last 10 min) + - **'Inactive'** (seen more than 10 min ago) + + Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty + taker_status: + type: boolean + description: True if you are either a taker or maker, False otherwise + price_now: + type: integer + description: Price of the order in the order's currency at the time of request + (upto 5 significant digits) + premium_percentile: + type: number + format: double + description: (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: + type: integer + description: (Only if `is_maker`) The number of public orders of the same + currency currently in the order book + tg_enabled: + type: boolean + description: (Only if `is_maker`) Whether Telegram notification is enabled + or not + tg_token: + type: string + description: (Only if `is_maker`) Your telegram bot token required to enable + notifications. + tg_bot_name: + type: string + description: (Only if `is_maker`) The Telegram username of the bot + is_buyer: + type: boolean + description: Whether you are a buyer of sats (you will be receiving sats) + is_seller: + type: boolean + description: Whether you are a seller of sats or not (you will be sending + sats) + maker_nick: + type: string + description: Nickname (Robot name) of the maker + taker_nick: + type: string + description: Nickname (Robot name) of the taker + status_message: + type: string + description: The current status of the order corresponding to the `status` + is_fiat_sent: + type: boolean + description: Whether or not the fiat amount is sent by the buyer + is_disputed: + type: boolean + description: Whether or not the counterparty raised a dispute + ur_nick: + type: string + description: Your Nick + maker_locked: + type: boolean + description: True if maker bond is locked, False otherwise + taker_locked: + type: boolean + description: True if taker bond is locked, False otherwise + escrow_locked: + type: boolean + description: True if escrow is locked, False otherwise. Escrow is the sats + to be sold, held by Robosats until the trade is finised. + trade_satoshis: + type: integer + description: 'Seller sees the amount of sats they need to send. Buyer sees + the amount of sats they will receive ' + bond_invoice: + type: string + description: When `status` = `0`, `3`. Bond invoice to be paid + bond_satoshis: + type: integer + description: The bond amount in satoshis + escrow_invoice: + type: string + description: For the seller, the escrow invoice to be held by RoboSats + escrow_satoshis: + type: integer + description: The escrow amount in satoshis + invoice_amount: + type: integer + description: The amount in sats the buyer needs to submit an invoice of + to receive the trade amount + swap_allowed: + type: boolean + description: Whether on-chain swap is allowed + swap_failure_reason: + type: string + description: Reason for why on-chain swap is not available + suggested_mining_fee_rate: + type: integer + description: fee in sats/vbyte for the on-chain swap + swap_fee_rate: + type: number + format: double + description: in percentage, the swap fee rate the platform charges + pending_cancel: + type: boolean + description: Your counterparty requested for a collaborative cancel when + `status` is either `8`, `9` or `10` + asked_for_cancel: + type: boolean + description: You requested for a collaborative cancel `status` is either + `8`, `9` or `10` + statement_submitted: + type: boolean + description: True if you have submitted a statement. Available when `status` + is `11` + retries: + type: integer + description: Number of times ln node has tried to make the payment to you + (only if you are the buyer) + next_retry_time: + type: string + format: date-time + description: The next time payment will be retried. Payment is retried every + 1 sec + failure_reason: + type: string + description: The reason the payout failed + invoice_expired: + type: boolean + description: 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 + public_duration: + type: integer + maximum: 86400 + minimum: 597.6 + bond_size: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,2})?$ + trade_fee_percent: + type: integer + description: The fee for the trade (fees differ for maker and taker) + bond_size_sats: + type: integer + description: The size of the bond in sats + bond_size_percent: + type: integer + description: same as `bond_size` + maker_summary: + $ref: '#/components/schemas/Summary' + taker_summary: + $ref: '#/components/schemas/Summary' + platform_summary: + $ref: '#/components/schemas/PlatformSummary' + expiry_reason: + nullable: true + minimum: 0 + maximum: 32767 + oneOf: + - $ref: '#/components/schemas/ExpiryReasonEnum' + - $ref: '#/components/schemas/NullEnum' + expiry_message: + type: string + description: The reason the order expired (message associated with the `expiry_reason`) + num_satoshis: + type: integer + description: only if status = `14` (Successful Trade) and is_buyer = `true` + sent_satoshis: + type: integer + description: only if status = `14` (Successful Trade) and is_buyer = `true` + txid: + type: string + description: Transaction id of the on-chain swap payout. Only if status + = `14` (Successful Trade) and is_buyer = `true` + network: + type: string + description: The network eg. 'testnet', 'mainnet'. Only if status = `14` + (Successful Trade) and is_buyer = `true` + latitude: + type: string + description: Latitude of the order for F2F payments + longitude: + type: string + description: Longitude of the order for F2F payments + required: + - expires_at + - id + - type + OrderPublic: + type: object + properties: + id: + type: integer + readOnly: true + created_at: + type: string + format: date-time + expires_at: + type: string + format: date-time + type: + allOf: + - $ref: '#/components/schemas/TypeEnum' + minimum: 0 + maximum: 32767 + currency: + type: integer + nullable: true + amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + has_range: + type: boolean + min_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + max_amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + payment_method: + type: string + maxLength: 70 + is_explicit: + type: boolean + premium: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + nullable: true + satoshis: + type: integer + maximum: 5000000 + minimum: 20000 + nullable: true + maker: + type: integer + nullable: true + maker_nick: + type: string + maker_status: + type: string + description: Status of the nick - "Active" or "Inactive" + price: + type: number + format: double + description: Price in order's fiat currency + escrow_duration: + type: integer + maximum: 28800 + minimum: 1800 + satoshis_now: + type: integer + description: The amount of sats to be traded at the present moment (not + including the fees) + bond_size: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,2})?$ + latitude: + type: string + format: decimal + pattern: ^-?\d{0,2}(?:\.\d{0,6})?$ + nullable: true + longitude: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,6})?$ + nullable: true + required: + - expires_at + - id + - type + PlatformSummary: + type: object + properties: + contract_timestamp: + type: string + format: date-time + description: Timestamp of when the contract was finalized (price and sats + fixed) + contract_total_time: + type: number + format: double + description: The time taken for the contract to complete (from taker taking + the order to completion of order) in seconds + routing_fee_sats: + type: integer + description: Sats payed by the exchange for routing fees. Mining fee in + case of on-chain swap payout + trade_revenue_sats: + type: integer + description: The sats the exchange earned from the trade + PostMessage: + type: object + properties: + PGP_message: + type: string + nullable: true + maxLength: 5000 + order_id: + type: integer + minimum: 0 + description: Order ID of chatroom + offset: + type: integer + minimum: 0 + nullable: true + description: Offset for message index to get as response + required: + - order_id + RatingEnum: + enum: + - '1' + - '2' + - '3' + - '4' + - '5' + type: string + description: |- + * `1` - 1 + * `2` - 2 + * `3` - 3 + * `4` - 4 + * `5` - 5 + StatusEnum: + enum: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + type: integer + description: |- + * `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 + Stealth: + type: object + properties: + wantsStealth: + type: boolean + required: + - wantsStealth + Summary: + type: object + properties: + sent_fiat: + type: integer + description: same as `amount` (only for buyer) + received_sats: + type: integer + description: same as `trade_satoshis` (only for buyer) + is_swap: + type: boolean + description: True if the payout was on-chain (only for buyer) + received_onchain_sats: + type: integer + description: The on-chain sats received (only for buyer and if `is_swap` + is `true`) + mining_fee_sats: + type: integer + description: Mining fees paid in satoshis (only for buyer and if `is_swap` + is `true`) + swap_fee_sats: + type: integer + description: Exchange swap fee in sats (i.e excluding miner fees) (only + for buyer and if `is_swap` is `true`) + swap_fee_percent: + type: number + format: double + description: same as `swap_fee_rate` (only for buyer and if `is_swap` is + `true` + sent_sats: + type: integer + description: The total sats you sent (only for seller) + received_fiat: + type: integer + description: same as `amount` (only for seller) + trade_fee_sats: + type: integer + description: Exchange fees in sats (Does not include swap fee and miner + fee) + Tick: + type: object + properties: + timestamp: + type: string + format: date-time + currency: + allOf: + - $ref: '#/components/schemas/Nested' + readOnly: true + volume: + type: string + format: decimal + nullable: true + price: + type: string + format: decimal + pattern: ^-?\d{0,14}(?:\.\d{0,2})?$ + nullable: true + premium: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,2})?$ + nullable: true + fee: + type: string + format: decimal + required: + - currency + TypeEnum: + enum: + - 0 + - 1 + type: integer + description: |- + * `0` - BUY + * `1` - SELL + UpdateOrder: + type: object + properties: + invoice: + type: string + nullable: true + description: |+ + Invoice used for payouts. Must be PGP signed with the robot's public key. The expected Armored PGP header is -----BEGIN PGP SIGNED MESSAGE----- + Hash: SHA512 + + maxLength: 15000 + routing_budget_ppm: + type: integer + maximum: 100001 + minimum: 0 + nullable: true + default: 0 + description: Max budget to allocate for routing in PPM + address: + type: string + nullable: true + description: |+ + Onchain address used for payouts. Must be PGP signed with the robot's public key. The expected Armored PGP header is -----BEGIN PGP SIGNED MESSAGE----- + Hash: SHA512 + + maxLength: 15000 + statement: + type: string + nullable: true + maxLength: 500000 + action: + $ref: '#/components/schemas/ActionEnum' + rating: + nullable: true + oneOf: + - $ref: '#/components/schemas/RatingEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + amount: + type: string + format: decimal + pattern: ^-?\d{0,10}(?:\.\d{0,8})?$ + nullable: true + mining_fee_rate: + type: string + format: decimal + pattern: ^-?\d{0,3}(?:\.\d{0,3})?$ + nullable: true + required: + - action + Version: + type: object + properties: + major: + type: integer + minor: + type: integer + patch: + type: integer + required: + - major + - minor + - patch + securitySchemes: + tokenAuth: + type: apiKey + in: header + name: Authorization + description: Token-based authentication with required prefix "Token" diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..99a5ec7f --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,28 @@ +import urllib.request + +from openapi_tester.schema_tester import SchemaTester +from rest_framework.response import Response +from rest_framework.test import APITestCase + +# Update api specs to the newest from a running django server (if any) +try: + urllib.request.urlretrieve( + "http://127.0.0.1:8000/api/schema", "tests/api_specs.yaml" + ) +except Exception as e: + print(f"Could not fetch current API specs: {e}") + print("Using previously existing api_specs.yaml definitions") + +schema_tester = SchemaTester(schema_file_path="tests/api_specs.yaml") + + +class BaseAPITestCase(APITestCase): + @staticmethod + def assertResponse(response: Response, **kwargs) -> None: + """helper to run validate_response and pass kwargs to it""" + + # List of endpoints with no available OpenAPI schema + skip_paths = ["/coordinator/login/"] + + if response.request["PATH_INFO"] not in skip_paths: + schema_tester.validate_response(response=response, **kwargs) diff --git a/tests/test_coordinator_info.py b/tests/test_coordinator_info.py index 2abf984a..c4708f84 100644 --- a/tests/test_coordinator_info.py +++ b/tests/test_coordinator_info.py @@ -4,10 +4,12 @@ from unittest.mock import patch from decouple import config from django.conf import settings from django.contrib.auth.models import User -from django.test import Client, TestCase +from django.test import Client +from django.urls import reverse from tests.mocks.cln import MockNodeStub from tests.mocks.lnd import MockVersionerStub +from tests.test_api import BaseAPITestCase FEE = config("FEE", cast=float, default=0.2) NODE_ID = config("NODE_ID", cast=str, default="033b58d7......") @@ -18,7 +20,7 @@ NOTICE_SEVERITY = config("NOTICE_SEVERITY", cast=str, default="none") NOTICE_MESSAGE = config("NOTICE_MESSAGE", cast=str, default="") -class CoordinatorInfoTest(TestCase): +class CoordinatorInfoTest(BaseAPITestCase): su_pass = "12345678" su_name = config("ESCROW_USERNAME", cast=str, default="admin") @@ -32,12 +34,14 @@ class CoordinatorInfoTest(TestCase): @patch("api.lightning.cln.node_pb2_grpc.NodeStub", MockNodeStub) @patch("api.lightning.lnd.verrpc_pb2_grpc.VersionerStub", MockVersionerStub) def test_info(self): - path = "/api/info/" + path = reverse("info") response = self.client.get(path) data = json.loads(response.content.decode()) self.assertEqual(response.status_code, 200) + self.assertResponse(response) + self.assertEqual(data["num_public_buy_orders"], 0) self.assertEqual(data["num_public_sell_orders"], 0) self.assertEqual(data["book_liquidity"], 0) diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index d6883bed..6109d9d1 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -5,7 +5,7 @@ from unittest.mock import patch from decouple import config from django.contrib.auth.models import User -from django.test import Client, TestCase +from django.urls import reverse from api.management.commands.follow_invoices import Command as FollowInvoices from api.models import Currency, Order @@ -15,39 +15,84 @@ from tests.mocks.lnd import ( # MockRouterStub,; MockSignerStub,; MockVersioner MockInvoicesStub, MockLightningStub, ) +from tests.test_api import BaseAPITestCase -class TradeTest(TestCase): +def read_file(file_path): + """ + Read a file and return its content. + """ + with open(file_path, "r") as file: + return file.read() + + +class TradeTest(BaseAPITestCase): su_pass = "12345678" su_name = config("ESCROW_USERNAME", cast=str, default="admin") - def setUp(self): + maker_form_with_range = { + "type": Order.Types.BUY, + "currency": 1, + "has_range": True, + "min_amount": 21, + "max_amount": 101.7, + "payment_method": "Advcash Cash F2F", + "is_explicit": False, + "premium": 3.34, + "public_duration": 69360, + "escrow_duration": 8700, + "bond_size": 3.5, + "latitude": 34.7455, + "longitude": 135.503, + } + + @classmethod + def setUpTestData(cls): """ - Create a superuser. The superuser is the escrow party. + Set up initial data for the test case. """ - self.client = Client() - User.objects.create_superuser(self.su_name, "super@user.com", self.su_pass) + # Create super user + User.objects.create_superuser(cls.su_name, "super@user.com", cls.su_pass) + + # Fetch currency prices from external APIs + cache_market() def test_login_superuser(self): """ - Test logging in as a superuser. + Test the login functionality for the superuser. """ - path = "/coordinator/login/" + path = reverse("admin:login") data = {"username": self.su_name, "password": self.su_pass} response = self.client.post(path, data) self.assertEqual(response.status_code, 302) + self.assertResponse(response) + + def test_cache_market(self): + """ + Test if the cache_market() call during test setup worked + """ + usd = Currency.objects.get(id=1) + self.assertIsInstance( + usd.exchange_rate, + Decimal, + f"Exchange rate is not a Decimal. Got {type(usd.exchange_rate)}", + ) + self.assertGreater( + usd.exchange_rate, 0, "Exchange rate is not higher than zero" + ) + self.assertIsInstance( + usd.timestamp, datetime, "External price timestamp is not a datetime" + ) def get_robot_auth(self, robot_index, first_encounter=False): """ Create an AUTH header that embeds token, pub_key, and enc_priv_key into a single string as requested by the robosats token middleware. """ - with open(f"tests/robots/{robot_index}/b91_token", "r") as file: - b91_token = file.read() - with open(f"tests/robots/{robot_index}/pub_key", "r") as file: - pub_key = file.read() - with open(f"tests/robots/{robot_index}/enc_priv_key", "r") as file: - enc_priv_key = file.read() + + b91_token = read_file(f"tests/robots/{robot_index}/b91_token") + pub_key = read_file(f"tests/robots/{robot_index}/pub_key") + enc_priv_key = read_file(f"tests/robots/{robot_index}/enc_priv_key") # First time a robot authenticated, it is registered by the backend, so pub_key and enc_priv_key is needed if first_encounter: @@ -59,14 +104,21 @@ class TradeTest(TestCase): return headers, pub_key, enc_priv_key - def assert_robot(self, response, pub_key, enc_priv_key, expected_nickname): + def assert_robot(self, response, pub_key, enc_priv_key, robot_index): + """ + Assert that the robot is created correctly. + """ + nickname = read_file(f"tests/robots/{robot_index}/nickname") + data = json.loads(response.content.decode()) self.assertEqual(response.status_code, 200) + self.assertResponse(response) + self.assertEqual( data["nickname"], - expected_nickname, - "Robot created nickname is not MyopicRacket333", + nickname, + f"Robot created nickname is not {nickname}", ) self.assertEqual( data["public_key"], pub_key, "Returned public Kky does not match" @@ -95,67 +147,37 @@ class TradeTest(TestCase): """ Creates the robots in /tests/robots/{robot_index} """ - path = "/api/robot/" + path = reverse("robot") headers, pub_key, enc_priv_key = self.get_robot_auth(robot_index, True) response = self.client.get(path, **headers) - with open(f"tests/robots/{robot_index}/nickname", "r") as file: - expected_nickname = file.read() - - self.assert_robot(response, pub_key, enc_priv_key, expected_nickname) + self.assert_robot(response, pub_key, enc_priv_key, robot_index) def test_create_robots(self): """ - Creates two robots to be used in the trade tests + Test the creation of two robots to be used in the trade tests """ self.create_robot(robot_index=1) self.create_robot(robot_index=2) - def test_cache_market(self): - cache_market() - - usd = Currency.objects.get(id=1) - self.assertIsInstance( - usd.exchange_rate, - Decimal, - f"Exchange rate is not a Decimal. Got {type(usd.exchange_rate)}", - ) - self.assertGreater( - usd.exchange_rate, 0, "Exchange rate is not higher than zero" - ) - self.assertIsInstance( - usd.timestamp, datetime, "External price timestamp is not a datetime" - ) - - def create_order(self, maker_form, robot_index=1): - # Requisites - # Cache market prices - self.test_cache_market() - path = "/api/make/" + def make_order(self, maker_form, robot_index=1): + """ + Create an order for the test. + """ + path = reverse("make") # Get valid robot auth headers headers, _, _ = self.get_robot_auth(robot_index, True) response = self.client.post(path, maker_form, **headers) return response - def test_create_order(self): - maker_form = { - "type": Order.Types.BUY, - "currency": 1, - "has_range": True, - "min_amount": 21, - "max_amount": 101.7, - "payment_method": "Advcash Cash F2F", - "is_explicit": False, - "premium": 3.34, - "public_duration": 69360, - "escrow_duration": 8700, - "bond_size": 3.5, - "latitude": 34.7455, - "longitude": 135.503, - } - response = self.create_order(maker_form, robot_index=1) + def test_make_order(self): + """ + Test the creation of an order. + """ + maker_form = self.maker_form_with_range + response = self.make_order(maker_form, robot_index=1) data = json.loads(response.content.decode()) # Checks @@ -237,7 +259,7 @@ class TradeTest(TestCase): @patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub) @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub) def get_order(self, order_id, robot_index=1, first_encounter=False): - path = "/api/order/" + path = reverse("order") params = f"?order_id={order_id}" headers, _, _ = self.get_robot_auth(robot_index, first_encounter) response = self.client.get(path + params, **headers) @@ -246,22 +268,10 @@ class TradeTest(TestCase): def test_get_order_created(self): # Make an order - maker_form = { - "type": Order.Types.BUY, - "currency": 1, - "has_range": True, - "min_amount": 21, - "max_amount": 101.7, - "payment_method": "Advcash Cash F2F", - "is_explicit": False, - "premium": 3.34, - "public_duration": 69360, - "escrow_duration": 8700, - "bond_size": 3.5, - "latitude": 34.7455, - "longitude": 135.503, - } - order_made_response = self.create_order(maker_form, robot_index=1) + maker_form = self.maker_form_with_range + robot_index = 1 + + order_made_response = self.make_order(maker_form, robot_index) order_made_data = json.loads(order_made_response.content.decode()) # Maker's first order fetch. Should trigger maker bond hold invoice generation. @@ -284,7 +294,9 @@ class TradeTest(TestCase): self.assertEqual(data["status_message"], Order.Status(Order.Status.WFB).label) self.assertFalse(data["is_fiat_sent"]) self.assertFalse(data["is_disputed"]) - self.assertEqual(data["ur_nick"], "MyopicRacket333") + self.assertEqual( + data["ur_nick"], read_file(f"tests/robots/{robot_index}/nickname") + ) self.assertTrue(isinstance(data["satoshis_now"], int)) self.assertFalse(data["maker_locked"]) self.assertFalse(data["taker_locked"]) @@ -302,9 +314,9 @@ class TradeTest(TestCase): follow_invoices = FollowInvoices() follow_invoices.follow_hold_invoices() - def create_and_publish_order(self, maker_form, robot_index=1): + def make_and_publish_order(self, maker_form, robot_index=1): # Make an order - order_made_response = self.create_order(maker_form, robot_index=1) + order_made_response = self.make_order(maker_form, robot_index) order_made_data = json.loads(order_made_response.content.decode()) # Maker's first order fetch. Should trigger maker bond hold invoice generation. @@ -318,23 +330,9 @@ class TradeTest(TestCase): return response def test_publish_order(self): - maker_form = { - "type": Order.Types.BUY, - "currency": 1, - "has_range": True, - "min_amount": 21, - "max_amount": 101.7, - "payment_method": "Advcash Cash F2F", - "is_explicit": False, - "premium": 3.34, - "public_duration": 69360, - "escrow_duration": 8700, - "bond_size": 3.5, - "latitude": 34.7455, - "longitude": 135.503, - } + maker_form = self.maker_form_with_range # Get order - response = self.create_and_publish_order(maker_form) + response = self.make_and_publish_order(maker_form) data = json.loads(response.content.decode()) self.assertEqual(response.status_code, 200) @@ -353,3 +351,66 @@ class TradeTest(TestCase): self.assertFalse(public_data["is_participant"]) self.assertTrue(isinstance(public_data["price_now"], float)) self.assertTrue(isinstance(data["satoshis_now"], int)) + + @patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub) + @patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub) + @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub) + def take_order(self, order_id, amount, robot_index=2): + path = reverse("order") + params = f"?order_id={order_id}" + headers, _, _ = self.get_robot_auth(robot_index, first_encounter=True) + body = {"action": "take", "amount": amount} + response = self.client.post(path + params, body, **headers) + + return response + + def make_and_take_order( + self, maker_form, take_amount=80, maker_index=1, taker_index=2 + ): + response_published = self.make_and_publish_order(maker_form, maker_index) + data_publised = json.loads(response_published.content.decode()) + response = self.take_order(data_publised["id"], take_amount, taker_index) + return response + + # def test_make_and_take_order(self): + # maker_index = 1 + # taker_index = 2 + # maker_form = self.maker_form_with_range + # self.create_robot(taker_index) #### WEEEE SHOULD NOT BE NEEDED >??? WHY ROBOT HAS NO LOGIN TIME?? + # response = self.make_and_take_order(maker_form, 80, maker_index, taker_index) + # data = json.loads(response.content.decode()) + + # print(data) + + # self.assertEqual( + # data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname") + # ) + # self.assertEqual( + # data["taker_nick"], read_file(f"tests/robots/{taker_index}/nickname") + # ) + # self.assertEqual( + # data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname") + # ) + # self.assertFalse(data["is_maker"]) + # self.assertTrue(data["is_taker"]) + # self.assertTrue(data["is_participant"]) + + # a = { + # "maker_status": "Active", + # "taker_status": "Active", + # "price_now": 38205.0, + # "premium_now": 3.34, + # "satoshis_now": 266196, + # "is_buyer": False, + # "is_seller": True, + # "taker_nick": "EquivalentWool707", + # "status_message": "Waiting for taker bond", + # "is_fiat_sent": False, + # "is_disputed": False, + # "ur_nick": "EquivalentWool707", + # "maker_locked": True, + # "taker_locked": False, + # "escrow_locked": False, + # "bond_invoice": "lntb73280n1pj5uypwpp5vklcx3s3c66ltz5v7kglppke5n3u6sa6h8m6whe278lza7rwfc7qd2j2pshjmt9de6zqun9vejhyetwvdjn5gp3vgcxgvfkv43z6e3cvyez6dpkxejj6cnxvsmj6c3exsuxxden89skzv3j9cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz2sxqzfvsp5hkz0dnvja244hc8jwmpeveaxtjd4ddzuqlpqc5zxa6tckr8py50s9qyyssqdcl6w2rhma7k3v904q4tuz68z82d6x47dgflk6m8jdtgt9dg3n9304axv8qvd66dq39sx7yu20sv5pyguv9dnjw3385y8utadxxsqtsqpf7p3w", + # "bond_satoshis": 7328, + # }