Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add geoblocked countries #1258

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
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)
ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
ALTERNATIVE_NAME = 'RoboSats Mainnet'
Expand Down
19 changes: 17 additions & 2 deletions api/logics.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import math
from datetime import timedelta

from decouple import config
from decouple import config, Csv
from django.contrib.auth.models import User
from django.db.models import Q, Sum
from django.utils import timezone

from api.lightning.node import LNNode
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
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

FEE = float(config("FEE"))
Expand All @@ -29,6 +29,8 @@
config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED")
)

GEOBLOCKED_COUNTRIES = config("GEOBLOCKED_COUNTRIES", cast=Csv(), default=[])


class Logics:
@classmethod
Expand Down Expand Up @@ -137,6 +139,19 @@ def validate_order_size(cls, order):

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):
if amount > float(order.max_amount) or amount < float(order.min_amount):
return False, {
Expand Down
27 changes: 27 additions & 0 deletions api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,33 @@ def is_valid_token(token: str) -> bool:
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:
"""
Parses strings that have Object(ID,NAME) that match API models.
Expand Down
4 changes: 4 additions & 0 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ def post(self, request):
if not valid:
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.log(
f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ psycopg2==2.9.9
SQLAlchemy==2.0.16
django-import-export==3.3.8
requests[socks]
shapely==2.0.4
python-gnupg==0.5.2
daphne==4.1.0
drf-spectacular==0.27.2
Expand Down
32 changes: 32 additions & 0 deletions tests/test_trade_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,38 @@ def test_make_order(self):
self.assertIsNone(data["taker"], "New order's taker is not null")
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):
"""
Tests the creation of an order and the first request to see details,
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ def make_order(self, maker_form, robot_index=1):

response = self.client.post(path, maker_form, **headers)

self.response = response
if response.status_code == 201:
self.response = response
self.order_id = response.json()["id"]

def get_order(self, robot_index=1, first_encounter=False):
Expand Down
Loading