mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 11:26:24 +00:00
Add lock escrow tests
This commit is contained in:
parent
eaa0872f1b
commit
79a3df66a2
@ -350,6 +350,10 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`",
|
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`",
|
||||||
)
|
)
|
||||||
|
chat_last_index = serializers.IntegerField(
|
||||||
|
required=False,
|
||||||
|
help_text="The index of the last message sent in the trade chatroom",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
@ -431,6 +435,7 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
|||||||
"network",
|
"network",
|
||||||
"latitude",
|
"latitude",
|
||||||
"longitude",
|
"longitude",
|
||||||
|
"chat_last_index",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ services:
|
|||||||
- cln:/root/.lightning
|
- cln:/root/.lightning
|
||||||
- ./docker/cln/plugins/cln-grpc-hold:/root/.lightning/plugins/cln-grpc-hold
|
- ./docker/cln/plugins/cln-grpc-hold:/root/.lightning/plugins/cln-grpc-hold
|
||||||
- bitcoin:/root/.bitcoin
|
- bitcoin:/root/.bitcoin
|
||||||
command: --regtest --wumbo --bitcoin-rpcuser=test --bitcoin-rpcpassword=test --rest-host=0.0.0.0 --bind-addr=127.0.0.1:9737 --grpc-port=9999 --grpc-hold-port=9998 --important-plugin=/root/.lightning/plugins/cln-grpc-hold --database-upgrade=true
|
command: --regtest --wumbo --bitcoin-rpcuser=test --bitcoin-rpcpassword=test --rest-host=0.0.0.0 --bind-addr=127.0.0.1:9737 --max-concurrent-htlcs=483 --grpc-port=9999 --grpc-hold-port=9998 --important-plugin=/root/.lightning/plugins/cln-grpc-hold --database-upgrade=true
|
||||||
depends_on:
|
depends_on:
|
||||||
- bitcoind
|
- bitcoind
|
||||||
network_mode: service:bitcoind
|
network_mode: service:bitcoind
|
||||||
|
@ -1671,6 +1671,9 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
description: Longitude of the order for F2F payments
|
description: Longitude of the order for F2F payments
|
||||||
|
chat_last_index:
|
||||||
|
type: integer
|
||||||
|
description: The index of the last message sent in the trade chatroom
|
||||||
required:
|
required:
|
||||||
- expires_at
|
- expires_at
|
||||||
- id
|
- id
|
||||||
|
@ -63,19 +63,39 @@ def wait_for_lnd_node_sync(node_name):
|
|||||||
time.sleep(wait_step)
|
time.sleep(wait_step)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_lnd_active_channels(node_name):
|
def LND_has_active_channels(node_name):
|
||||||
node = get_node(node_name)
|
node = get_node(node_name)
|
||||||
waited = 0
|
|
||||||
while True:
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f'http://localhost:{node["port"]}/v1/getinfo', headers=node["headers"]
|
f'http://localhost:{node["port"]}/v1/getinfo', headers=node["headers"]
|
||||||
)
|
)
|
||||||
if response.json()["num_active_channels"] > 0:
|
return True if response.json()["num_active_channels"] > 0 else False
|
||||||
|
|
||||||
|
|
||||||
|
def CLN_has_active_channels():
|
||||||
|
from api.lightning.cln import CLNNode
|
||||||
|
|
||||||
|
response = CLNNode.get_info()
|
||||||
|
return True if response.num_active_channels > 0 else False
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_active_channels(lnvendor, node_name="coordinator"):
|
||||||
|
waited = 0
|
||||||
|
while True:
|
||||||
|
if lnvendor == "LND":
|
||||||
|
if LND_has_active_channels(node_name):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
f"\rWaiting for {node_name} node channels to be active {round(waited,1)}s"
|
f"\rWaiting for {node_name} LND node channel to be active {round(waited,1)}s"
|
||||||
)
|
)
|
||||||
|
elif lnvendor == "CLN":
|
||||||
|
if CLN_has_active_channels():
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
sys.stdout.write(
|
||||||
|
f"\rWaiting for {node_name} CLN node channel to be active {round(waited,1)}s"
|
||||||
|
)
|
||||||
|
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
waited += wait_step
|
waited += wait_step
|
||||||
time.sleep(wait_step)
|
time.sleep(wait_step)
|
||||||
|
@ -10,7 +10,10 @@ from django.urls import reverse
|
|||||||
from api.management.commands.follow_invoices import Command as FollowInvoices
|
from api.management.commands.follow_invoices import Command as FollowInvoices
|
||||||
from api.models import Currency, Order
|
from api.models import Currency, Order
|
||||||
from api.tasks import cache_market
|
from api.tasks import cache_market
|
||||||
|
from control.tasks import compute_node_balance
|
||||||
from tests.node_utils import (
|
from tests.node_utils import (
|
||||||
|
CLN_has_active_channels,
|
||||||
|
LND_has_active_channels,
|
||||||
connect_to_node,
|
connect_to_node,
|
||||||
create_address,
|
create_address,
|
||||||
generate_blocks,
|
generate_blocks,
|
||||||
@ -18,9 +21,8 @@ from tests.node_utils import (
|
|||||||
get_lnd_node_id,
|
get_lnd_node_id,
|
||||||
open_channel,
|
open_channel,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
wait_for_cln_active_channels,
|
wait_for_active_channels,
|
||||||
wait_for_cln_node_sync,
|
wait_for_cln_node_sync,
|
||||||
wait_for_lnd_active_channels,
|
|
||||||
wait_for_lnd_node_sync,
|
wait_for_lnd_node_sync,
|
||||||
)
|
)
|
||||||
from tests.test_api import BaseAPITestCase
|
from tests.test_api import BaseAPITestCase
|
||||||
@ -40,7 +42,7 @@ class TradeTest(BaseAPITestCase):
|
|||||||
su_pass = "12345678"
|
su_pass = "12345678"
|
||||||
su_name = config("ESCROW_USERNAME", cast=str, default="admin")
|
su_name = config("ESCROW_USERNAME", cast=str, default="admin")
|
||||||
|
|
||||||
maker_form_with_range = {
|
maker_form_buy_with_range = {
|
||||||
"type": Order.Types.BUY,
|
"type": Order.Types.BUY,
|
||||||
"currency": 1,
|
"currency": 1,
|
||||||
"has_range": True,
|
"has_range": True,
|
||||||
@ -63,12 +65,17 @@ class TradeTest(BaseAPITestCase):
|
|||||||
elif LNVENDOR == "CLN":
|
elif LNVENDOR == "CLN":
|
||||||
wait_for_cln_node_sync()
|
wait_for_cln_node_sync()
|
||||||
|
|
||||||
def wait_active_channels():
|
def wait_channels():
|
||||||
wait_for_lnd_active_channels("robot")
|
wait_for_active_channels("LND", "robot")
|
||||||
|
wait_for_active_channels(LNVENDOR, "coordinator")
|
||||||
|
|
||||||
|
def channel_is_active():
|
||||||
|
robot_channel_active = LND_has_active_channels("robot")
|
||||||
if LNVENDOR == "LND":
|
if LNVENDOR == "LND":
|
||||||
wait_for_lnd_active_channels("coordinator")
|
coordinator_channel_active = LND_has_active_channels("coordinator")
|
||||||
elif LNVENDOR == "CLN":
|
elif LNVENDOR == "CLN":
|
||||||
wait_for_cln_active_channels()
|
coordinator_channel_active = CLN_has_active_channels()
|
||||||
|
return robot_channel_active and coordinator_channel_active
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -81,6 +88,13 @@ class TradeTest(BaseAPITestCase):
|
|||||||
# Fetch currency prices from external APIs
|
# Fetch currency prices from external APIs
|
||||||
cache_market()
|
cache_market()
|
||||||
|
|
||||||
|
# Skip node setup and channel creation if both nodes have an active channel already
|
||||||
|
if cls.channel_is_active():
|
||||||
|
print("Regtest network was already ready. Skipping initalization.")
|
||||||
|
# Take the first node balances snapshot
|
||||||
|
compute_node_balance()
|
||||||
|
return
|
||||||
|
|
||||||
# Fund two LN nodes in regtest and open channels
|
# Fund two LN nodes in regtest and open channels
|
||||||
# Coordinator is either LND or CLN. Robot user is always LND.
|
# Coordinator is either LND or CLN. Robot user is always LND.
|
||||||
if LNVENDOR == "LND":
|
if LNVENDOR == "LND":
|
||||||
@ -106,9 +120,12 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
# Wait a tiny bit so payments can be done in the new channel
|
# Wait a tiny bit so payments can be done in the new channel
|
||||||
cls.wait_nodes_sync()
|
cls.wait_nodes_sync()
|
||||||
cls.wait_active_channels()
|
cls.wait_channels()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Take the first node balances snapshot
|
||||||
|
compute_node_balance()
|
||||||
|
|
||||||
def test_login_superuser(self):
|
def test_login_superuser(self):
|
||||||
"""
|
"""
|
||||||
Test the login functionality for the superuser.
|
Test the login functionality for the superuser.
|
||||||
@ -231,7 +248,7 @@ class TradeTest(BaseAPITestCase):
|
|||||||
"""
|
"""
|
||||||
Test the creation of an order.
|
Test the creation of an order.
|
||||||
"""
|
"""
|
||||||
maker_form = self.maker_form_with_range
|
maker_form = self.maker_form_buy_with_range
|
||||||
response = self.make_order(maker_form, robot_index=1)
|
response = self.make_order(maker_form, robot_index=1)
|
||||||
data = json.loads(response.content.decode())
|
data = json.loads(response.content.decode())
|
||||||
|
|
||||||
@ -320,9 +337,21 @@ class TradeTest(BaseAPITestCase):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def cancel_order(self, order_id, robot_index=1):
|
||||||
|
path = reverse("order")
|
||||||
|
params = f"?order_id={order_id}"
|
||||||
|
headers = self.get_robot_auth(robot_index)
|
||||||
|
body = {"action": "cancel"}
|
||||||
|
response = self.client.post(path + params, body, **headers)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def test_get_order_created(self):
|
def test_get_order_created(self):
|
||||||
# Make an order
|
"""
|
||||||
maker_form = self.maker_form_with_range
|
Tests the creation of an order and the first request to see details,
|
||||||
|
including, the creation of the maker bond invoice.
|
||||||
|
"""
|
||||||
|
maker_form = self.maker_form_buy_with_range
|
||||||
robot_index = 1
|
robot_index = 1
|
||||||
|
|
||||||
order_made_response = self.make_order(maker_form, robot_index)
|
order_made_response = self.make_order(maker_form, robot_index)
|
||||||
@ -359,6 +388,9 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertFalse(data["escrow_locked"])
|
self.assertFalse(data["escrow_locked"])
|
||||||
self.assertTrue(isinstance(data["bond_satoshis"], int))
|
self.assertTrue(isinstance(data["bond_satoshis"], int))
|
||||||
|
|
||||||
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
|
self.cancel_order(data["id"])
|
||||||
|
|
||||||
def check_for_locked_bonds(self):
|
def check_for_locked_bonds(self):
|
||||||
# A background thread checks every 5 second the status of invoices. We invoke directly during test.
|
# A background thread checks every 5 second the status of invoices. We invoke directly during test.
|
||||||
# It will ask LND via gRPC. In our test, the request/response from LND is mocked, and it will return fake invoice status "ACCEPTED"
|
# It will ask LND via gRPC. In our test, the request/response from LND is mocked, and it will return fake invoice status "ACCEPTED"
|
||||||
@ -385,7 +417,10 @@ class TradeTest(BaseAPITestCase):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def test_publish_order(self):
|
def test_publish_order(self):
|
||||||
maker_form = self.maker_form_with_range
|
"""
|
||||||
|
Tests a trade from order creation to published (maker bond locked).
|
||||||
|
"""
|
||||||
|
maker_form = self.maker_form_buy_with_range
|
||||||
# Get order
|
# Get order
|
||||||
response = self.make_and_publish_order(maker_form)
|
response = self.make_and_publish_order(maker_form)
|
||||||
data = json.loads(response.content.decode())
|
data = json.loads(response.content.decode())
|
||||||
@ -409,9 +444,9 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertTrue(isinstance(public_data["price_now"], float))
|
self.assertTrue(isinstance(public_data["price_now"], float))
|
||||||
self.assertTrue(isinstance(data["satoshis_now"], int))
|
self.assertTrue(isinstance(data["satoshis_now"], int))
|
||||||
|
|
||||||
# @patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub)
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
# @patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub)
|
self.cancel_order(data["id"])
|
||||||
# @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub)
|
|
||||||
def take_order(self, order_id, amount, robot_index=2):
|
def take_order(self, order_id, amount, robot_index=2):
|
||||||
path = reverse("order")
|
path = reverse("order")
|
||||||
params = f"?order_id={order_id}"
|
params = f"?order_id={order_id}"
|
||||||
@ -430,9 +465,12 @@ class TradeTest(BaseAPITestCase):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def test_make_and_take_order(self):
|
def test_make_and_take_order(self):
|
||||||
|
"""
|
||||||
|
Tests a trade from order creation to taken.
|
||||||
|
"""
|
||||||
maker_index = 1
|
maker_index = 1
|
||||||
taker_index = 2
|
taker_index = 2
|
||||||
maker_form = self.maker_form_with_range
|
maker_form = self.maker_form_buy_with_range
|
||||||
|
|
||||||
response = self.make_and_take_order(maker_form, 80, maker_index, taker_index)
|
response = self.make_and_take_order(maker_form, 80, maker_index, taker_index)
|
||||||
data = json.loads(response.content.decode())
|
data = json.loads(response.content.decode())
|
||||||
@ -440,6 +478,7 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertResponse(response)
|
self.assertResponse(response)
|
||||||
|
|
||||||
|
self.assertEqual(data["status_message"], Order.Status(Order.Status.TAK).label)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname")
|
data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname")
|
||||||
)
|
)
|
||||||
@ -449,26 +488,112 @@ class TradeTest(BaseAPITestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname")
|
data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname")
|
||||||
)
|
)
|
||||||
|
self.assertEqual(data["maker_status"], "Active")
|
||||||
|
self.assertEqual(data["taker_status"], "Active")
|
||||||
self.assertFalse(data["is_maker"])
|
self.assertFalse(data["is_maker"])
|
||||||
|
self.assertFalse(data["is_buyer"])
|
||||||
|
self.assertTrue(data["is_seller"])
|
||||||
self.assertTrue(data["is_taker"])
|
self.assertTrue(data["is_taker"])
|
||||||
self.assertTrue(data["is_participant"])
|
self.assertTrue(data["is_participant"])
|
||||||
|
self.assertTrue(data["maker_locked"])
|
||||||
|
self.assertFalse(data["taker_locked"])
|
||||||
|
self.assertFalse(data["escrow_locked"])
|
||||||
|
|
||||||
# a = {
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
# "maker_status": "Active",
|
self.cancel_order(data["id"])
|
||||||
# "taker_status": "Active",
|
|
||||||
# "price_now": 38205.0,
|
def make_and_lock_contract(
|
||||||
# "premium_now": 3.34,
|
self, maker_form, take_amount=80, maker_index=1, taker_index=2
|
||||||
# "satoshis_now": 266196,
|
):
|
||||||
# "is_buyer": False,
|
# Make an order
|
||||||
# "is_seller": True,
|
order_taken_response = self.make_and_take_order(
|
||||||
# "taker_nick": "EquivalentWool707",
|
maker_form, take_amount, maker_index, taker_index
|
||||||
# "status_message": "Waiting for taker bond",
|
)
|
||||||
# "is_fiat_sent": False,
|
order_taken_data = json.loads(order_taken_response.content.decode())
|
||||||
# "is_disputed": False,
|
|
||||||
# "ur_nick": "EquivalentWool707",
|
# Maker's first order fetch. Should trigger maker bond hold invoice generation.
|
||||||
# "maker_locked": True,
|
response = self.get_order(order_taken_data["id"], taker_index)
|
||||||
# "taker_locked": False,
|
invoice = response.json()["bond_invoice"]
|
||||||
# "escrow_locked": False,
|
|
||||||
# "bond_invoice": "lntb73280n1pj5uypwpp5vklcx3s3c66ltz5v7kglppke5n3u6sa6h8m6whe278lza7rwfc7qd2j2pshjmt9de6zqun9vejhyetwvdjn5gp3vgcxgvfkv43z6e3cvyez6dpkxejj6cnxvsmj6c3exsuxxden89skzv3j9cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz2sxqzfvsp5hkz0dnvja244hc8jwmpeveaxtjd4ddzuqlpqc5zxa6tckr8py50s9qyyssqdcl6w2rhma7k3v904q4tuz68z82d6x47dgflk6m8jdtgt9dg3n9304axv8qvd66dq39sx7yu20sv5pyguv9dnjw3385y8utadxxsqtsqpf7p3w",
|
# Lock the invoice from the robot's node
|
||||||
# "bond_satoshis": 7328,
|
pay_invoice("robot", invoice)
|
||||||
# }
|
|
||||||
|
# Check for invoice locked (the mocked LND will return ACCEPTED)
|
||||||
|
self.check_for_locked_bonds()
|
||||||
|
|
||||||
|
# Get order
|
||||||
|
response = self.get_order(order_taken_data["id"], taker_index)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def test_make_and_lock_contract(self):
|
||||||
|
"""
|
||||||
|
Tests a trade from order creation to taker bond locked.
|
||||||
|
"""
|
||||||
|
maker_index = 1
|
||||||
|
taker_index = 2
|
||||||
|
maker_form = self.maker_form_buy_with_range
|
||||||
|
|
||||||
|
response = self.make_and_lock_contract(maker_form, 80, maker_index, taker_index)
|
||||||
|
data = json.loads(response.content.decode())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertResponse(response)
|
||||||
|
|
||||||
|
self.assertEqual(data["status_message"], Order.Status(Order.Status.WF2).label)
|
||||||
|
self.assertEqual(data["maker_status"], "Active")
|
||||||
|
self.assertEqual(data["taker_status"], "Active")
|
||||||
|
self.assertTrue(data["is_participant"])
|
||||||
|
self.assertTrue(data["maker_locked"])
|
||||||
|
self.assertTrue(data["taker_locked"])
|
||||||
|
self.assertFalse(data["escrow_locked"])
|
||||||
|
|
||||||
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
|
self.cancel_order(data["id"])
|
||||||
|
|
||||||
|
def trade_to_locked_escrow(
|
||||||
|
self, maker_form, take_amount=80, maker_index=1, taker_index=2
|
||||||
|
):
|
||||||
|
# Make an order
|
||||||
|
locked_taker_response = self.make_and_lock_contract(
|
||||||
|
maker_form, take_amount, maker_index, taker_index
|
||||||
|
)
|
||||||
|
locked_taker_response_data = json.loads(locked_taker_response.content.decode())
|
||||||
|
|
||||||
|
# Maker's first order fetch. Should trigger maker bond hold invoice generation.
|
||||||
|
response = self.get_order(locked_taker_response_data["id"], taker_index)
|
||||||
|
print("HEREEEEEEEEEEEEEEEEEEEEEEREEEEEEEEEEEEEEEE")
|
||||||
|
print(response.json())
|
||||||
|
invoice = response.json()["escrow_invoice"]
|
||||||
|
|
||||||
|
# Lock the invoice from the robot's node
|
||||||
|
pay_invoice("robot", invoice)
|
||||||
|
|
||||||
|
# Check for invoice locked (the mocked LND will return ACCEPTED)
|
||||||
|
self.check_for_locked_bonds()
|
||||||
|
|
||||||
|
# Get order
|
||||||
|
response = self.get_order(locked_taker_response_data["id"], taker_index)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def test_trade_to_locked_escrow(self):
|
||||||
|
"""
|
||||||
|
Tests a trade from order creation until escrow locked, before
|
||||||
|
invoice/address is submitted by buyer.
|
||||||
|
"""
|
||||||
|
maker_index = 1
|
||||||
|
taker_index = 2
|
||||||
|
maker_form = self.maker_form_buy_with_range
|
||||||
|
|
||||||
|
response = self.trade_to_locked_escrow(maker_form, 80, maker_index, taker_index)
|
||||||
|
data = json.loads(response.content.decode())
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertResponse(response)
|
||||||
|
|
||||||
|
self.assertEqual(data["status_message"], Order.Status(Order.Status.WFI).label)
|
||||||
|
self.assertTrue(data["maker_locked"])
|
||||||
|
self.assertTrue(data["taker_locked"])
|
||||||
|
self.assertTrue(data["escrow_locked"])
|
||||||
|
|
||||||
|
# Cancel order to avoid leaving pending HTLCs after a successful test
|
||||||
|
self.cancel_order(data["id"], 2)
|
||||||
|
Loading…
Reference in New Issue
Block a user