Skip to content

Commit

Permalink
Fix DutchX Oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
Uxio0 committed Jun 3, 2019
1 parent 9e425c0 commit 7d2160d
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 59 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Django==2.2
cachetools==3.1.1
celery==4.3.0
django-authtools==1.6.0
django-celery-beat==1.4.0
Expand Down
26 changes: 21 additions & 5 deletions safe_relay_service/tokens/admin.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
from django.contrib import admin

from .models import PriceOracle, PriceOracleTicker, Token
from .exchanges import CannotGetTokenPriceFromApi


@admin.register(PriceOracle)
class PriceOracleAdmin(admin.ModelAdmin):
list_display = ('name', )
ordering = ('name',)


@admin.register(PriceOracleTicker)
class PriceOracleTicker(admin.ModelAdmin):
list_display = ('price_oracle_name', 'token_symbol', 'ticker', 'inverse')
list_display = ('token_symbol', 'price_oracle_name', 'ticker', 'inverse', 'price')
list_filter = (('token', admin.RelatedOnlyFieldListFilter), 'inverse')
list_select_related = ('price_oracle', 'token')
search_fields = ['token__symbol', '=token__address', 'price_oracle__name']

def price_oracle_name(self, obj):
return obj.price_oracle.name

def token_symbol(self, obj):
return obj.token.symbol

def get_queryset(self, request):
return super().get_queryset(request).select_related('price_oracle', 'token')


@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
list_display = ('address', 'name', 'symbol', 'decimals', 'fixed_eth_conversion', 'relevance', 'gas')
list_display = ('relevance', 'address', 'name', 'symbol', 'decimals', 'fixed_eth_conversion', 'gas')
list_filter = ('gas', 'decimals', 'fixed_eth_conversion')
ordering = ('relevance',)
search_fields = ['symbol', 'address', 'name']
readonly_fields = ('eth_value', 'price_oracle_ticker_pairs')

def eth_value(self, obj: Token):
try:
return obj.get_eth_value()
except CannotGetTokenPriceFromApi:
return None

def price_oracle_ticker_pairs(self, obj: Token):
return [(price_oracle_ticker.price_oracle.name, price_oracle_ticker.ticker) for price_oracle_ticker
in obj.price_oracle_tickers.all()]
59 changes: 27 additions & 32 deletions safe_relay_service/tokens/exchanges.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from abc import ABC, abstractmethod

import requests
from cachetools import TTLCache, cached

logger = logging.getLogger(__name__)

Expand All @@ -24,11 +25,13 @@ def get_price(self, ticker) -> float:
pass


class Binance:
class Binance(PriceOracle):
"""
Get valid symbols from https://api.binance.com/api/v1/exchangeInfo
Remember to always use USDT instead of USD
"""

@cached(cache=TTLCache(maxsize=1024, ttl=60))
def get_price(self, ticker) -> float:
url = 'https://api.binance.com/api/v3/avgPrice?symbol=' + ticker
response = requests.get(url)
Expand All @@ -39,33 +42,20 @@ def get_price(self, ticker) -> float:
return float(api_json['price'])


class DutchX:
def replace_tokens(self, ticker):
symbols = []
for symbol in ticker.split('-'):
symbol_lower = symbol.lower()
if symbol_lower == 'dai':
symbol = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'
symbols.append(symbol)
return '-'.join(symbols)

class DutchX(PriceOracle):
def validate_ticker(self, ticker: str):
if '-' not in ticker:
# Example ticker `0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359-WETH`
if 'WETH' not in ticker:
raise InvalidTicker(ticker)

def reverse_ticker(self, ticker: str):
return '-'.join(reversed(ticker.split('-')))

@cached(cache=TTLCache(maxsize=1024, ttl=1200))
def get_price(self, ticker: str) -> float:
self.validate_ticker(ticker)
try:
return self._get_price(ticker)
except CannotGetTokenPriceFromApi:
return 1 / self._get_price(self.reverse_ticker(ticker))

def _get_price(self, ticker: str):
ticker = self.replace_tokens(ticker)
url = 'https://dutchx.d.exchange/api/v1/markets/{}/price'.format(ticker)
url = 'https://dutchx.d.exchange/api/v1/markets/{}/prices/custom-median?requireWhitelisted=false&' \
'maximumTimePeriod=388800&numberOfAuctions=3'.format(ticker)
response = requests.get(url)
api_json = response.json()
if not response.ok or api_json is None:
Expand All @@ -74,10 +64,12 @@ def _get_price(self, ticker: str):
return float(api_json)


class Huobi:
class Huobi(PriceOracle):
"""
Get valid symbols from https://api.huobi.pro/v1/common/symbols
"""

@cached(cache=TTLCache(maxsize=1024, ttl=60))
def get_price(self, ticker) -> float:
url = 'https://api.huobi.pro/market/detail/merged?symbol=%s' % ticker
response = requests.get(url)
Expand All @@ -89,7 +81,9 @@ def get_price(self, ticker) -> float:
return float(api_json['tick']['close'])


class Kraken:
class Kraken(PriceOracle):

@cached(cache=TTLCache(maxsize=1024, ttl=60))
def get_price(self, ticker) -> float:
url = 'https://api.kraken.com/0/public/Ticker?pair=' + ticker
response = requests.get(url)
Expand All @@ -105,14 +99,15 @@ def get_price(self, ticker) -> float:


def get_price_oracle(name) -> PriceOracle:
name = name.lower()
if name == 'binance':
return Binance()
elif name == 'dutchx':
return DutchX()
elif name == 'huobi':
return Huobi()
elif name == 'kraken':
return Kraken()
oracles = {
'binance': Binance,
'dutchx': DutchX,
'huobi': Huobi,
'kraken': Kraken,
}

oracle = oracles.get(name.lower())
if oracle:
return oracle()
else:
raise NotImplementedError
raise NotImplementedError("Oracle '%s' not found" % name)
45 changes: 23 additions & 22 deletions safe_relay_service/tokens/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import math
from typing import Optional
from urllib.parse import urljoin, urlparse

from django.conf import settings
Expand Down Expand Up @@ -29,6 +30,18 @@ class PriceOracleTicker(models.Model):
def __str__(self):
return '%s - %s - %s - Inverse %s' % (self.price_oracle.name, self.token.symbol, self.ticker, self.inverse)

def _price(self) -> Optional[float]:
try:
price = get_price_oracle(self.price_oracle.name).get_price(self.ticker)
if price and self.inverse: # Avoid 1 / 0
price = 1 / price
except ExchangeApiException:
logger.warning('Cannot get price for %s - %s', self.price_oracle.name, self.ticker, exc_info=True)
price = None
return price

price = property(_price)


class Token(models.Model):
address = EthereumAddressField(primary_key=True)
Expand All @@ -46,31 +59,19 @@ class Token(models.Model):
def __str__(self):
return '%s - %s' % (self.name, self.address)

# TODO Cache
def get_eth_value(self) -> float:
if not self.fixed_eth_conversion: # None or 0 ignored
prices = []
# Get the average price of the price oracles
for price_oracle_ticker in self.price_oracle_tickers.all():
price_oracle_name = price_oracle_ticker.price_oracle.name
ticker = price_oracle_ticker.ticker
try:
price = get_price_oracle(price_oracle_name).get_price(ticker)
if price and price_oracle_ticker.inverse: # Avoid 1 / 0
price = 1 / price
prices.append(price)
except ExchangeApiException:
logger.warning('Cannot get price for %s', price_oracle_ticker, exc_info=True)
pass
number_prices = len(prices)
if number_prices == 0:
raise CannotGetTokenPriceFromApi('There is no working provider')
else:
return sum(prices) / number_prices
else:
if self.fixed_eth_conversion: # `None` or `0` are ignored
# Ether has 18 decimals, but maybe the token has a different number
multiplier = 1e18 / 10**self.decimals
multiplier = 1e18 / 10 ** self.decimals
return round(multiplier * float(self.fixed_eth_conversion), 10)
else:
prices = [price_oracle_ticker.price for price_oracle_ticker in self.price_oracle_tickers.all()]
prices = [price for price in prices if price is not None]
if prices:
# Get the average price of the price oracles
return sum(prices) / len(prices)
else:
raise CannotGetTokenPriceFromApi('There is no working provider for token=%s' % self.address)

def calculate_gas_price(self, gas_price: int, price_margin: float=1.0) -> int:
"""
Expand Down

0 comments on commit 7d2160d

Please sign in to comment.