Feat: add coordinator opt for geoblocked countries (#1258)

* Add location validator

* Add bad location tests
This commit is contained in:
Reckless_Satoshi 2024-04-29 22:58:03 +00:00 committed by GitHub
parent ebc1bb70fa
commit 34ef099573
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 88 additions and 3 deletions

View File

@ -58,6 +58,12 @@ SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion # e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
ONION_LOCATION = '' ONION_LOCATION = ''
# Geoblocked countries (will reject F2F trades).
# List of A3 country codes (see fhttps://en.wikipedia.org/wiki/ISO_3166-1_alpha-3)
# Leave empty '' to allow all countries.
# Example 'NOR,USA,CZE'.
GEOBLOCKED_COUNTRIES = 'ABW,AFG,AGO'
# Link to robosats alternative site (shown in frontend in statsfornerds so users can switch mainnet/testnet) # Link to robosats alternative site (shown in frontend in statsfornerds so users can switch mainnet/testnet)
ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion' ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
ALTERNATIVE_NAME = 'RoboSats Mainnet' ALTERNATIVE_NAME = 'RoboSats Mainnet'

View File

@ -1,7 +1,7 @@
import math import math
from datetime import timedelta from datetime import timedelta
from decouple import config from decouple import config, Csv
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q, Sum from django.db.models import Q, Sum
from django.utils import timezone from django.utils import timezone
@ -9,7 +9,7 @@ from django.utils import timezone
from api.lightning.node import LNNode from api.lightning.node import LNNode
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
from api.tasks import send_devfund_donation, send_notification from api.tasks import send_devfund_donation, send_notification
from api.utils import get_minning_fee, validate_onchain_address from api.utils import get_minning_fee, validate_onchain_address, location_country
from chat.models import Message from chat.models import Message
FEE = float(config("FEE")) FEE = float(config("FEE"))
@ -29,6 +29,8 @@ MAX_MINING_NETWORK_SPEEDUP_EXPECTED = float(
config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED") config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED")
) )
GEOBLOCKED_COUNTRIES = config("GEOBLOCKED_COUNTRIES", cast=Csv(), default=[])
class Logics: class Logics:
@classmethod @classmethod
@ -137,6 +139,19 @@ class Logics:
return True, None return True, None
@classmethod
def validate_location(cls, order) -> bool:
if not (order.latitude or order.longitude):
return True, None
country = location_country(order.longitude, order.latitude)
if country in GEOBLOCKED_COUNTRIES:
return False, {
"bad_request": f"The coordinator does not support orders in {country}"
}
else:
return True, None
def validate_amount_within_range(order, amount): def validate_amount_within_range(order, amount):
if amount > float(order.max_amount) or amount < float(order.min_amount): if amount > float(order.max_amount) or amount < float(order.min_amount):
return False, { return False, {

View File

@ -479,6 +479,33 @@ def is_valid_token(token: str) -> bool:
return all(c in charset for c in token) return all(c in charset for c in token)
def location_country(lon: float, lat: float) -> str:
"""
Returns the country code of a lon/lat location
"""
from shapely.geometry import shape, Point
from shapely.prepared import prep
# Load the GeoJSON data from a local file
with open("frontend/static/assets/geo/countries-coastline-10km.geo.json") as f:
countries_geojeson = json.load(f)
# Prepare the countries for reverse geocoding
countries = {}
for feature in countries_geojeson["features"]:
geom = feature["geometry"]
country_code = feature["properties"]["A3"]
countries[country_code] = prep(shape(geom))
point = Point(lon, lat)
for country_code, geom in countries.items():
if geom.contains(point):
return country_code
return "unknown"
def objects_to_hyperlinks(logs: str) -> str: def objects_to_hyperlinks(logs: str) -> str:
""" """
Parses strings that have Object(ID,NAME) that match API models. Parses strings that have Object(ID,NAME) that match API models.

View File

@ -166,6 +166,10 @@ class MakerView(CreateAPIView):
if not valid: if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
valid, context = Logics.validate_location(order)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
order.save() order.save()
order.log( order.log(
f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})" f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})"

View File

@ -22,6 +22,7 @@ psycopg2==2.9.9
SQLAlchemy==2.0.16 SQLAlchemy==2.0.16
django-import-export==3.3.8 django-import-export==3.3.8
requests[socks] requests[socks]
shapely==2.0.4
python-gnupg==0.5.2 python-gnupg==0.5.2
daphne==4.1.0 daphne==4.1.0
drf-spectacular==0.27.2 drf-spectacular==0.27.2

View File

@ -239,6 +239,38 @@ class TradeTest(BaseAPITestCase):
self.assertIsNone(data["taker"], "New order's taker is not null") self.assertIsNone(data["taker"], "New order's taker is not null")
self.assert_order_logs(data["id"]) self.assert_order_logs(data["id"])
def test_make_order_on_blocked_country(self):
"""
Test the creation of an F2F order on a geoblocked location
"""
trade = Trade(
self.client,
# latitude and longitud in Aruba. One of the countries blocked in the example conf.
maker_form={
"type": 0,
"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": -11.8014, # Angola AGO
"longitude": 17.3575,
},
) # init of Trade calls make_order() with the default maker form.
data = trade.response.json()
self.assertEqual(trade.response.status_code, 400)
self.assertResponse(trade.response)
self.assertEqual(
data["bad_request"], "The coordinator does not support orders in AGO"
)
def test_get_order_created(self): def test_get_order_created(self):
""" """
Tests the creation of an order and the first request to see details, Tests the creation of an order and the first request to see details,

View File

@ -98,8 +98,8 @@ class Trade:
response = self.client.post(path, maker_form, **headers) response = self.client.post(path, maker_form, **headers)
self.response = response
if response.status_code == 201: if response.status_code == 201:
self.response = response
self.order_id = response.json()["id"] self.order_id = response.json()["id"]
def get_order(self, robot_index=1, first_encounter=False): def get_order(self, robot_index=1, first_encounter=False):