Merge pull request #1326 from aftermath2/add_order_cancel_status

Add cancel_status param to the cancel order action
This commit is contained in:
KoalaSat
2025-03-30 12:47:53 +00:00
committed by GitHub
10 changed files with 96 additions and 4 deletions

View File

@ -1024,7 +1024,17 @@ class Logics:
return False, None return False, None
@classmethod @classmethod
def cancel_order(cls, order, user, state=None): def cancel_order(cls, order, user, cancel_status=None):
# If cancel status is specified, do no cancel the order
# if it is not the correct one.
# This prevents the client from cancelling an order that
# recently changed status.
if cancel_status is not None:
if order.status != cancel_status:
return False, {
"bad_request": f"Current order status is {order.status}, not {cancel_status}."
}
# Do not change order status if an is in order # Do not change order status if an is in order
# any of these status # any of these status
do_not_cancel = [ do_not_cancel = [

View File

@ -245,6 +245,10 @@ class OrderViewSchema:
- `17` - Maker lost dispute - `17` - Maker lost dispute
- `18` - Taker lost dispute - `18` - Taker lost dispute
The client can use `cancel_status` to cancel the order only
if it is in the specified status. The server will
return an error without cancelling the trade otherwise.
Note that there are penalties involved for cancelling a order Note that there are penalties involved for cancelling a order
mid-trade so use this action carefully: mid-trade so use this action carefully:

View File

@ -642,6 +642,13 @@ class UpdateOrderSerializer(serializers.Serializer):
mining_fee_rate = serializers.DecimalField( mining_fee_rate = serializers.DecimalField(
max_digits=6, decimal_places=3, allow_null=True, required=False, default=None max_digits=6, decimal_places=3, allow_null=True, required=False, default=None
) )
cancel_status = serializers.ChoiceField(
choices=Order.Status.choices,
allow_null=True,
allow_blank=True,
default=None,
help_text="Status the order should have for it to be cancelled.",
)
class ClaimRewardSerializer(serializers.Serializer): class ClaimRewardSerializer(serializers.Serializer):

View File

@ -540,6 +540,7 @@ class OrderView(viewsets.ViewSet):
mining_fee_rate = serializer.data.get("mining_fee_rate") mining_fee_rate = serializer.data.get("mining_fee_rate")
statement = serializer.data.get("statement") statement = serializer.data.get("statement")
rating = serializer.data.get("rating") rating = serializer.data.get("rating")
cancel_status = serializer.data.get("cancel_status")
# 1) If action is take, it is a taker request! # 1) If action is take, it is a taker request!
if action == "take": if action == "take":
@ -571,7 +572,7 @@ class OrderView(viewsets.ViewSet):
# 2) If action is cancel # 2) If action is cancel
elif action == "cancel": elif action == "cancel":
valid, context = Logics.cancel_order(order, request.user) valid, context = Logics.cancel_order(order, request.user, cancel_status)
if not valid: if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)

View File

@ -1974,6 +1974,9 @@ components:
format: decimal format: decimal
pattern: ^-?\d{0,3}(?:\.\d{0,3})?$ pattern: ^-?\d{0,3}(?:\.\d{0,3})?$
nullable: true nullable: true
cancel_status:
allOf:
- $ref: '#/components/schemas/StatusEnum'
required: required:
- action - action
Version: Version:

View File

@ -151,6 +151,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
mining_fee_rate?: number; mining_fee_rate?: number;
statement?: string; statement?: string;
rating?: number; rating?: number;
cancel_status?: number;
} }
const renewOrder = function (): void { const renewOrder = function (): void {
@ -189,6 +190,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
mining_fee_rate, mining_fee_rate,
statement, statement,
rating, rating,
cancel_status
}: SubmitActionProps): void { }: SubmitActionProps): void {
const slot = garage.getSlot(); const slot = garage.getSlot();
@ -202,6 +204,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
mining_fee_rate, mining_fee_rate,
statement, statement,
rating, rating,
cancel_status
}) })
.then((data: Order) => { .then((data: Order) => {
setOpen(closeAll); setOpen(closeAll);
@ -223,8 +226,14 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
}; };
const cancel = function (): void { const cancel = function (): void {
const order = garage.getSlot()?.activeOrder;
const noConfirmation = Boolean(order?.is_maker && [0, 1, 2, 3].includes(order?.status));
setLoadingButtons({ ...noLoadingButtons, cancel: true }); setLoadingButtons({ ...noLoadingButtons, cancel: true });
submitAction({ action: 'cancel' }); submitAction({
action: 'cancel',
cancel_status: noConfirmation ? order?.status : undefined
});
}; };
const openDispute = function (): void { const openDispute = function (): void {

View File

@ -21,6 +21,7 @@ export interface SubmitActionProps {
statement?: string; statement?: string;
rating?: number; rating?: number;
amount?: number; amount?: number;
cancel_status?: number;
} }
export interface TradeRobotSummary { export interface TradeRobotSummary {

View File

@ -143,6 +143,9 @@ SPECTACULAR_SETTINGS = {
} }
}, },
"REDOC_DIST": "SIDECAR", "REDOC_DIST": "SIDECAR",
"ENUM_NAME_OVERRIDES": {
"StatusEnum": "api.models.order.Order.Status",
}
} }

View File

@ -1101,6 +1101,58 @@ class TradeTest(BaseAPITestCase):
data["bad_request"], "This order has been cancelled by the maker" data["bad_request"], "This order has been cancelled by the maker"
) )
def test_cancel_order_cancel_status(self):
"""
Tests the cancellation of a public order using cancel_status.
"""
trade = Trade(self.client)
trade.publish_order()
data = trade.response.json()
self.assertEqual(trade.response.status_code, 200)
self.assertResponse(trade.response)
self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label)
# Cancel order if the order status is public
trade.cancel_order(cancel_status=Order.Status.PUB)
self.assertEqual(trade.response.status_code, 400)
self.assertResponse(trade.response)
self.assertEqual(
trade.response.json()["bad_request"], "This order has been cancelled by the maker"
)
def test_cancel_order_different_cancel_status(self):
"""
Tests the cancellation of a paused order with a different cancel_status.
"""
trade = Trade(self.client)
trade.publish_order()
trade.pause_order()
data = trade.response.json()
self.assertEqual(trade.response.status_code, 200)
self.assertResponse(trade.response)
self.assertEqual(data["status_message"], Order.Status(Order.Status.PAU).label)
# Try to cancel order if it is public
trade.cancel_order(cancel_status=Order.Status.PUB)
data = trade.response.json()
self.assertEqual(trade.response.status_code, 400)
self.assertResponse(trade.response)
self.assertEqual(
trade.response.json()["bad_request"],
f"Current order status is {Order.Status.PAU}, not {Order.Status.PUB}."
)
# Cancel order to avoid leaving pending HTLCs after a successful test
trade.cancel_order()
def test_collaborative_cancel_order_in_chat(self): def test_collaborative_cancel_order_in_chat(self):
""" """
Tests the collaborative cancellation of an order in the chat state Tests the collaborative cancellation of an order in the chat state

View File

@ -114,11 +114,13 @@ class Trade:
self.response = self.client.get(path + params, **headers) self.response = self.client.get(path + params, **headers)
@patch("api.tasks.send_notification.delay", send_notification) @patch("api.tasks.send_notification.delay", send_notification)
def cancel_order(self, robot_index=1): def cancel_order(self, robot_index=1, cancel_status=None):
path = reverse("order") path = reverse("order")
params = f"?order_id={self.order_id}" params = f"?order_id={self.order_id}"
headers = self.get_robot_auth(robot_index) headers = self.get_robot_auth(robot_index)
body = {"action": "cancel"} body = {"action": "cancel"}
if cancel_status is not None:
body.update({"cancel_status": cancel_status})
self.response = self.client.post(path + params, body, **headers) self.response = self.client.post(path + params, body, **headers)
@patch("api.tasks.send_notification.delay", send_notification) @patch("api.tasks.send_notification.delay", send_notification)