-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Related to safe-global/safe-transaction-service#645
- Loading branch information
Showing
12 changed files
with
233 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import logging | ||
from typing import Optional | ||
|
||
from gnosis.protocol import GnosisProtocolAPI, OrderKind | ||
|
||
from .. import EthereumClient, EthereumNetworkNotSupported | ||
from .exceptions import CannotGetPriceFromOracle | ||
from .oracles import PriceOracle | ||
from .utils import get_decimals | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class CowswapOracle(PriceOracle): | ||
""" | ||
CowSwap Oracle implementation | ||
https://docs.cow.fi/off-chain-services/api | ||
""" | ||
|
||
def __init__( | ||
self, | ||
ethereum_client: EthereumClient, | ||
): | ||
""" | ||
:param ethereum_client: | ||
""" | ||
self.ethereum_client = ethereum_client | ||
self.w3 = ethereum_client.w3 | ||
self.api = GnosisProtocolAPI(ethereum_client.get_network()) | ||
|
||
@classmethod | ||
def is_available( | ||
cls, | ||
ethereum_client: EthereumClient, | ||
) -> bool: | ||
""" | ||
:param ethereum_client: | ||
:return: `True` if CowSwap is available for the EthereumClient provided, `False` otherwise | ||
""" | ||
try: | ||
GnosisProtocolAPI(ethereum_client.get_network()) | ||
return True | ||
except EthereumNetworkNotSupported: | ||
return False | ||
|
||
def get_price( | ||
self, token_address_1: str, token_address_2: Optional[str] = None | ||
) -> float: | ||
token_address_2 = token_address_2 or self.api.weth_address | ||
if token_address_1 == token_address_2: | ||
return 1.0 | ||
|
||
token_1_decimals = get_decimals(token_address_1, self.ethereum_client) | ||
result = self.api.get_estimated_amount( | ||
token_address_1, token_address_2, OrderKind.SELL, 10**token_1_decimals | ||
) | ||
if "errorType" in result: | ||
error_message = ( | ||
f"Cannot get price from CowSwap {result} " | ||
f"for token-1={token_address_1} to token-2={token_address_2}" | ||
) | ||
logger.warning(error_message) | ||
raise CannotGetPriceFromOracle(error_message) | ||
else: | ||
# Decimals needs to be adjusted | ||
token_2_decimals = get_decimals(token_address_2, self.ethereum_client) | ||
return float(result["amount"]) / 10**token_2_decimals |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class OracleException(Exception): | ||
pass | ||
|
||
|
||
class InvalidPriceFromOracle(OracleException): | ||
pass | ||
|
||
|
||
class CannotGetPriceFromOracle(OracleException): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import functools | ||
import logging | ||
|
||
from eth_abi.exceptions import DecodingError | ||
from eth_typing import ChecksumAddress | ||
from web3.exceptions import BadFunctionCallOutput | ||
|
||
from gnosis.eth import EthereumClient | ||
|
||
from .exceptions import CannotGetPriceFromOracle | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@functools.lru_cache(maxsize=10_000) | ||
def get_decimals( | ||
token_address: ChecksumAddress, ethereum_client: EthereumClient | ||
) -> int: | ||
""" | ||
Auxiliary function so RPC call to get `token decimals` is cached and | ||
can be reused for every Oracle, instead of having a cache per Oracle. | ||
:param token_address: | ||
:param ethereum_client: | ||
:return: Decimals for a token | ||
:raises CannotGetPriceFromOracle: If there's a problem with the query | ||
""" | ||
try: | ||
return ethereum_client.erc20.get_decimals(token_address) | ||
except ( | ||
ValueError, | ||
BadFunctionCallOutput, | ||
DecodingError, | ||
) as e: | ||
error_message = f"Cannot get decimals for token={token_address}" | ||
logger.warning(error_message) | ||
raise CannotGetPriceFromOracle(error_message) from e |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from unittest import mock | ||
|
||
from django.test import TestCase | ||
|
||
from eth_account import Account | ||
|
||
from ... import EthereumClient | ||
from ...oracles import CannotGetPriceFromOracle, CowswapOracle | ||
from ..ethereum_test_case import EthereumTestCaseMixin | ||
from ..test_oracles import ( | ||
dai_token_mainnet_address, | ||
usdc_token_mainnet_address, | ||
usdt_token_mainnet_address, | ||
weth_token_mainnet_address, | ||
) | ||
from ..utils import just_test_if_mainnet_node | ||
|
||
|
||
class TestCowswapOracle(EthereumTestCaseMixin, TestCase): | ||
def test_get_price(self): | ||
mainnet_node = just_test_if_mainnet_node() | ||
ethereum_client = EthereumClient(mainnet_node) | ||
|
||
self.assertTrue(CowswapOracle.is_available(ethereum_client)) | ||
|
||
cowswap_oracle = CowswapOracle(ethereum_client) | ||
|
||
price = cowswap_oracle.get_price(weth_token_mainnet_address) | ||
self.assertEqual(price, 1.0) | ||
|
||
price = cowswap_oracle.get_price(dai_token_mainnet_address) | ||
self.assertLess(price, 1) | ||
self.assertGreater(price, 0) | ||
|
||
price = cowswap_oracle.get_price( | ||
weth_token_mainnet_address, dai_token_mainnet_address | ||
) | ||
self.assertGreater(price, 1) | ||
|
||
# Test with 2 stablecoins with same decimals | ||
price = cowswap_oracle.get_price( | ||
usdt_token_mainnet_address, usdc_token_mainnet_address | ||
) | ||
self.assertAlmostEqual(price, 1.0, delta=0.5) | ||
|
||
# Test with 2 stablecoins with different decimals | ||
price = cowswap_oracle.get_price( | ||
dai_token_mainnet_address, usdc_token_mainnet_address | ||
) | ||
self.assertAlmostEqual(price, 1.0, delta=0.5) | ||
|
||
price = cowswap_oracle.get_price( | ||
usdc_token_mainnet_address, dai_token_mainnet_address | ||
) | ||
self.assertAlmostEqual(price, 1.0, delta=0.5) | ||
|
||
random_token = Account.create().address | ||
with self.assertRaisesMessage( | ||
CannotGetPriceFromOracle, | ||
f"Cannot get decimals for token={random_token}", | ||
): | ||
cowswap_oracle.get_price(random_token) | ||
|
||
with mock.patch( | ||
"gnosis.eth.oracles.cowswap.get_decimals", autospec=True, return_value=18 | ||
): | ||
with self.assertRaisesMessage( | ||
CannotGetPriceFromOracle, | ||
f"Cannot get price from CowSwap " | ||
f"{{'errorType': 'UnsupportedToken', 'description': 'Token address {random_token.lower()}'}} " | ||
f"for token-1={random_token} to token-2={weth_token_mainnet_address}", | ||
): | ||
cowswap_oracle.get_price(random_token) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.