From da4b21f4887716cfa886662d0d85f45cb14f8e93 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 7 Nov 2019 15:37:44 +0100 Subject: [PATCH 01/38] Include owners in `safeCreation` notification --- safe_relay_service/relay/services/notification_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/safe_relay_service/relay/services/notification_service.py b/safe_relay_service/relay/services/notification_service.py index b011a1e2..c20b7162 100644 --- a/safe_relay_service/relay/services/notification_service.py +++ b/safe_relay_service/relay/services/notification_service.py @@ -51,6 +51,7 @@ def send_create_notification(self, safe_address: str, owners: List[str]) -> bool message = { "type": "safeCreation", "safe": safe_address, + "owners": owners, } return self.send_notification(message, owners) From 79171d69c1b16bc489a7af19683eec87aaec146e Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 7 Nov 2019 17:59:06 +0100 Subject: [PATCH 02/38] Stringify owners for `safeCreation` notification --- safe_relay_service/relay/services/notification_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_relay_service/relay/services/notification_service.py b/safe_relay_service/relay/services/notification_service.py index c20b7162..6362fee1 100644 --- a/safe_relay_service/relay/services/notification_service.py +++ b/safe_relay_service/relay/services/notification_service.py @@ -51,7 +51,7 @@ def send_create_notification(self, safe_address: str, owners: List[str]) -> bool message = { "type": "safeCreation", "safe": safe_address, - "owners": owners, + "owners": ','.join(owners), # Firebase just allows strings } return self.send_notification(message, owners) From 2c0b605ca3a78a559b082ebe69d02af364876011 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 12 Nov 2019 18:22:54 +0100 Subject: [PATCH 03/38] Optimize ERC20/721 indexing in batches --- .gitignore | 1 + .venv | 1 + .../relay/services/erc20_events_service.py | 9 +++++++++ .../relay/services/transaction_scan_service.py | 2 +- safe_relay_service/relay/services/transaction_service.py | 2 +- 5 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .venv diff --git a/.gitignore b/.gitignore index 34309cd3..c7275fec 100644 --- a/.gitignore +++ b/.gitignore @@ -71,5 +71,6 @@ typings/ .vscode venv/ +.venv config/settings/dev.py db.sqlite3 diff --git a/.venv b/.venv new file mode 100644 index 00000000..acf5ebe0 --- /dev/null +++ b/.venv @@ -0,0 +1 @@ +safe-services diff --git a/safe_relay_service/relay/services/erc20_events_service.py b/safe_relay_service/relay/services/erc20_events_service.py index c415397c..a98a8d32 100644 --- a/safe_relay_service/relay/services/erc20_events_service.py +++ b/safe_relay_service/relay/services/erc20_events_service.py @@ -26,6 +26,15 @@ class Erc20EventsService(TransactionScanService): """ Indexes ERC20 and ERC721 `Transfer` Event (as ERC721 has the same topic) """ + + def __init__(self, ethereum_client: EthereumClient, block_process_limit: int = 10000, + updated_blocks_behind: int = 300, query_chunk_size: int = 500, **kwargs): + super().__init__(ethereum_client, + block_process_limit=block_process_limit, + updated_blocks_behind=updated_blocks_behind, + query_chunk_size=query_chunk_size, + **kwargs) + @property def database_field(self): return 'erc_20_block_number' diff --git a/safe_relay_service/relay/services/transaction_scan_service.py b/safe_relay_service/relay/services/transaction_scan_service.py index 20e6a478..0316e6ce 100644 --- a/safe_relay_service/relay/services/transaction_scan_service.py +++ b/safe_relay_service/relay/services/transaction_scan_service.py @@ -17,7 +17,7 @@ class TransactionScanService(ABC): def __init__(self, ethereum_client: EthereumClient, confirmations: int = 10, - block_process_limit: int = 10000, updated_blocks_behind: int = 100, + block_process_limit: int = 10000, updated_blocks_behind: int = 300, query_chunk_size: int = 100, safe_creation_threshold: int = 150000): """ :param ethereum_client: diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index 58288170..814b320e 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -411,7 +411,7 @@ def _send_multisig_tx(self, if safe_tx.signers != safe_tx.sorted_signers: raise SignaturesNotSorted('Safe-tx-hash=%s - Signatures are not sorted by owner: %s' % - (safe_tx.safe_tx_hash, safe_tx.signers)) + (safe_tx.safe_tx_hash.hex(), safe_tx.signers)) safe_tx.call(tx_sender_address=tx_sender_address, block_identifier=block_identifier) From 0d3ee3d884219c0163f69deb263ee682a627ccb8 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 12 Nov 2019 18:27:08 +0100 Subject: [PATCH 04/38] Update dependencies --- requirements-test.txt | 2 +- requirements.txt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index cd53bc0a..9cf21283 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,5 @@ -r requirements.txt pytest==5.2.2 -pytest-django==3.6.0 +pytest-django==3.7.0 pytest-sugar==0.9.2 coverage==4.5.3 diff --git a/requirements.txt b/requirements.txt index 027df596..f4263f16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==2.2.6 +Django==2.2.7 cachetools==3.1.1 celery==4.3.0 django-authtools==1.7.0 @@ -16,11 +16,11 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.6.2 -gunicorn==19.9.0 +gnosis-py==1.6.3 +gunicorn==20.0.0 hexbytes==0.2.0 -lxml==4.2.5 -numpy==1.17.3 +lxml==4.4.1 +numpy==1.17.4 packaging>=19.0 psycopg2-binary==2.8.4 redis==3.3.11 From b6ff3a6077b0264d9615f073e9d8929c5526217a Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 13 Nov 2019 13:16:20 +0100 Subject: [PATCH 05/38] Optimize internalTx processing --- safe_relay_service/relay/services/erc20_events_service.py | 2 +- safe_relay_service/relay/services/internal_tx_service.py | 2 +- safe_relay_service/relay/services/transaction_scan_service.py | 4 ++-- safe_relay_service/tokens/tests/test_exchanges.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/safe_relay_service/relay/services/erc20_events_service.py b/safe_relay_service/relay/services/erc20_events_service.py index a98a8d32..ef2ef588 100644 --- a/safe_relay_service/relay/services/erc20_events_service.py +++ b/safe_relay_service/relay/services/erc20_events_service.py @@ -28,7 +28,7 @@ class Erc20EventsService(TransactionScanService): """ def __init__(self, ethereum_client: EthereumClient, block_process_limit: int = 10000, - updated_blocks_behind: int = 300, query_chunk_size: int = 500, **kwargs): + updated_blocks_behind: int = 200, query_chunk_size: int = 500, **kwargs): super().__init__(ethereum_client, block_process_limit=block_process_limit, updated_blocks_behind=updated_blocks_behind, diff --git a/safe_relay_service/relay/services/internal_tx_service.py b/safe_relay_service/relay/services/internal_tx_service.py index 17b6c96f..57c61427 100644 --- a/safe_relay_service/relay/services/internal_tx_service.py +++ b/safe_relay_service/relay/services/internal_tx_service.py @@ -3,7 +3,7 @@ from gnosis.eth import EthereumClient -from ..models import EthereumTxCallType, EthereumTxType, InternalTx +from ..models import InternalTx from .transaction_scan_service import TransactionScanService logger = getLogger(__name__) diff --git a/safe_relay_service/relay/services/transaction_scan_service.py b/safe_relay_service/relay/services/transaction_scan_service.py index 0316e6ce..b0c698d6 100644 --- a/safe_relay_service/relay/services/transaction_scan_service.py +++ b/safe_relay_service/relay/services/transaction_scan_service.py @@ -17,8 +17,8 @@ class TransactionScanService(ABC): def __init__(self, ethereum_client: EthereumClient, confirmations: int = 10, - block_process_limit: int = 10000, updated_blocks_behind: int = 300, - query_chunk_size: int = 100, safe_creation_threshold: int = 150000): + block_process_limit: int = 10000, updated_blocks_behind: int = 100, + query_chunk_size: int = 500, safe_creation_threshold: int = 150000): """ :param ethereum_client: :param confirmations: Threshold of blocks to scan to prevent reorgs diff --git a/safe_relay_service/tokens/tests/test_exchanges.py b/safe_relay_service/tokens/tests/test_exchanges.py index be3e0bdd..be87b91d 100644 --- a/safe_relay_service/tokens/tests/test_exchanges.py +++ b/safe_relay_service/tokens/tests/test_exchanges.py @@ -40,8 +40,8 @@ def test_dutchx(self): # Dai address is 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 # Gno address is 0x6810e776880C02933D47DB1b9fc05908e5386b96 self.exchange_helper(exchange, - [# 'WETH-0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359-WETH', + [# 'WETH-0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', # DAI + # '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359-WETH', # DAI # 'WETH-0x6810e776880C02933D47DB1b9fc05908e5386b96' '0x6810e776880C02933D47DB1b9fc05908e5386b96-WETH', ], From 501edf29f7d5081c7ec065f387e9aeaf676c66b8 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 19 Nov 2019 13:20:03 +0100 Subject: [PATCH 06/38] Just autodeploy Safes created in last 10 days --- config/settings/base.py | 2 ++ safe_relay_service/relay/tasks.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index d0e326fe..63465021 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -190,6 +190,8 @@ CELERY_TASK_SERIALIZER = 'json' # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer CELERY_RESULT_SERIALIZER = 'json' +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_ignore_result +CELERY_TASK_IGNORE_RESULT = True # Django REST Framework # ------------------------------------------------------------------------------ diff --git a/safe_relay_service/relay/tasks.py b/safe_relay_service/relay/tasks.py index ac5a4759..48c70ac2 100644 --- a/safe_relay_service/relay/tasks.py +++ b/safe_relay_service/relay/tasks.py @@ -300,7 +300,8 @@ def check_create2_deployed_safes_task() -> None: safe_creation2.save() deploy_create2_safe_task.delay(safe_address, retry=False) - for safe_creation2 in SafeCreation2.objects.not_deployed(): + for safe_creation2 in SafeCreation2.objects.not_deployed().filter( + created__gte=timezone.now() - timedelta(days=10)): deploy_create2_safe_task.delay(safe_creation2.safe.address, retry=False) except LockError: pass From 6060d4ac7c4e753cb257b57afbadc7ef60c4e45a Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 19 Nov 2019 13:20:53 +0100 Subject: [PATCH 07/38] Set version 3.6.10 --- safe_relay_service/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_relay_service/version.py b/safe_relay_service/version.py index 65f069d3..627984e5 100644 --- a/safe_relay_service/version.py +++ b/safe_relay_service/version.py @@ -1,2 +1,2 @@ -__version__ = '3.6.9' +__version__ = '3.6.10' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) From 1c7b7a94974f527523f79ce944c02971d35b5256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ux=C3=ADo?= Date: Thu, 21 Nov 2019 17:00:25 +0100 Subject: [PATCH 08/38] Support Safe contracts V1.1.0 (#168) - Add new v3 endpoints with tests - Closes #160 - Closes #161 - Deprecate old CREATE endpoints - Remove not used methods - Remove venv - Update gnosis-py --- .env | 2 +- .env_local | 2 +- .venv | 1 - config/settings/base.py | 20 +- config/settings/test.py | 3 - config/urls.py | 1 + requirements.txt | 2 +- .../commands/deploy_safe_contracts.py | 2 +- .../management/commands/test_relay_server.py | 2 +- .../relay/services/safe_creation_service.py | 101 ++------ .../relay/tests/relay_test_case.py | 9 - .../relay/tests/test_safe_creation_service.py | 36 --- safe_relay_service/relay/tests/test_tasks.py | 185 -------------- safe_relay_service/relay/tests/test_views.py | 231 +----------------- .../relay/tests/test_views_v2.py | 8 +- .../relay/tests/test_views_v3.py | 164 +++++++++++++ safe_relay_service/relay/urls.py | 2 - safe_relay_service/relay/urls_v3.py | 12 + safe_relay_service/relay/views.py | 26 +- safe_relay_service/relay/views_v2.py | 9 +- safe_relay_service/relay/views_v3.py | 76 ++++++ 21 files changed, 312 insertions(+), 582 deletions(-) delete mode 100644 .venv create mode 100644 safe_relay_service/relay/tests/test_views_v3.py create mode 100644 safe_relay_service/relay/urls_v3.py create mode 100644 safe_relay_service/relay/views_v3.py diff --git a/.env b/.env index 01811543..ae698c97 100644 --- a/.env +++ b/.env @@ -10,7 +10,7 @@ ETHEREUM_NODE_URL=http://ganache:8545 REDIS_URL=redis://redis/0 SAFE_CONTRACT_ADDRESS=0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab SAFE_FUNDER_PRIVATE_KEY=4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d -SAFE_OLD_CONTRACT_ADDRESS=0xCfEB869F69431e42cdB54A4F4f105C19C080A601 +SAFE_V1_0_0_CONTRACT_ADDRESS=0xCfEB869F69431e42cdB54A4F4f105C19C080A601 SAFE_PROXY_FACTORY_ADDRESS=0xC89Ce4735882C9F0f0FE26686c53074E09B0D550 SAFE_TX_SENDER_PRIVATE_KEY=6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1 DEPLOY_MASTER_COPY_ON_INIT=1 diff --git a/.env_local b/.env_local index 01c3027b..8e4507f0 100644 --- a/.env_local +++ b/.env_local @@ -10,7 +10,7 @@ ETHEREUM_NODE_URL=http://localhost:8545 REDIS_URL=redis://localhost/0 SAFE_CONTRACT_ADDRESS=0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A SAFE_FUNDER_PRIVATE_KEY=4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d -SAFE_OLD_CONTRACT_ADDRESS=0x8942595A2dC5181Df0465AF0D7be08c8f23C93af +SAFE_V1_0_0_CONTRACT_ADDRESS=0x8942595A2dC5181Df0465AF0D7be08c8f23C93af SAFE_PROXY_FACTORY_ADDRESS=0x12302fE9c02ff50939BaAaaf415fc226C078613C SAFE_TX_SENDER_PRIVATE_KEY=6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1 DEPLOY_MASTER_COPY_ON_INIT=0 diff --git a/.venv b/.venv deleted file mode 100644 index acf5ebe0..00000000 --- a/.venv +++ /dev/null @@ -1 +0,0 @@ -safe-services diff --git a/config/settings/base.py b/config/settings/base.py index 63465021..8654a21e 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -296,11 +296,23 @@ SAFE_FUNDER_MAX_ETH = env.int('SAFE_FUNDER_MAX_ETH', default=0.1) SAFE_FUNDING_CONFIRMATIONS = env.int('SAFE_FUNDING_CONFIRMATIONS', default=0) # Set to at least 3 # Master Copy Address of Safe Contract -SAFE_CONTRACT_ADDRESS = env('SAFE_CONTRACT_ADDRESS', default='0x' + '0' * 39 + '1') -SAFE_OLD_CONTRACT_ADDRESS = env('SAFE_OLD_CONTRACT_ADDRESS', default='0x' + '0' * 39 + '1') +SAFE_CONTRACT_ADDRESS = env('SAFE_CONTRACT_ADDRESS', default='0xaE32496491b53841efb51829d6f886387708F99B') +SAFE_V1_0_0_CONTRACT_ADDRESS = env('SAFE_V1_0_0_CONTRACT_ADDRESS', default='0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A') +SAFE_V0_0_1_CONTRACT_ADDRESS = env('SAFE_V0_0_1_CONTRACT_ADDRESS', default='0x8942595A2dC5181Df0465AF0D7be08c8f23C93af') SAFE_VALID_CONTRACT_ADDRESSES = set(env.list('SAFE_VALID_CONTRACT_ADDRESSES', - default=[])) | {SAFE_CONTRACT_ADDRESS, SAFE_OLD_CONTRACT_ADDRESS} -SAFE_PROXY_FACTORY_ADDRESS = env('SAFE_PROXY_FACTORY_ADDRESS', default='0x' + '0' * 39 + '2') + default=['0xaE32496491b53841efb51829d6f886387708F99B', + '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A' + '0x8942595A2dC5181Df0465AF0D7be08c8f23C93af', + '0xAC6072986E985aaBE7804695EC2d8970Cf7541A2']) + ) | {SAFE_CONTRACT_ADDRESS, + SAFE_V1_0_0_CONTRACT_ADDRESS, + SAFE_V0_0_1_CONTRACT_ADDRESS} +SAFE_PROXY_FACTORY_ADDRESS = env('SAFE_PROXY_FACTORY_ADDRESS', default='0x50e55Af101C777bA7A1d560a774A82eF002ced9F') +SAFE_PROXY_FACTORY_V1_0_0_ADDRESS = env('SAFE_PROXY_FACTORY_V1_0_0_ADDRESS', + default='0x12302fE9c02ff50939BaAaaf415fc226C078613C') +SAFE_DEFAULT_CALLBACK_HANDLER = env('SAFE_DEFAULT_CALLBACK_HANDLER', + default='0x40A930851BD2e590Bd5A5C981b436de25742E980') + # If FIXED_GAS_PRICE is None, GasStation will be used FIXED_GAS_PRICE = env.int('FIXED_GAS_PRICE', default=None) SAFE_TX_SENDER_PRIVATE_KEY = env('SAFE_TX_SENDER_PRIVATE_KEY', default=None) diff --git a/config/settings/test.py b/config/settings/test.py index f66bcf08..41c47882 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -49,6 +49,3 @@ SAFE_TX_SENDER_PRIVATE_KEY = '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c' SAFE_FUNDING_CONFIRMATIONS = 0 FIXED_GAS_PRICE = 1 -SAFE_CONTRACT_ADDRESS = '0x2727D69C0BD14B1dDd28371B8D97e808aDc1C2f7' -SAFE_OLD_CONTRACT_ADDRESS = '0x8942595A2dC5181Df0465AF0D7be08c8f23C93af' -SAFE_PROXY_FACTORY_ADDRESS = '0x3327d69c0bd14B1DDD28371B8D97E808Adc1c2F7' diff --git a/config/urls.py b/config/urls.py index 31c68ddc..b83d7a26 100644 --- a/config/urls.py +++ b/config/urls.py @@ -29,6 +29,7 @@ url(settings.ADMIN_URL, admin.site.urls), url(r'^api/v1/', include('safe_relay_service.relay.urls', namespace='v1')), url(r'^api/v2/', include('safe_relay_service.relay.urls_v2', namespace='v2')), + url(r'^api/v3/', include('safe_relay_service.relay.urls_v3', namespace='v3')), url(r'^check/', lambda request: HttpResponse("Ok"), name='check'), ] diff --git a/requirements.txt b/requirements.txt index f4263f16..a83c2896 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.6.3 +gnosis-py==1.7.2 gunicorn==20.0.0 hexbytes==0.2.0 lxml==4.4.1 diff --git a/safe_relay_service/relay/management/commands/deploy_safe_contracts.py b/safe_relay_service/relay/management/commands/deploy_safe_contracts.py index 027e2ff8..c627dfba 100644 --- a/safe_relay_service/relay/management/commands/deploy_safe_contracts.py +++ b/safe_relay_service/relay/management/commands/deploy_safe_contracts.py @@ -24,7 +24,7 @@ def handle(self, *args, **options): master_copies_with_deploy_fn = { settings.SAFE_CONTRACT_ADDRESS: Safe.deploy_master_contract, - settings.SAFE_OLD_CONTRACT_ADDRESS: Safe.deploy_old_master_contract, + settings.SAFE_V1_0_0_CONTRACT_ADDRESS: Safe.deploy_old_master_contract, settings.SAFE_PROXY_FACTORY_ADDRESS: ProxyFactory.deploy_proxy_factory_contract, } diff --git a/safe_relay_service/relay/management/commands/test_relay_server.py b/safe_relay_service/relay/management/commands/test_relay_server.py index 3ae48a51..64fc0f2e 100644 --- a/safe_relay_service/relay/management/commands/test_relay_server.py +++ b/safe_relay_service/relay/management/commands/test_relay_server.py @@ -87,7 +87,7 @@ def handle(self, *args, **options): if create2: master_copy_address = about_json['settings']['SAFE_CONTRACT_ADDRESS'] else: - master_copy_address = about_json['settings']['SAFE_OLD_CONTRACT_ADDRESS'] + master_copy_address = about_json['settings']['SAFE_V1_0_0_CONTRACT_ADDRESS'] accounts = [Account.create() for _ in range(3)] for account in accounts: diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index 93e146a1..1f0eb5f2 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -17,7 +17,7 @@ from safe_relay_service.tokens.models import Token from safe_relay_service.tokens.price_oracles import CannotGetTokenPriceFromApi -from ..models import (EthereumTx, SafeContract, SafeCreation, SafeCreation2, +from ..models import (EthereumTx, SafeContract, SafeCreation2, SafeTxStatus) from ..repositories.redis_repository import EthereumNonceLock, RedisRepository @@ -60,8 +60,27 @@ def __new__(cls): EthereumClientProvider(), RedisRepository().redis, settings.SAFE_CONTRACT_ADDRESS, - settings.SAFE_OLD_CONTRACT_ADDRESS, settings.SAFE_PROXY_FACTORY_ADDRESS, + settings.SAFE_DEFAULT_CALLBACK_HANDLER, + settings.SAFE_FUNDER_PRIVATE_KEY, + settings.SAFE_FIXED_CREATION_COST) + return cls.instance + + @classmethod + def del_singleton(cls): + if hasattr(cls, "instance"): + del cls.instance + + +class SafeCreationV1_0_0ServiceProvider: + def __new__(cls): + if not hasattr(cls, 'instance'): + cls.instance = SafeCreationService(GasStationProvider(), + EthereumClientProvider(), + RedisRepository().redis, + settings.SAFE_V1_0_0_CONTRACT_ADDRESS, + settings.SAFE_PROXY_FACTORY_V1_0_0_ADDRESS, + settings.SAFE_DEFAULT_CALLBACK_HANDLER, settings.SAFE_FUNDER_PRIVATE_KEY, settings.SAFE_FIXED_CREATION_COST) return cls.instance @@ -74,14 +93,14 @@ def del_singleton(cls): class SafeCreationService: def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, redis: Redis, - safe_contract_address: str, safe_old_contract_address: str, proxy_factory_address: str, + safe_contract_address: str, proxy_factory_address: str, default_callback_handler: str, safe_funder_private_key: str, safe_fixed_creation_cost: int): self.gas_station = gas_station self.ethereum_client = ethereum_client self.redis = redis self.safe_contract_address = safe_contract_address - self.safe_old_contract_address = safe_old_contract_address self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) + self.default_callback_handler = default_callback_handler self.funder_account = Account.privateKeyToAccount(safe_funder_private_key) self.safe_fixed_creation_cost = safe_fixed_creation_cost @@ -108,62 +127,6 @@ def _get_configured_gas_price(self) -> int: """ return self.gas_station.get_gas_prices().fast - def create_safe_tx(self, s: int, owners: List[str], threshold: int, - payment_token: Optional[str]) -> SafeCreation: - """ - Prepare creation tx for a new safe using classic CREATE method. Deprecated, it's recommended - to use `create2_safe_tx` - :param s: Random s value for ecdsa signature - :param owners: Owners of the new Safe - :param threshold: Minimum number of users required to operate the Safe - :param payment_token: Address of the payment token, if ether is not used - :rtype: SafeCreation - :raises: InvalidPaymentToken - """ - - payment_token = payment_token or NULL_ADDRESS - payment_token_eth_value = self._get_token_eth_value_or_raise(payment_token) - gas_price: int = self._get_configured_gas_price() - current_block_number = self.ethereum_client.current_block_number - - logger.debug('Building safe creation tx with gas price %d' % gas_price) - safe_creation_tx = Safe.build_safe_creation_tx(self.ethereum_client, self.safe_old_contract_address, - s, owners, threshold, gas_price, payment_token, - self.funder_account.address, - payment_token_eth_value=payment_token_eth_value, - fixed_creation_cost=self.safe_fixed_creation_cost) - - safe_contract = SafeContract.objects.create( - address=safe_creation_tx.safe_address, - master_copy=safe_creation_tx.master_copy - ) - - # Enable tx and erc20 tracing - SafeTxStatus.objects.create(safe=safe_contract, - initial_block_number=current_block_number, - tx_block_number=current_block_number, - erc_20_block_number=current_block_number) - - return SafeCreation.objects.create( - deployer=safe_creation_tx.deployer_address, - safe=safe_contract, - master_copy=safe_creation_tx.master_copy, - funder=safe_creation_tx.funder, - owners=owners, - threshold=threshold, - payment=safe_creation_tx.payment, - tx_hash=safe_creation_tx.tx_hash.hex(), - gas=safe_creation_tx.gas, - gas_price=safe_creation_tx.gas_price, - payment_token=None if safe_creation_tx.payment_token == NULL_ADDRESS else safe_creation_tx.payment_token, - value=safe_creation_tx.tx_pyethereum.value, - v=safe_creation_tx.v, - r=safe_creation_tx.r, - s=safe_creation_tx.s, - data=safe_creation_tx.tx_pyethereum.data, - signed_tx=safe_creation_tx.tx_raw - ) - def create2_safe_tx(self, salt_nonce: int, owners: Iterable[str], threshold: int, payment_token: Optional[str]) -> SafeCreation2: """ @@ -184,6 +147,7 @@ def create2_safe_tx(self, salt_nonce: int, owners: Iterable[str], threshold: int safe_creation_tx = Safe.build_safe_create2_tx(self.ethereum_client, self.safe_contract_address, self.proxy_factory.address, salt_nonce, owners, threshold, gas_price, payment_token, + fallback_handler=self.default_callback_handler, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=self.safe_fixed_creation_cost) @@ -265,22 +229,6 @@ def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: logger.info('Deployed safe=%s with tx-hash=%s', safe_address, ethereum_tx_sent.tx_hash.hex()) return safe_creation2 - def estimate_safe_creation(self, number_owners: int, payment_token: Optional[str] = None) -> SafeCreationEstimate: - """ - :param number_owners: - :param payment_token: - :return: - :raises: InvalidPaymentToken - """ - payment_token = payment_token or NULL_ADDRESS - payment_token_eth_value = self._get_token_eth_value_or_raise(payment_token) - gas_price = self._get_configured_gas_price() - fixed_creation_cost = self.safe_fixed_creation_cost - return Safe.estimate_safe_creation(self.ethereum_client, - self.safe_old_contract_address, number_owners, gas_price, payment_token, - payment_token_eth_value=payment_token_eth_value, - fixed_creation_cost=fixed_creation_cost) - def estimate_safe_creation2(self, number_owners: int, payment_token: Optional[str] = None) -> SafeCreationEstimate: """ :param number_owners: @@ -295,6 +243,7 @@ def estimate_safe_creation2(self, number_owners: int, payment_token: Optional[st return Safe.estimate_safe_creation_2(self.ethereum_client, self.safe_contract_address, self.proxy_factory.address, number_owners, gas_price, payment_token, + fallback_handler=self.default_callback_handler, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=fixed_creation_cost) diff --git a/safe_relay_service/relay/tests/relay_test_case.py b/safe_relay_service/relay/tests/relay_test_case.py index fda39f05..68e54cd1 100644 --- a/safe_relay_service/relay/tests/relay_test_case.py +++ b/safe_relay_service/relay/tests/relay_test_case.py @@ -24,15 +24,6 @@ def setUpTestData(cls): cls.safe_creation_service = SafeCreationServiceProvider() cls.transaction_service = TransactionServiceProvider() - def create_test_safe_in_db(self, owners=None, number_owners=3, threshold=None, - payment_token=None) -> SafeCreation: - s = generate_valid_s() - owners = owners or [Account.create().address for _ in range(number_owners)] - threshold = threshold if threshold else len(owners) - - safe_creation = self.safe_creation_service.create_safe_tx(s, owners, threshold, payment_token) - return safe_creation - def create2_test_safe_in_db(self, owners=None, number_owners=3, threshold=None, payment_token=None, salt_nonce=None) -> SafeCreation2: diff --git a/safe_relay_service/relay/tests/test_safe_creation_service.py b/safe_relay_service/relay/tests/test_safe_creation_service.py index d3f9f6cf..43b24192 100644 --- a/safe_relay_service/relay/tests/test_safe_creation_service.py +++ b/safe_relay_service/relay/tests/test_safe_creation_service.py @@ -49,42 +49,6 @@ def test_deploy_create2_safe_tx(self): def test_creation_service_provider_singleton(self): self.assertEqual(SafeCreationServiceProvider(), SafeCreationServiceProvider()) - def test_estimate_safe_creation(self): - gas_price = self.safe_creation_service._get_configured_gas_price() - - number_owners = 4 - payment_token = None - safe_creation_estimate = self.safe_creation_service.estimate_safe_creation(number_owners, payment_token) - self.assertGreater(safe_creation_estimate.gas, 0) - self.assertEqual(safe_creation_estimate.gas_price, gas_price) - self.assertGreater(safe_creation_estimate.payment, 0) - self.assertEqual(safe_creation_estimate.payment_token, NULL_ADDRESS) - estimated_payment = safe_creation_estimate.payment - - number_owners = 8 - payment_token = None - safe_creation_estimate = self.safe_creation_service.estimate_safe_creation(number_owners, payment_token) - self.assertGreater(safe_creation_estimate.gas, 0) - self.assertEqual(safe_creation_estimate.gas_price, gas_price) - self.assertGreater(safe_creation_estimate.payment, estimated_payment) - self.assertEqual(safe_creation_estimate.payment_token, NULL_ADDRESS) - - payment_token = get_eth_address_with_key()[0] - with self.assertRaisesMessage(InvalidPaymentToken, payment_token): - self.safe_creation_service.estimate_safe_creation(number_owners, payment_token) - - number_tokens = 1000 - owner = Account.create() - erc20 = self.deploy_example_erc20(number_tokens, owner.address) - number_owners = 4 - payment_token = erc20.address - payment_token_db = TokenFactory(address=payment_token, fixed_eth_conversion=0.1) - safe_creation_estimate = self.safe_creation_service.estimate_safe_creation(number_owners, payment_token) - self.assertGreater(safe_creation_estimate.gas, 0) - self.assertEqual(safe_creation_estimate.gas_price, gas_price) - self.assertGreater(safe_creation_estimate.payment, estimated_payment) - self.assertEqual(safe_creation_estimate.payment_token, payment_token) - def test_estimate_safe_creation2(self): gas_price = self.safe_creation_service._get_configured_gas_price() diff --git a/safe_relay_service/relay/tests/test_tasks.py b/safe_relay_service/relay/tests/test_tasks.py index c46a4d46..34b4b0c9 100644 --- a/safe_relay_service/relay/tests/test_tasks.py +++ b/safe_relay_service/relay/tests/test_tasks.py @@ -23,191 +23,6 @@ class TestTasks(RelayTestCaseMixin, TestCase): - def test_balance_in_deployer(self): - safe_creation = self.create_test_safe_in_db() - safe, deployer, payment = safe_creation.safe.address, safe_creation.deployer, safe_creation.payment - - self.send_ether(to=safe, value=payment) - - # If deployer has balance already no ether is sent to the account - deployer_payment = 1 - self.send_ether(to=deployer, value=deployer_payment) - - self.assertEqual(self.ethereum_client.get_balance(deployer), deployer_payment) - - fund_deployer_task.delay(safe, retry=False).get() - - self.assertEqual(self.ethereum_client.get_balance(deployer), deployer_payment) - - def test_deploy_safe(self): - safe_creation = self.create_test_safe_in_db() - safe, deployer, payment = safe_creation.safe.address, safe_creation.deployer, safe_creation.payment - - self.send_ether(to=safe, value=payment) - - fund_deployer_task.delay(safe).get() - - safe_funding = SafeFunding.objects.get(safe=safe) - - self.assertTrue(safe_funding.deployer_funded) - self.assertTrue(safe_funding.safe_funded) - self.assertFalse(safe_funding.safe_deployed) - self.assertIsNone(safe_funding.safe_deployed_tx_hash) - - # Safe code is not deployed - self.assertEqual(self.w3.eth.getCode(safe), b'') - - # This task will check safes with deployer funded and confirmed and send safe raw contract creation tx - deploy_safes_task.delay().get() - - safe_funding.refresh_from_db() - self.assertTrue(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - # This time task will check the tx_hash for the safe - deploy_safes_task.delay().get() - - safe_funding.refresh_from_db() - self.assertTrue(safe_funding.safe_deployed_tx_hash) - self.assertTrue(safe_funding.safe_deployed) - - # Safe code is deployed - self.assertTrue(len(self.w3.eth.getCode(safe)) > 10) - - # Nothing happens if safe is funded - fund_deployer_task.delay(safe).get() - - # Check deployer tx is checked again - old_deployer_tx_hash = safe_funding.deployer_funded_tx_hash - safe_funding.deployer_funded = False - safe_funding.save() - fund_deployer_task.delay(safe).get() - - safe_funding.refresh_from_db() - self.assertEqual(old_deployer_tx_hash, safe_funding.deployer_funded_tx_hash) - self.assertTrue(safe_funding.deployer_funded) - - def test_safe_with_no_funds(self): - safe_creation = self.create_test_safe_in_db() - safe, deployer, payment = safe_creation.safe.address, safe_creation.deployer, safe_creation.payment - - self.assertEqual(self.ethereum_client.get_balance(deployer), 0) - # No ether is sent to the deployer is safe is empty - fund_deployer_task.delay(safe, retry=False).get() - self.assertEqual(self.ethereum_client.get_balance(deployer), 0) - - # No ether is sent to the deployer is safe has less balance than needed - self.send_ether(to=safe, value=payment - 1) - fund_deployer_task.delay(safe, retry=False).get() - self.assertEqual(self.ethereum_client.get_balance(deployer), 0) - - def test_check_deployer_funded(self): - safe_creation = self.create_test_safe_in_db() - safe, deployer, payment = safe_creation.safe.address, safe_creation.deployer, safe_creation.payment - - safe_contract = SafeContract.objects.get(address=safe) - safe_funding = SafeFunding.objects.create(safe=safe_contract) - - safe_funding.safe_funded = True - safe_funding.deployer_funded_tx_hash = self.w3.sha3(0).hex() - safe_funding.save() - - # If tx hash is not found should be deleted from database - check_deployer_funded_task.delay(safe, retry=False).get() - - safe_funding.refresh_from_db() - self.assertFalse(safe_funding.deployer_funded_tx_hash) - - def test_reorg_before_safe_deploy(self): - safe_creation = self.create_test_safe_in_db() - safe, deployer, payment = safe_creation.safe.address, safe_creation.deployer, safe_creation.payment - - self.send_ether(to=safe, value=payment) - - fund_deployer_task.delay(safe).get() - check_deployer_funded_task.delay(safe).get() - - safe_funding = SafeFunding.objects.get(safe=safe) - - self.assertTrue(safe_funding.safe_funded) - self.assertTrue(safe_funding.deployer_funded_tx_hash) - self.assertTrue(safe_funding.deployer_funded) - self.assertFalse(safe_funding.safe_deployed) - - # Set invalid tx_hash for deployer funding tx - safe_funding.deployer_funded_tx_hash = self.w3.sha3(0).hex() - safe_funding.save() - - deploy_safes_task.delay(retry=False).get() - - safe_funding.refresh_from_db() - - # Safe is deployed even if deployer tx is not valid, if balance is found - self.assertTrue(safe_funding.safe_funded) - self.assertTrue(safe_funding.deployer_funded_tx_hash) - self.assertTrue(safe_funding.deployer_funded) - self.assertTrue(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - # Try to deploy safe with no balance and invalid tx-hash. It will not be deployed and - # `deployer_funded` will be set to `False` and `deployer_funded_tx_hash` to `None` - safe_creation = SafeCreationFactory() - safe_funding = SafeFundingFactory(safe=safe_creation.safe, - safe_funded=True, - deployer_funded=True, - deployer_funded_tx_hash=self.w3.sha3(1).hex()) - - self.assertTrue(safe_funding.safe_funded) - self.assertTrue(safe_funding.deployer_funded) - self.assertTrue(safe_funding.deployer_funded_tx_hash) - self.assertFalse(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - deploy_safes_task.delay(retry=False).get() - safe_funding.refresh_from_db() - self.assertTrue(safe_funding.safe_funded) - self.assertFalse(safe_funding.deployer_funded) - self.assertFalse(safe_funding.deployer_funded_tx_hash) - self.assertFalse(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - def test_reorg_after_safe_deployed(self): - safe_creation = self.create_test_safe_in_db() - safe, deployer, payment = safe_creation.safe.address, safe_creation.deployer, safe_creation.payment - - self.send_ether(to=safe, value=payment) - - fund_deployer_task.delay(safe).get() - check_deployer_funded_task.delay(safe).get() - deploy_safes_task.delay().get() - - safe_funding = SafeFunding.objects.get(safe=safe) - - self.assertTrue(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - # Set an invalid tx - safe_funding.safe_deployed_tx_hash = self.w3.sha3(0).hex() - safe_funding.save() - - # If tx is not found before 10 minutes, nothing should happen - deploy_safes_task.delay().get() - - safe_funding.refresh_from_db() - self.assertTrue(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - # If tx is not found after 10 minutes, safe will be marked to deploy again - SafeFunding.objects.update(modified=timezone.now() - timedelta(minutes=11)) - deploy_safes_task.delay().get() - - safe_funding.refresh_from_db() - self.assertFalse(safe_funding.safe_deployed_tx_hash) - self.assertFalse(safe_funding.safe_deployed) - - # No error when trying to deploy again the contract - deploy_safes_task.delay().get() - def test_deploy_create2_safe_task(self): safe_creation2 = self.create2_test_safe_in_db() diff --git a/safe_relay_service/relay/tests/test_views.py b/safe_relay_service/relay/tests/test_views.py index d38b576f..42563883 100644 --- a/safe_relay_service/relay/tests/test_views.py +++ b/safe_relay_service/relay/tests/test_views.py @@ -96,218 +96,6 @@ def test_safe_balances(self): self.assertCountEqual(response.json(), [{'tokenAddress': None, 'balance': str(value)}, {'tokenAddress': erc20.address, 'balance': str(tokens_value)}]) - def test_safe_creation(self): - s = generate_valid_s() - owner1, _ = get_eth_address_with_key() - owner2, _ = get_eth_address_with_key() - serializer = SafeCreationSerializer(data={ - 's': s, - 'owners': [owner1, owner2], - 'threshold': 2 - }) - self.assertTrue(serializer.is_valid()) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - response_json = response.json() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - deployer = response_json['deployer'] - self.assertTrue(check_checksum(deployer)) - self.assertTrue(check_checksum(response_json['safe'])) - self.assertTrue(check_checksum(response_json['funder'])) - self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) - self.assertGreater(int(response_json['payment']), 0) - - self.assertTrue(SafeContract.objects.filter(address=response.data['safe'])) - self.assertTrue(SafeCreation.objects.filter(owners__contains=[owner1])) - safe_creation = SafeCreation.objects.get(deployer=deployer) - self.assertEqual(safe_creation.payment_token, None) - # Payment includes deployment gas + gas to send eth to the deployer - self.assertGreater(safe_creation.payment, safe_creation.wei_deploy_cost()) - - serializer = SafeCreationSerializer(data={ - 's': -1, - 'owners': [owner1, owner2], - 'threshold': 2 - }) - self.assertFalse(serializer.is_valid()) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - - def test_safe_creation_with_fixed_cost(self): - s = generate_valid_s() - owner1, _ = get_eth_address_with_key() - owner2, _ = get_eth_address_with_key() - serializer = SafeCreationSerializer(data={ - 's': s, - 'owners': [owner1, owner2], - 'threshold': 2 - }) - self.assertTrue(serializer.is_valid()) - fixed_creation_cost = 123 - with self.settings(SAFE_FIXED_CREATION_COST=fixed_creation_cost): - SafeCreationServiceProvider.del_singleton() - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - response_json = response.json() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - deployer = response_json['deployer'] - self.assertTrue(check_checksum(deployer)) - self.assertTrue(check_checksum(response_json['safe'])) - self.assertTrue(check_checksum(response_json['funder'])) - self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) - self.assertEqual(int(response_json['payment']), fixed_creation_cost) - - safe_creation = SafeCreation.objects.get(deployer=deployer) - self.assertEqual(safe_creation.payment_token, None) - self.assertEqual(safe_creation.payment, fixed_creation_cost) - self.assertGreater(safe_creation.wei_deploy_cost(), safe_creation.payment) - SafeCreationServiceProvider.del_singleton() - - def test_safe_creation_with_payment_token(self): - s = generate_valid_s() - owner1, _ = get_eth_address_with_key() - owner2, _ = get_eth_address_with_key() - payment_token, _ = get_eth_address_with_key() - serializer = SafeCreationSerializer(data={ - 's': s, - 'owners': [owner1, owner2], - 'threshold': 2, - 'payment_token': payment_token, - }) - self.assertTrue(serializer.is_valid()) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - response_json = response.json() - self.assertIn('InvalidPaymentToken', response_json['exception']) - self.assertIn(payment_token, response_json['exception']) - - # With previous versions of ganache it failed, because token was on DB but not in blockchain, - # so gas cannot be estimated. With new versions of ganache estimation is working - token_model = TokenFactory(address=payment_token, fixed_eth_conversion=0.1) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - erc20_contract = self.deploy_example_erc20(10000, NULL_ADDRESS) - payment_token = erc20_contract.address - serializer = SafeCreationSerializer(data={ - 's': s, - 'owners': [owner1, owner2], - 'threshold': 2, - 'payment_token': payment_token, - }) - self.assertTrue(serializer.is_valid()) - token_model = TokenFactory(address=payment_token, fixed_eth_conversion=0.1) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response_json = response.json() - deployer = response_json['deployer'] - self.assertTrue(check_checksum(deployer)) - self.assertTrue(check_checksum(response_json['safe'])) - self.assertEqual(response_json['paymentToken'], payment_token) - - self.assertTrue(SafeContract.objects.filter(address=response.data['safe'])) - safe_creation = SafeCreation.objects.get(deployer=deployer) - self.assertIn(owner1, safe_creation.owners) - self.assertEqual(safe_creation.payment_token, payment_token) - self.assertGreater(safe_creation.payment, safe_creation.wei_deploy_cost()) - - # Check that payment is more than with ether - token_payment = response_json['payment'] - serializer = SafeCreationSerializer(data={ - 's': s, - 'owners': [owner1, owner2], - 'threshold': 2, - }) - self.assertTrue(serializer.is_valid()) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response_json = response.json() - payment_using_ether = response_json['payment'] - self.assertGreater(token_payment, payment_using_ether) - - # Check that token with fixed conversion price to 1 is a little higher than with ether - # (We need to pay for storage for token transfer, as funder does not own any token yet) - erc20_contract = self.deploy_example_erc20(10000, NULL_ADDRESS) - payment_token = erc20_contract.address - token_model = TokenFactory(address=payment_token, fixed_eth_conversion=1) - serializer = SafeCreationSerializer(data={ - 's': s, - 'owners': [owner1, owner2], - 'threshold': 2, - 'payment_token': payment_token - }) - self.assertTrue(serializer.is_valid()) - response = self.client.post(reverse('v1:safe-creation'), data=serializer.data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response_json = response.json() - deployer = response_json['deployer'] - payment_using_token = response_json['payment'] - self.assertGreater(payment_using_token, payment_using_ether) - safe_creation = SafeCreation.objects.get(deployer=deployer) - # Payment includes also the gas to send ether to the safe deployer - self.assertGreater(safe_creation.payment, safe_creation.wei_deploy_cost()) - - def test_safe_creation_estimate(self): - data = { - 'number_owners': 4, - 'payment_token': None, - } - - response = self.client.post(reverse('v1:safe-creation-estimate'), data=data, format='json') - response_json = response.json() - for field in ['payment', 'gasPrice', 'gas']: - self.assertIn(field, response_json) - self.assertGreater(int(response_json[field]), 0) - estimated_payment = response_json['payment'] - - # With payment token - erc20_contract = self.deploy_example_erc20(10000, NULL_ADDRESS) - payment_token = erc20_contract.address - token_model = TokenFactory(address=payment_token, gas=True, fixed_eth_conversion=0.1) - data = { - 'number_owners': 4, - 'payment_token': payment_token, - } - - response = self.client.post(reverse('v1:safe-creation-estimate'), data=data, format='json') - response_json = response.json() - for field in ['payment', 'gasPrice', 'gas']: - self.assertIn(field, response_json) - self.assertGreater(int(response_json[field]), 0) - self.assertGreater(response_json['payment'], estimated_payment) - - def test_safe_view(self): - owners_with_keys = [get_eth_address_with_key(), - get_eth_address_with_key(), - get_eth_address_with_key()] - owners = [x[0] for x in owners_with_keys] - threshold = len(owners) - 1 - safe_creation = self.deploy_test_safe(owners=owners, threshold=threshold) - my_safe_address = safe_creation.safe_address - SafeContractFactory(address=my_safe_address) - SafeFundingFactory(safe=SafeContract.objects.get(address=my_safe_address), safe_deployed=True) - response = self.client.get(reverse('v1:safe', args=(my_safe_address,)), format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - safe_json = response.json() - self.assertEqual(safe_json['address'], my_safe_address) - self.assertEqual(safe_json['masterCopy'], self.safe_contract_address) - self.assertEqual(safe_json['nonce'], 0) - self.assertEqual(safe_json['threshold'], threshold) - self.assertEqual(safe_json['owners'], owners) - self.assertIn('version', safe_json) - - random_address, _ = get_eth_address_with_key() - response = self.client.get(reverse('v1:safe', args=(random_address,)), format='json') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - response = self.client.get(reverse('v1:safe', args=(my_safe_address + ' ',)), format='json') - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - - response = self.client.get(reverse('v1:safe', args=('0xabfG',)), format='json') - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - - response = self.client.get(reverse('v1:safe', args=('batman',)), format='json') - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - def test_safe_multisig_tx_post(self): # Create Safe ------------------------------------------------ w3 = self.ethereum_client.w3 @@ -585,7 +373,7 @@ def test_safe_multisig_tx_errors(self): format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - my_safe_address = self.create_test_safe_in_db().safe.address + my_safe_address = self.create2_test_safe_in_db().safe.address response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address,)), data={}, format='json') @@ -699,23 +487,6 @@ def test_safe_multisig_tx_estimates(self): self.assertAlmostEqual(int(estimation_token['gasPrice']), int(estimation_ether['gasPrice']) // 2, delta=1.0) self.assertEqual(estimation_token['gasToken'], valid_token.address) - def test_get_safe_signal(self): - safe_address, _ = get_eth_address_with_key() - - response = self.client.get(reverse('v1:safe-signal', args=(safe_address,))) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - invalid_address = get_eth_address_with_invalid_checksum() - - response = self.client.get(reverse('v1:safe-signal', args=(invalid_address,))) - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - - my_safe_address = self.create_test_safe_in_db().safe.address - response = self.client.post(reverse('v1:safe-multisig-txs', args=(my_safe_address,)), - data={}, - format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - def test_get_all_txs(self): safe_address = Account().create().address SafeContractFactory(address=safe_address) diff --git a/safe_relay_service/relay/tests/test_views_v2.py b/safe_relay_service/relay/tests/test_views_v2.py index c90b7b45..0333e99e 100644 --- a/safe_relay_service/relay/tests/test_views_v2.py +++ b/safe_relay_service/relay/tests/test_views_v2.py @@ -15,7 +15,7 @@ from safe_relay_service.tokens.tests.factories import TokenFactory from ..models import SafeContract, SafeCreation2 -from ..services.safe_creation_service import SafeCreationServiceProvider +from ..services.safe_creation_service import SafeCreationV1_0_0ServiceProvider from .factories import SafeContractFactory from .relay_test_case import RelayTestCaseMixin @@ -110,9 +110,9 @@ def test_safe_creation_with_fixed_cost(self): fixed_creation_cost = 123 with self.settings(SAFE_FIXED_CREATION_COST=fixed_creation_cost): - SafeCreationServiceProvider.del_singleton() + SafeCreationV1_0_0ServiceProvider.del_singleton() response = self.client.post(reverse('v2:safe-creation'), data, format='json') - SafeCreationServiceProvider.del_singleton() + SafeCreationV1_0_0ServiceProvider.del_singleton() self.assertEqual(response.status_code, status.HTTP_201_CREATED) response_json = response.json() @@ -120,7 +120,7 @@ def test_safe_creation_with_fixed_cost(self): self.assertTrue(check_checksum(safe_address)) self.assertTrue(check_checksum(response_json['paymentReceiver'])) self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) - self.assertEqual(response_json['payment'], '123') + self.assertEqual(response_json['payment'], str(fixed_creation_cost)) self.assertGreater(int(response_json['gasEstimated']), 0) self.assertGreater(int(response_json['gasPriceEstimated']), 0) self.assertGreater(len(response_json['setupData']), 2) diff --git a/safe_relay_service/relay/tests/test_views_v3.py b/safe_relay_service/relay/tests/test_views_v3.py new file mode 100644 index 00000000..623eaa9f --- /dev/null +++ b/safe_relay_service/relay/tests/test_views_v3.py @@ -0,0 +1,164 @@ +import logging + +from django.urls import reverse + +from eth_account import Account +from ethereum.utils import check_checksum +from faker import Faker +from rest_framework import status +from rest_framework.test import APITestCase + +from gnosis.eth.constants import NULL_ADDRESS +from gnosis.safe.tests.utils import generate_salt_nonce + +from safe_relay_service.tokens.tests.factories import TokenFactory + +from ..models import SafeContract, SafeCreation2 +from ..services.safe_creation_service import SafeCreationServiceProvider +from .relay_test_case import RelayTestCaseMixin + +faker = Faker() + +logger = logging.getLogger(__name__) + + +class TestViewsV3(RelayTestCaseMixin, APITestCase): + def test_safe_creation_estimate(self): + url = reverse('v3:safe-creation-estimates') + number_owners = 4 + data = { + 'numberOwners': number_owners + } + + response = self.client.post(url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + safe_creation_estimates = response.json() + self.assertEqual(len(safe_creation_estimates), 1) + safe_creation_estimate = safe_creation_estimates[0] + self.assertEqual(safe_creation_estimate['paymentToken'], NULL_ADDRESS) + + token = TokenFactory(gas=True, fixed_eth_conversion=None) + response = self.client.post(url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + safe_creation_estimates = response.json() + # No price oracles, so no estimation + self.assertEqual(len(safe_creation_estimates), 1) + + fixed_price_token = TokenFactory(gas=True, fixed_eth_conversion=1.0) + response = self.client.post(url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + safe_creation_estimates = response.json() + # Fixed price oracle, so estimation will work + self.assertEqual(len(safe_creation_estimates), 2) + safe_creation_estimate = safe_creation_estimates[1] + self.assertEqual(safe_creation_estimate['paymentToken'], fixed_price_token.address) + self.assertGreater(int(safe_creation_estimate['payment']), 0) + self.assertGreater(int(safe_creation_estimate['gasPrice']), 0) + self.assertGreater(int(safe_creation_estimate['gas']), 0) + + def test_safe_creation(self): + salt_nonce = generate_salt_nonce() + owners = [Account.create().address for _ in range(2)] + data = { + 'saltNonce': salt_nonce, + 'owners': owners, + 'threshold': len(owners) + } + response = self.client.post(reverse('v3:safe-creation'), data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + response_json = response.json() + safe_address = response_json['safe'] + self.assertTrue(check_checksum(safe_address)) + self.assertTrue(check_checksum(response_json['paymentReceiver'])) + self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) + self.assertEqual(int(response_json['payment']), + int(response_json['gasEstimated']) * int(response_json['gasPriceEstimated'])) + self.assertGreater(int(response_json['gasEstimated']), 0) + self.assertGreater(int(response_json['gasPriceEstimated']), 0) + self.assertGreater(len(response_json['setupData']), 2) + + self.assertTrue(SafeContract.objects.filter(address=safe_address)) + self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]])) + safe_creation = SafeCreation2.objects.get(safe=safe_address) + self.assertEqual(safe_creation.payment_token, None) + # Payment includes deployment gas + gas to send eth to the deployer + self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost()) + + # Test exception when same Safe is created + response = self.client.post(reverse('v3:safe-creation'), data, format='json') + self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) + self.assertIn('SafeAlreadyExistsException', response.json()['exception']) + + data = { + 'salt_nonce': -1, + 'owners': owners, + 'threshold': 2 + } + response = self.client.post(reverse('v3:safe-creation'), data, format='json') + self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) + + def test_safe_creation_with_fixed_cost(self): + salt_nonce = generate_salt_nonce() + owners = [Account.create().address for _ in range(2)] + data = { + 'saltNonce': salt_nonce, + 'owners': owners, + 'threshold': len(owners) + } + + fixed_creation_cost = 456 + with self.settings(SAFE_FIXED_CREATION_COST=fixed_creation_cost): + SafeCreationServiceProvider.del_singleton() + response = self.client.post(reverse('v3:safe-creation'), data, format='json') + SafeCreationServiceProvider.del_singleton() + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + response_json = response.json() + safe_address = response_json['safe'] + self.assertTrue(check_checksum(safe_address)) + self.assertTrue(check_checksum(response_json['paymentReceiver'])) + self.assertEqual(response_json['paymentToken'], NULL_ADDRESS) + self.assertEqual(response_json['payment'], str(fixed_creation_cost)) + self.assertGreater(int(response_json['gasEstimated']), 0) + self.assertGreater(int(response_json['gasPriceEstimated']), 0) + self.assertGreater(len(response_json['setupData']), 2) + + def test_safe_creation_with_payment_token(self): + salt_nonce = generate_salt_nonce() + owners = [Account.create().address for _ in range(2)] + payment_token = Account.create().address + data = { + 'saltNonce': salt_nonce, + 'owners': owners, + 'threshold': len(owners), + 'paymentToken': payment_token, + } + + response = self.client.post(reverse('v3:safe-creation'), data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) + response_json = response.json() + self.assertIn('InvalidPaymentToken', response_json['exception']) + self.assertIn(payment_token, response_json['exception']) + + fixed_eth_conversion = 0.1 + token_model = TokenFactory(address=payment_token, fixed_eth_conversion=fixed_eth_conversion) + response = self.client.post(reverse('v3:safe-creation'), data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + response_json = response.json() + safe_address = response_json['safe'] + self.assertTrue(check_checksum(safe_address)) + self.assertTrue(check_checksum(response_json['paymentReceiver'])) + self.assertEqual(response_json['paymentToken'], payment_token) + self.assertEqual(int(response_json['payment']), + int(response_json['gasEstimated']) * int(response_json['gasPriceEstimated']) * + (1 / fixed_eth_conversion)) + self.assertGreater(int(response_json['gasEstimated']), 0) + self.assertGreater(int(response_json['gasPriceEstimated']), 0) + self.assertGreater(len(response_json['setupData']), 2) + + self.assertTrue(SafeContract.objects.filter(address=safe_address)) + self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]])) + safe_creation = SafeCreation2.objects.get(safe=safe_address) + self.assertEqual(safe_creation.payment_token, payment_token) + # Payment includes deployment gas + gas to send eth to the deployer + self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost() * (1 / fixed_eth_conversion)) diff --git a/safe_relay_service/relay/urls.py b/safe_relay_service/relay/urls.py index ab546441..ec5bb084 100644 --- a/safe_relay_service/relay/urls.py +++ b/safe_relay_service/relay/urls.py @@ -19,8 +19,6 @@ path('gas-station/history/', GasStationHistoryView.as_view(), name='gas-station-history'), path('tokens/', TokensView.as_view(), name='tokens'), path('tokens//', TokenView.as_view(), name='tokens'), - path('safes/', views.SafeCreationView.as_view(), name='safe-creation'), - path('safes/estimate/', views.SafeCreationEstimateView.as_view(), name='safe-creation-estimate'), path('safes//', views.SafeView.as_view(), name='safe'), path('safes//balances/', views.SafeBalanceView.as_view(), name='safe-balances'), path('safes//funded/', views.SafeSignalView.as_view(), name='safe-signal'), diff --git a/safe_relay_service/relay/urls_v3.py b/safe_relay_service/relay/urls_v3.py new file mode 100644 index 00000000..5735fffe --- /dev/null +++ b/safe_relay_service/relay/urls_v3.py @@ -0,0 +1,12 @@ +from django.urls import path + +from . import views_v3 + +app_name = "safe" + +timestamp_regex = '\\d{4}[-]?\\d{1,2}[-]?\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}' + +urlpatterns = [ + path('safes/', views_v3.SafeCreationView.as_view(), name='safe-creation'), + path('safes/estimates/', views_v3.SafeCreationEstimateView.as_view(), name='safe-creation-estimates'), +] diff --git a/safe_relay_service/relay/views.py b/safe_relay_service/relay/views.py index 9dad4287..8b57f49f 100644 --- a/safe_relay_service/relay/views.py +++ b/safe_relay_service/relay/views.py @@ -97,13 +97,16 @@ def get(self, request, format=None): 'SAFE_CHECK_DEPLOYER_FUNDED_DELAY': settings.SAFE_CHECK_DEPLOYER_FUNDED_DELAY, 'SAFE_CHECK_DEPLOYER_FUNDED_RETRIES': settings.SAFE_CHECK_DEPLOYER_FUNDED_RETRIES, 'SAFE_CONTRACT_ADDRESS': settings.SAFE_CONTRACT_ADDRESS, + 'SAFE_DEFAULT_CALLBACK_HANDLER': settings.SAFE_DEFAULT_CALLBACK_HANDLER, 'SAFE_FIXED_CREATION_COST': settings.SAFE_FIXED_CREATION_COST, 'SAFE_FUNDER_MAX_ETH': settings.SAFE_FUNDER_MAX_ETH, 'SAFE_FUNDER_PUBLIC_KEY': safe_funder_public_key, 'SAFE_FUNDING_CONFIRMATIONS': settings.SAFE_FUNDING_CONFIRMATIONS, - 'SAFE_OLD_CONTRACT_ADDRESS': settings.SAFE_OLD_CONTRACT_ADDRESS, + 'SAFE_PROXY_FACTORY_V1_0_0_ADDRESS': settings.SAFE_PROXY_FACTORY_V1_0_0_ADDRESS, 'SAFE_PROXY_FACTORY_ADDRESS': settings.SAFE_PROXY_FACTORY_ADDRESS, 'SAFE_TX_SENDER_PUBLIC_KEY': safe_sender_public_key, + 'SAFE_V0_0_1_CONTRACT_ADDRESS': settings.SAFE_V0_0_1_CONTRACT_ADDRESS, + 'SAFE_V1_0_0_CONTRACT_ADDRESS': settings.SAFE_V1_0_0_CONTRACT_ADDRESS, 'SAFE_VALID_CONTRACT_ADDRESSES': settings.SAFE_VALID_CONTRACT_ADDRESSES, } } @@ -156,27 +159,6 @@ def post(self, request, *args, **kwargs): return Response(status=http_status, data=serializer.errors) -class SafeCreationEstimateView(CreateAPIView): - permission_classes = (AllowAny,) - serializer_class = SafeCreationEstimateSerializer - - @swagger_auto_schema(responses={201: SafeCreationEstimateResponseSerializer(), - 400: 'Invalid data', - 422: 'Cannot process data'}) - def post(self, request, *args, **kwargs): - """ - Estimates creation of a Safe - """ - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - number_owners, payment_token = serializer.data['number_owners'], serializer.data['payment_token'] - safe_creation_estimate = SafeCreationServiceProvider().estimate_safe_creation(number_owners, payment_token) - safe_creation_estimate_response_data = SafeCreationEstimateResponseSerializer(safe_creation_estimate) - return Response(status=status.HTTP_200_OK, data=safe_creation_estimate_response_data.data) - else: - return Response(status=status.HTTP_422_UNPROCESSABLE_ENTITY, data=serializer.errors) - - class SafeView(APIView): permission_classes = (AllowAny,) serializer_class = SafeResponseSerializer diff --git a/safe_relay_service/relay/views_v2.py b/safe_relay_service/relay/views_v2.py index 50285d94..2d0fa3e4 100644 --- a/safe_relay_service/relay/views_v2.py +++ b/safe_relay_service/relay/views_v2.py @@ -3,7 +3,7 @@ from drf_yasg.utils import swagger_auto_schema from hexbytes import HexBytes from rest_framework import status -from rest_framework.generics import CreateAPIView, ListAPIView +from rest_framework.generics import CreateAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView @@ -20,9 +20,8 @@ SafeCreationEstimateResponseSerializer, SafeCreationEstimateV2Serializer, SafeFunding2ResponseSerializer, - SafeMultisigEstimateTxResponseSerializer, SafeMultisigEstimateTxResponseV2Serializer) -from .services.safe_creation_service import SafeCreationServiceProvider +from .services.safe_creation_service import SafeCreationV1_0_0ServiceProvider from .tasks import deploy_create2_safe_task logger = getLogger(__name__) @@ -42,7 +41,7 @@ def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): number_owners = serializer.data['number_owners'] - safe_creation_estimates = SafeCreationServiceProvider().estimate_safe_creation_for_all_tokens(number_owners) + safe_creation_estimates = SafeCreationV1_0_0ServiceProvider().estimate_safe_creation_for_all_tokens(number_owners) safe_creation_estimate_response_data = SafeCreationEstimateResponseSerializer(safe_creation_estimates, many=True) return Response(status=status.HTTP_200_OK, data=safe_creation_estimate_response_data.data) @@ -67,7 +66,7 @@ def post(self, request, *args, **kwargs): serializer.data['threshold'], serializer.data['payment_token']) - safe_creation_service = SafeCreationServiceProvider() + safe_creation_service = SafeCreationV1_0_0ServiceProvider() safe_creation = safe_creation_service.create2_safe_tx(salt_nonce, owners, threshold, payment_token) safe_creation_response_data = SafeCreation2ResponseSerializer(data={ 'safe': safe_creation.safe.address, diff --git a/safe_relay_service/relay/views_v3.py b/safe_relay_service/relay/views_v3.py new file mode 100644 index 00000000..93c86daf --- /dev/null +++ b/safe_relay_service/relay/views_v3.py @@ -0,0 +1,76 @@ +from logging import getLogger + +from drf_yasg.utils import swagger_auto_schema +from hexbytes import HexBytes +from rest_framework import status +from rest_framework.generics import CreateAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from gnosis.eth.constants import NULL_ADDRESS +from .serializers import (SafeCreation2ResponseSerializer, + SafeCreation2Serializer, + SafeCreationEstimateResponseSerializer, + SafeCreationEstimateV2Serializer) +from .services.safe_creation_service import SafeCreationServiceProvider + +logger = getLogger(__name__) + + +class SafeCreationEstimateView(CreateAPIView): + permission_classes = (AllowAny,) + serializer_class = SafeCreationEstimateV2Serializer + + @swagger_auto_schema(responses={201: SafeCreationEstimateResponseSerializer(), + 400: 'Invalid data', + 422: 'Cannot process data'}) + def post(self, request, *args, **kwargs): + """ + Estimates creation of a Safe + """ + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + number_owners = serializer.data['number_owners'] + safe_creation_estimates = SafeCreationServiceProvider().estimate_safe_creation_for_all_tokens(number_owners) + safe_creation_estimate_response_data = SafeCreationEstimateResponseSerializer(safe_creation_estimates, + many=True) + return Response(status=status.HTTP_200_OK, data=safe_creation_estimate_response_data.data) + else: + return Response(status=status.HTTP_422_UNPROCESSABLE_ENTITY, data=serializer.errors) + + +class SafeCreationView(CreateAPIView): + permission_classes = (AllowAny,) + serializer_class = SafeCreation2Serializer + + @swagger_auto_schema(responses={201: SafeCreation2ResponseSerializer(), + 400: 'Invalid data', + 422: 'Cannot process data'}) + def post(self, request, *args, **kwargs): + """ + Begins creation of a Safe + """ + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + salt_nonce, owners, threshold, payment_token = (serializer.data['salt_nonce'], serializer.data['owners'], + serializer.data['threshold'], + serializer.data['payment_token']) + + safe_creation_service = SafeCreationServiceProvider() + safe_creation = safe_creation_service.create2_safe_tx(salt_nonce, owners, threshold, payment_token) + safe_creation_response_data = SafeCreation2ResponseSerializer(data={ + 'safe': safe_creation.safe.address, + 'master_copy': safe_creation.master_copy, + 'proxy_factory': safe_creation.proxy_factory, + 'payment': safe_creation.payment, + 'payment_token': safe_creation.payment_token or NULL_ADDRESS, + 'payment_receiver': safe_creation.payment_receiver or NULL_ADDRESS, + 'setup_data': HexBytes(safe_creation.setup_data).hex(), + 'gas_estimated': safe_creation.gas_estimated, + 'gas_price_estimated': safe_creation.gas_price_estimated, + }) + safe_creation_response_data.is_valid(raise_exception=True) + return Response(status=status.HTTP_201_CREATED, data=safe_creation_response_data.data) + else: + return Response(status=status.HTTP_422_UNPROCESSABLE_ENTITY, + data=serializer.errors) From 9c64b751d7371c90e8a5070cc941413063c84114 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 21 Nov 2019 17:52:48 +0100 Subject: [PATCH 09/38] Return fallback handler --- requirements.txt | 2 +- safe_relay_service/relay/serializers.py | 1 + safe_relay_service/relay/services/safe_creation_service.py | 4 +++- safe_relay_service/relay/tests/test_safe_creation_service.py | 5 ++++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a83c2896..588bcd01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.7.2 +gnosis-py==1.7.3 gunicorn==20.0.0 hexbytes==0.2.0 lxml==4.4.1 diff --git a/safe_relay_service/relay/serializers.py b/safe_relay_service/relay/serializers.py index 91834af8..fac2dc52 100644 --- a/safe_relay_service/relay/serializers.py +++ b/safe_relay_service/relay/serializers.py @@ -100,6 +100,7 @@ class SafeBalanceResponseSerializer(serializers.Serializer): class SafeResponseSerializer(serializers.Serializer): address = EthereumAddressField() master_copy = EthereumAddressField() + fallback_handler = EthereumAddressField() nonce = serializers.IntegerField(min_value=0) threshold = serializers.IntegerField(min_value=1) owners = serializers.ListField(child=EthereumAddressField(), min_length=1) diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index 1f0eb5f2..c82e3927 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -51,6 +51,7 @@ class SafeInfo(NamedTuple): owners: List[str] master_copy: str version: str + fallback_handler: str class SafeCreationServiceProvider: @@ -275,4 +276,5 @@ def retrieve_safe_info(self, address: str) -> SafeInfo: owners = safe.retrieve_owners() master_copy = safe.retrieve_master_copy_address() version = safe.retrieve_version() - return SafeInfo(address, nonce, threshold, owners, master_copy, version) + fallback_handler = safe.retrieve_fallback_handler() + return SafeInfo(address, nonce, threshold, owners, master_copy, version, fallback_handler) diff --git a/safe_relay_service/relay/tests/test_safe_creation_service.py b/safe_relay_service/relay/tests/test_safe_creation_service.py index 43b24192..31d0ed01 100644 --- a/safe_relay_service/relay/tests/test_safe_creation_service.py +++ b/safe_relay_service/relay/tests/test_safe_creation_service.py @@ -113,6 +113,9 @@ def test_retrieve_safe_info(self): self.safe_creation_service.retrieve_safe_info(fake_safe_address) threshold = 1 - safe_address = self.deploy_test_safe(threshold=threshold).safe_address + random_fallback_handler = Account.create().address + safe_address = self.deploy_test_safe(threshold=threshold, + fallback_handler=random_fallback_handler).safe_address safe_info = self.safe_creation_service.retrieve_safe_info(safe_address) self.assertEqual(safe_info.threshold, threshold) + self.assertEqual(safe_info.fallback_handler, random_fallback_handler) From 67cef1efd7e4dd50e6c018ad20bb2710ede35db7 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 22 Nov 2019 13:31:42 +0100 Subject: [PATCH 10/38] Add Cache-Control to GasStation endpoint - Closes #165 --- safe_relay_service/gas_station/views.py | 2 +- .../relay/management/commands/deploy_safe_contracts.py | 2 +- .../relay/services/safe_creation_service.py | 3 +-- safe_relay_service/relay/tests/test_views.py | 10 +++------- safe_relay_service/relay/views_v3.py | 1 + 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/safe_relay_service/gas_station/views.py b/safe_relay_service/gas_station/views.py index 8452f26e..4edd2dcc 100644 --- a/safe_relay_service/gas_station/views.py +++ b/safe_relay_service/gas_station/views.py @@ -32,7 +32,7 @@ def get(self, request, format=None): gas_station = GasStationProvider() gas_prices = gas_station.get_gas_prices() serializer = GasPriceSerializer(gas_prices) - return Response(serializer.data) + return Response(serializer.data, headers={'Cache-Control': f'max-age={60 * 4}'}) class GasStationHistoryView(ListAPIView): diff --git a/safe_relay_service/relay/management/commands/deploy_safe_contracts.py b/safe_relay_service/relay/management/commands/deploy_safe_contracts.py index c627dfba..1eec628f 100644 --- a/safe_relay_service/relay/management/commands/deploy_safe_contracts.py +++ b/safe_relay_service/relay/management/commands/deploy_safe_contracts.py @@ -4,7 +4,7 @@ from eth_account import Account from gnosis.eth import EthereumClientProvider -from gnosis.safe import Safe, ProxyFactory +from gnosis.safe import ProxyFactory, Safe class Command(BaseCommand): diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index c82e3927..98e2b276 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -17,8 +17,7 @@ from safe_relay_service.tokens.models import Token from safe_relay_service.tokens.price_oracles import CannotGetTokenPriceFromApi -from ..models import (EthereumTx, SafeContract, SafeCreation2, - SafeTxStatus) +from ..models import EthereumTx, SafeContract, SafeCreation2, SafeTxStatus from ..repositories.redis_repository import EthereumNonceLock, RedisRepository logger = getLogger(__name__) diff --git a/safe_relay_service/relay/tests/test_views.py b/safe_relay_service/relay/tests/test_views.py index 42563883..dff934c7 100644 --- a/safe_relay_service/relay/tests/test_views.py +++ b/safe_relay_service/relay/tests/test_views.py @@ -6,7 +6,6 @@ from django.utils import dateparse, timezone from eth_account import Account -from ethereum.utils import check_checksum from faker import Faker from rest_framework import status from rest_framework.test import APITestCase @@ -16,18 +15,14 @@ get_eth_address_with_key) from gnosis.safe import SafeOperation, SafeTx from gnosis.safe.signatures import signatures_to_bytes -from gnosis.safe.tests.utils import generate_valid_s from safe_relay_service.gas_station.tests.factories import GasPriceFactory from safe_relay_service.tokens.tests.factories import TokenFactory -from ..models import SafeContract, SafeCreation, SafeMultisigTx -from ..serializers import SafeCreationSerializer -from ..services.safe_creation_service import SafeCreationServiceProvider +from ..models import SafeMultisigTx from .factories import (EthereumEventFactory, EthereumTxFactory, InternalTxFactory, SafeContractFactory, - SafeCreation2Factory, SafeFundingFactory, - SafeMultisigTxFactory) + SafeCreation2Factory, SafeMultisigTxFactory) from .relay_test_case import RelayTestCaseMixin faker = Faker() @@ -43,6 +38,7 @@ def test_about(self): def test_gas_station(self): response = self.client.get(reverse('v1:gas-station')) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.has_header('Cache-Control')) def test_gas_station_history(self): response = self.client.get(reverse('v1:gas-station-history'), format='json') diff --git a/safe_relay_service/relay/views_v3.py b/safe_relay_service/relay/views_v3.py index 93c86daf..1a5e33b1 100644 --- a/safe_relay_service/relay/views_v3.py +++ b/safe_relay_service/relay/views_v3.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from gnosis.eth.constants import NULL_ADDRESS + from .serializers import (SafeCreation2ResponseSerializer, SafeCreation2Serializer, SafeCreationEstimateResponseSerializer, From 5c77d403b91f81d4fcacc710d314a64b56a000d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ux=C3=ADo?= Date: Thu, 5 Dec 2019 16:11:26 +0100 Subject: [PATCH 11/38] Implement Uniswap and Kyber oracles (#174) - Implement Uniswap as oracle - Implement Kyber as oracle - Add field to `Oracle` model to allow configuration --- requirements-test.txt | 2 +- requirements.txt | 6 +- safe_relay_service/tokens/admin.py | 2 +- .../0012_priceoracle_configuration.py | 31 +++++++ safe_relay_service/tokens/models.py | 6 +- safe_relay_service/tokens/price_oracles.py | 83 +++++++++++++++++++ .../tokens/tests/test_models.py | 2 +- 7 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 safe_relay_service/tokens/migrations/0012_priceoracle_configuration.py diff --git a/requirements-test.txt b/requirements-test.txt index 9cf21283..0b28cbd6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==5.2.2 +pytest==5.3.1 pytest-django==3.7.0 pytest-sugar==0.9.2 coverage==4.5.3 diff --git a/requirements.txt b/requirements.txt index 588bcd01..97e4da1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==2.2.7 +Django==2.2.8 cachetools==3.1.1 celery==4.3.0 django-authtools==1.7.0 @@ -16,8 +16,8 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.7.3 -gunicorn==20.0.0 +gnosis-py==1.7.6 +gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 numpy==1.17.4 diff --git a/safe_relay_service/tokens/admin.py b/safe_relay_service/tokens/admin.py index fdbc9f50..2398d412 100644 --- a/safe_relay_service/tokens/admin.py +++ b/safe_relay_service/tokens/admin.py @@ -6,7 +6,7 @@ @admin.register(PriceOracle) class PriceOracleAdmin(admin.ModelAdmin): - list_display = ('name', ) + list_display = ('name', 'configuration') ordering = ('name',) diff --git a/safe_relay_service/tokens/migrations/0012_priceoracle_configuration.py b/safe_relay_service/tokens/migrations/0012_priceoracle_configuration.py new file mode 100644 index 00000000..2f9a7fbe --- /dev/null +++ b/safe_relay_service/tokens/migrations/0012_priceoracle_configuration.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.7 on 2019-11-22 15:07 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +def create_uniswap_price_oracle(apps, schema_editor): + PriceOracle = apps.get_model('tokens', 'PriceOracle') + # Use uniswap mainnet address + PriceOracle.objects.create(name='Uniswap', configuration={'uniswap_exchange_address': + '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'}) + PriceOracle.objects.create(name='Kyber', configuration={'kyber_network_proxy_address': + '0x818E6FECD516Ecc3849DAf6845e3EC868087B755', + 'weth_token_address': + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'}) + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokens', '0011_auto_20190225_1646'), + ] + + operations = [ + migrations.AddField( + model_name='priceoracle', + name='configuration', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.RunPython(create_uniswap_price_oracle) + ] diff --git a/safe_relay_service/tokens/models.py b/safe_relay_service/tokens/models.py index 037523da..317936df 100644 --- a/safe_relay_service/tokens/models.py +++ b/safe_relay_service/tokens/models.py @@ -4,6 +4,7 @@ from urllib.parse import urljoin, urlparse from django.conf import settings +from django.contrib.postgres.fields import JSONField from django.db import models from gnosis.eth.django.models import EthereumAddressField @@ -16,9 +17,10 @@ class PriceOracle(models.Model): name = models.CharField(max_length=50, unique=True) + configuration = JSONField(null=False, default=dict) def __str__(self): - return self.name + return f'{self.name} configuration={self.configuration}' class PriceOracleTicker(models.Model): @@ -32,7 +34,7 @@ def __str__(self): def _price(self) -> Optional[float]: try: - price = get_price_oracle(self.price_oracle.name).get_price(self.ticker) + price = get_price_oracle(self.price_oracle.name, self.price_oracle.configuration).get_price(self.ticker) if price and self.inverse: # Avoid 1 / 0 price = 1 / price except ExchangeApiException: diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index 3938cf25..c0161287 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -1,9 +1,15 @@ import logging from abc import ABC, abstractmethod +from typing import Any, Dict import requests from cachetools import TTLCache, cached +from gnosis.eth import EthereumClientProvider +from gnosis.eth.constants import NULL_ADDRESS +from gnosis.eth.contracts import (get_uniswap_exchange_contract, get_kyber_network_proxy_contract, + get_uniswap_factory_contract) + logger = logging.getLogger(__name__) @@ -111,3 +117,80 @@ def get_price_oracle(name) -> PriceOracle: return oracle() else: raise NotImplementedError("Oracle '%s' not found" % name) + + +class Uniswap(PriceOracle): + def __init__(self, uniswap_exchange_address: str): + self.uniswap_exchange_address = uniswap_exchange_address + + @cached(cache=TTLCache(maxsize=1024, ttl=60)) + def get_price(self, ticker: str) -> float: + """ + :param ticker: Address of the token + :return: price + """ + ethereum_client = EthereumClientProvider() + uniswap_factory = get_uniswap_factory_contract(ethereum_client.w3, self.uniswap_exchange_address) + uniswap_exchange_address = uniswap_factory.functions.getExchange(ticker).call() + if uniswap_exchange_address == NULL_ADDRESS: + error_message = f'Cannot get price from uniswap-factory={uniswap_exchange_address} for token={ticker}' + logger.warning(error_message) + raise CannotGetTokenPriceFromApi(error_message) + + uniswap_exchange = get_uniswap_exchange_contract(ethereum_client.w3, uniswap_exchange_address) + value = ethereum_client.w3.toWei(1, 'ether') + price = uniswap_exchange.functions.getTokenToEthInputPrice(value).call() / value + if price <= 0.: + error_message = f'price={price} <= 0 from uniswap-factory={uniswap_exchange_address} for token={ticker}' + logger.warning(error_message) + raise CannotGetTokenPriceFromApi(error_message) + return price + + +class Kyber(PriceOracle): + def __init__(self, kyber_network_proxy_address: str, weth_token_address: str): + self.kyber_network_proxy_address = kyber_network_proxy_address + self.weth_token_address = weth_token_address + + @cached(cache=TTLCache(maxsize=1024, ttl=60)) + def get_price(self, ticker: str) -> float: + """ + :param ticker: Address of the token + :return: price + """ + ethereum_client = EthereumClientProvider() + kyber_network_proxy_contract = get_kyber_network_proxy_contract(ethereum_client.w3, + self.kyber_network_proxy_address) + try: + expected_rate, _ = kyber_network_proxy_contract.functions.getExpectedRate(ticker, + self.weth_token_address, + int(1e18)).call() + price = expected_rate / 1e18 + if price <= 0.: + error_message = f'price={price} <= 0 from kyber-network-proxy={self.kyber_network_proxy_address} ' \ + f'for token={ticker} to weth={self.weth_token_address}' + logger.warning(error_message) + raise CannotGetTokenPriceFromApi(error_message) + return price + except ValueError as e: + error_message = f'Cannot get price from kyber-network-proxy={self.kyber_network_proxy_address} ' \ + f'for token={ticker} to weth={self.weth_token_address}' + logger.warning(error_message) + raise CannotGetTokenPriceFromApi(error_message) from e + + +def get_price_oracle(name: str, configuration: Dict[Any, Any] = {}) -> PriceOracle: + oracles = { + 'binance': Binance, + 'dutchx': DutchX, + 'huobi': Huobi, + 'kraken': Kraken, + 'kyber': Kyber, + 'uniswap': Uniswap, + } + + oracle = oracles.get(name.lower()) + if oracle: + return oracle(**configuration) + else: + raise NotImplementedError("Oracle '%s' not found" % name) diff --git a/safe_relay_service/tokens/tests/test_models.py b/safe_relay_service/tokens/tests/test_models.py index 273010ee..95291003 100644 --- a/safe_relay_service/tokens/tests/test_models.py +++ b/safe_relay_service/tokens/tests/test_models.py @@ -12,7 +12,7 @@ class TestModels(TestCase): def test_price_oracles(self): - self.assertEqual(PriceOracle.objects.count(), 4) + self.assertEqual(PriceOracle.objects.count(), 6) def test_token_calculate_payment(self): token = TokenFactory(fixed_eth_conversion=0.1) From eac109a81f7c9e9c9708c9188586b34305fa0e3d Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 5 Dec 2019 16:15:56 +0100 Subject: [PATCH 12/38] Fix typo --- safe_relay_service/tokens/price_oracles.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index c0161287..64c33e8a 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -104,21 +104,6 @@ def get_price(self, ticker) -> float: return float(result[new_ticker]['c'][0]) -def get_price_oracle(name) -> PriceOracle: - oracles = { - 'binance': Binance, - 'dutchx': DutchX, - 'huobi': Huobi, - 'kraken': Kraken, - } - - oracle = oracles.get(name.lower()) - if oracle: - return oracle() - else: - raise NotImplementedError("Oracle '%s' not found" % name) - - class Uniswap(PriceOracle): def __init__(self, uniswap_exchange_address: str): self.uniswap_exchange_address = uniswap_exchange_address From 4429f319a2d43e07be880381fcfb6c6b4227907d Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 5 Dec 2019 17:44:48 +0100 Subject: [PATCH 13/38] Show info about skipped/failed tests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b7d93b22..167768c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ install: before_script: - psql -c 'create database travisci;' -U postgres script: - - coverage run --source=$SOURCE_FOLDER -m py.test -W ignore::DeprecationWarning + - coverage run --source=$SOURCE_FOLDER -m py.test -W ignore::DeprecationWarning -rxXs deploy: - provider: script script: bash scripts/deploy_docker.sh staging From b9aa91a15d18bbd5ff60d5e4781a928c19e7e9c4 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 5 Dec 2019 17:55:22 +0100 Subject: [PATCH 14/38] Use oracles in package - Closes #164 --- requirements.txt | 2 +- safe_relay_service/tokens/price_oracles.py | 47 ++++++---------------- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index 97e4da1e..c83bfc75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.7.6 +gnosis-py==1.7.7 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index 64c33e8a..d6ef66de 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -6,9 +6,9 @@ from cachetools import TTLCache, cached from gnosis.eth import EthereumClientProvider -from gnosis.eth.constants import NULL_ADDRESS -from gnosis.eth.contracts import (get_uniswap_exchange_contract, get_kyber_network_proxy_contract, - get_uniswap_factory_contract) +from gnosis.eth.oracles import Kyber as KyberOracle +from gnosis.eth.oracles import OracleException +from gnosis.eth.oracles import Uniswap as UniswapOracle logger = logging.getLogger(__name__) @@ -115,21 +115,11 @@ def get_price(self, ticker: str) -> float: :return: price """ ethereum_client = EthereumClientProvider() - uniswap_factory = get_uniswap_factory_contract(ethereum_client.w3, self.uniswap_exchange_address) - uniswap_exchange_address = uniswap_factory.functions.getExchange(ticker).call() - if uniswap_exchange_address == NULL_ADDRESS: - error_message = f'Cannot get price from uniswap-factory={uniswap_exchange_address} for token={ticker}' - logger.warning(error_message) - raise CannotGetTokenPriceFromApi(error_message) - - uniswap_exchange = get_uniswap_exchange_contract(ethereum_client.w3, uniswap_exchange_address) - value = ethereum_client.w3.toWei(1, 'ether') - price = uniswap_exchange.functions.getTokenToEthInputPrice(value).call() / value - if price <= 0.: - error_message = f'price={price} <= 0 from uniswap-factory={uniswap_exchange_address} for token={ticker}' - logger.warning(error_message) - raise CannotGetTokenPriceFromApi(error_message) - return price + uniswap = UniswapOracle(ethereum_client.w3, self.uniswap_exchange_address) + try: + return uniswap.get_price(ticker) + except OracleException as e: + raise CannotGetTokenPriceFromApi from e class Kyber(PriceOracle): @@ -144,24 +134,11 @@ def get_price(self, ticker: str) -> float: :return: price """ ethereum_client = EthereumClientProvider() - kyber_network_proxy_contract = get_kyber_network_proxy_contract(ethereum_client.w3, - self.kyber_network_proxy_address) + kyber = KyberOracle(ethereum_client.w3, self.kyber_network_proxy_address) try: - expected_rate, _ = kyber_network_proxy_contract.functions.getExpectedRate(ticker, - self.weth_token_address, - int(1e18)).call() - price = expected_rate / 1e18 - if price <= 0.: - error_message = f'price={price} <= 0 from kyber-network-proxy={self.kyber_network_proxy_address} ' \ - f'for token={ticker} to weth={self.weth_token_address}' - logger.warning(error_message) - raise CannotGetTokenPriceFromApi(error_message) - return price - except ValueError as e: - error_message = f'Cannot get price from kyber-network-proxy={self.kyber_network_proxy_address} ' \ - f'for token={ticker} to weth={self.weth_token_address}' - logger.warning(error_message) - raise CannotGetTokenPriceFromApi(error_message) from e + return kyber.get_price(ticker, self.weth_token_address) + except OracleException as e: + raise CannotGetTokenPriceFromApi from e def get_price_oracle(name: str, configuration: Dict[Any, Any] = {}) -> PriceOracle: From 62a293b31bd7c0e6f99b981e42828e210d2fe0cc Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 10 Dec 2019 11:32:44 +0100 Subject: [PATCH 15/38] Fix logging typos --- .../relay/services/safe_creation_service.py | 2 +- safe_relay_service/relay/services/transaction_service.py | 4 ++-- safe_relay_service/relay/views.py | 7 ++++--- safe_relay_service/tokens/price_oracles.py | 8 ++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index 98e2b276..a722da9d 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -118,7 +118,7 @@ def _get_token_eth_value_or_raise(self, address: str) -> float: token = Token.objects.get(address=address, gas=True) return token.get_eth_value() except Token.DoesNotExist: - logger.warning('Cannot get value of token in eth: Gas token %s not valid' % address) + logger.warning('Cannot get value of token in eth: Gas token %s not valid', address) raise InvalidPaymentToken(address) def _get_configured_gas_price(self) -> int: diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index 814b320e..7cd329aa 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -146,7 +146,7 @@ def _is_valid_gas_token(address: Optional[str]) -> float: Token.objects.get(address=address, gas=True) return True except Token.DoesNotExist: - logger.warning('Cannot retrieve gas token from db: Gas token %s not valid' % address) + logger.warning('Cannot retrieve gas token from db: Gas token %s not valid', address) return False def _check_safe_gas_price(self, gas_token: Optional[str], safe_gas_price: int) -> bool: @@ -173,7 +173,7 @@ def _check_safe_gas_price(self, gas_token: Optional[str], safe_gas_price: int) - # We use gas station tx gas price. We cannot use internal tx's because is calculated # based on the gas token except Token.DoesNotExist: - logger.warning('Cannot retrieve gas token from db: Gas token %s not valid' % gas_token) + logger.warning('Cannot retrieve gas token from db: Gas token %s not valid', gas_token) raise InvalidGasToken('Gas token %s not valid' % gas_token) else: if safe_gas_price < minimum_accepted_gas_price: diff --git a/safe_relay_service/relay/views.py b/safe_relay_service/relay/views.py index 8b57f49f..223cb247 100644 --- a/safe_relay_service/relay/views.py +++ b/safe_relay_service/relay/views.py @@ -66,9 +66,10 @@ def custom_exception_handler(exc, context): exception_str = exc.__class__.__name__ response.data = {'exception': exception_str} - logger.warning('%s - Exception: %s - Data received %s' % (context['request'].build_absolute_uri(), - exception_str, - context['request'].data)) + logger.warning('%s - Exception: %s - Data received %s', + context['request'].build_absolute_uri(), + exception_str, + context['request'].data) return response diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index d6ef66de..52bb5eed 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -43,7 +43,7 @@ def get_price(self, ticker) -> float: response = requests.get(url) api_json = response.json() if not response.ok: - logger.warning('Cannot get price from url=%s' % url) + logger.warning('Cannot get price from url=%s', url) raise CannotGetTokenPriceFromApi(api_json.get('msg')) return float(api_json['price']) @@ -65,7 +65,7 @@ def get_price(self, ticker: str) -> float: response = requests.get(url) api_json = response.json() if not response.ok or api_json is None: - logger.warning('Cannot get price from url=%s' % url) + logger.warning('Cannot get price from url=%s', url) raise CannotGetTokenPriceFromApi(api_json) return float(api_json) @@ -82,7 +82,7 @@ def get_price(self, ticker) -> float: api_json = response.json() error = api_json.get('err-msg') if not response.ok or error: - logger.warning('Cannot get price from url=%s' % url) + logger.warning('Cannot get price from url=%s', url) raise CannotGetTokenPriceFromApi(error) return float(api_json['tick']['close']) @@ -96,7 +96,7 @@ def get_price(self, ticker) -> float: api_json = response.json() error = api_json.get('error') if not response.ok or error: - logger.warning('Cannot get price from url=%s' % url) + logger.warning('Cannot get price from url=%s', url) raise CannotGetTokenPriceFromApi(str(api_json['error'])) result = api_json['result'] From 36f2e79dc70b59c4f5d6e1fef57ab40f708850db Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 10 Dec 2019 12:52:00 +0100 Subject: [PATCH 16/38] Rename Oracles --- requirements.txt | 2 +- safe_relay_service/tokens/price_oracles.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index c83bfc75..6cfef7d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.7.7 +gnosis-py==1.7.8 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index 52bb5eed..1d4e4c1a 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -6,9 +6,7 @@ from cachetools import TTLCache, cached from gnosis.eth import EthereumClientProvider -from gnosis.eth.oracles import Kyber as KyberOracle -from gnosis.eth.oracles import OracleException -from gnosis.eth.oracles import Uniswap as UniswapOracle +from gnosis.eth.oracles import KyberOracle, OracleException, UniswapOracle logger = logging.getLogger(__name__) From 9ccfce3b98b20d5096588989b67662ae0a1fa6a2 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 11 Dec 2019 14:14:10 +0100 Subject: [PATCH 17/38] Update python to 3.8 --- .travis.yml | 2 +- docker/web/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 167768c0..cfcdbe11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ dist: xenial language: python cache: pip python: - - "3.7" + - "3.8" env: global: - DOCKERHUB_PROJECT=safe-relay-service diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index f1d272d8..fe941715 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-slim-stretch +FROM python:3.8-slim ENV PYTHONUNBUFFERED 1 WORKDIR /app From 0008aab91f9389e849d33137988797a7ff0176c1 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 11 Dec 2019 19:14:51 +0100 Subject: [PATCH 18/38] Update Safe addresses --- config/settings/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index 8654a21e..c4caed5c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -296,7 +296,7 @@ SAFE_FUNDER_MAX_ETH = env.int('SAFE_FUNDER_MAX_ETH', default=0.1) SAFE_FUNDING_CONFIRMATIONS = env.int('SAFE_FUNDING_CONFIRMATIONS', default=0) # Set to at least 3 # Master Copy Address of Safe Contract -SAFE_CONTRACT_ADDRESS = env('SAFE_CONTRACT_ADDRESS', default='0xaE32496491b53841efb51829d6f886387708F99B') +SAFE_CONTRACT_ADDRESS = env('SAFE_CONTRACT_ADDRESS', default='0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F') SAFE_V1_0_0_CONTRACT_ADDRESS = env('SAFE_V1_0_0_CONTRACT_ADDRESS', default='0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A') SAFE_V0_0_1_CONTRACT_ADDRESS = env('SAFE_V0_0_1_CONTRACT_ADDRESS', default='0x8942595A2dC5181Df0465AF0D7be08c8f23C93af') SAFE_VALID_CONTRACT_ADDRESSES = set(env.list('SAFE_VALID_CONTRACT_ADDRESSES', @@ -307,11 +307,11 @@ ) | {SAFE_CONTRACT_ADDRESS, SAFE_V1_0_0_CONTRACT_ADDRESS, SAFE_V0_0_1_CONTRACT_ADDRESS} -SAFE_PROXY_FACTORY_ADDRESS = env('SAFE_PROXY_FACTORY_ADDRESS', default='0x50e55Af101C777bA7A1d560a774A82eF002ced9F') +SAFE_PROXY_FACTORY_ADDRESS = env('SAFE_PROXY_FACTORY_ADDRESS', default='0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B') SAFE_PROXY_FACTORY_V1_0_0_ADDRESS = env('SAFE_PROXY_FACTORY_V1_0_0_ADDRESS', default='0x12302fE9c02ff50939BaAaaf415fc226C078613C') SAFE_DEFAULT_CALLBACK_HANDLER = env('SAFE_DEFAULT_CALLBACK_HANDLER', - default='0x40A930851BD2e590Bd5A5C981b436de25742E980') + default='0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44') # If FIXED_GAS_PRICE is None, GasStation will be used FIXED_GAS_PRICE = env.int('FIXED_GAS_PRICE', default=None) From e04a9ea07432f0df0a6c746ed820a93e02177bac Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 12 Dec 2019 14:56:46 +0100 Subject: [PATCH 19/38] Update gnosis-py to use v1.1.1 contracts --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6cfef7d0..f45c9455 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.7.8 +gnosis-py==1.8.0 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 From 15f3c58627829456422f9b7c41f4b4fb334f36c3 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 13 Dec 2019 10:40:13 +0100 Subject: [PATCH 20/38] Set version 3.7.0 --- safe_relay_service/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_relay_service/version.py b/safe_relay_service/version.py index 627984e5..b1b90bea 100644 --- a/safe_relay_service/version.py +++ b/safe_relay_service/version.py @@ -1,2 +1,2 @@ -__version__ = '3.6.10' +__version__ = '3.7.0' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) From b80686a22e201fbfce39dc98f3fb792efdf8b247 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 13 Dec 2019 13:07:42 +0100 Subject: [PATCH 21/38] Alert when txs take too long to mine - Configurable via `SAFE_TX_NOT_MINED_ALERT_MINUTES` - Closes #162 --- config/settings/base.py | 1 + docker/web/run_web.sh | 2 +- .../{setup_safe_relay.py => setup_service.py} | 4 +++- .../relay/services/transaction_service.py | 13 ++++++++++++ safe_relay_service/relay/tasks.py | 21 +++++++++++++++++++ .../relay/tests/test_commands.py | 6 +++--- safe_relay_service/relay/tests/test_tasks.py | 21 +++++++++++++++---- .../relay/tests/test_transaction_service.py | 20 +++++++++++++++++- safe_relay_service/relay/views.py | 3 ++- 9 files changed, 80 insertions(+), 11 deletions(-) rename safe_relay_service/relay/management/commands/{setup_safe_relay.py => setup_service.py} (89%) diff --git a/config/settings/base.py b/config/settings/base.py index c4caed5c..78e1430c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -321,6 +321,7 @@ SAFE_CHECK_DEPLOYER_FUNDED_RETRIES = env.int('SAFE_CHECK_DEPLOYER_FUNDED_RETRIES', default=10) SAFE_FIXED_CREATION_COST = env.int('SAFE_FIXED_CREATION_COST', default=None) SAFE_ACCOUNTS_BALANCE_WARNING = env.int('SAFE_ACCOUNTS_BALANCE_WARNING', default=200000000000000000) # 0.2 Eth +SAFE_TX_NOT_MINED_ALERT_MINUTES = env('SAFE_TX_NOT_MINED_ALERT_MINUTES', default=15) NOTIFICATION_SERVICE_URI = env('NOTIFICATION_SERVICE_URI', default=None) NOTIFICATION_SERVICE_PASS = env('NOTIFICATION_SERVICE_PASS', default=None) diff --git a/docker/web/run_web.sh b/docker/web/run_web.sh index 4df8a442..a8d1d7c2 100755 --- a/docker/web/run_web.sh +++ b/docker/web/run_web.sh @@ -7,7 +7,7 @@ python manage.py migrate --noinput echo "==> Setup Gas Station..." python manage.py setup_gas_station echo "==> Setup Safe Relay Task..." -python manage.py setup_safe_relay +python manage.py setup_service if [ "${DEPLOY_MASTER_COPY_ON_INIT:-0}" = 1 ]; then echo "==> Deploy Safe master copy..." python manage.py deploy_safe_contracts diff --git a/safe_relay_service/relay/management/commands/setup_safe_relay.py b/safe_relay_service/relay/management/commands/setup_service.py similarity index 89% rename from safe_relay_service/relay/management/commands/setup_safe_relay.py rename to safe_relay_service/relay/management/commands/setup_service.py index b69e03ad..811cb5a1 100644 --- a/safe_relay_service/relay/management/commands/setup_safe_relay.py +++ b/safe_relay_service/relay/management/commands/setup_service.py @@ -21,7 +21,7 @@ def create_task(self) -> Tuple[PeriodicTask, bool]: class Command(BaseCommand): - help = 'Setup Safe relay required tasks' + help = 'Setup service required tasks' tasks = [CeleryTaskConfiguration('safe_relay_service.relay.tasks.deploy_safes_task', 'Deploy Safes', 20, IntervalSchedule.SECONDS), CeleryTaskConfiguration('safe_relay_service.relay.tasks.check_balance_of_accounts_task', @@ -32,6 +32,8 @@ class Command(BaseCommand): 'Process Internal Txs for Safes', 2, IntervalSchedule.MINUTES), CeleryTaskConfiguration('safe_relay_service.relay.tasks.find_erc_20_721_transfers_task', 'Process ERC20/721 transfers for Safes', 2, IntervalSchedule.MINUTES), + CeleryTaskConfiguration('safe_relay_service.relay.tasks.check_pending_transactions', + 'Check transactions not mined after a while', 10, IntervalSchedule.MINUTES), ] def handle(self, *args, **options): diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index 7cd329aa..81d9174b 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -1,7 +1,9 @@ +from datetime import timedelta from logging import getLogger from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple from django.db import IntegrityError +from django.db.models import Q from django.utils import timezone from eth_account import Account @@ -420,3 +422,14 @@ def _send_multisig_tx(self, tx_hash, tx = safe_tx.execute(tx_sender_private_key, tx_gas=tx_gas, tx_gas_price=tx_gas_price, tx_nonce=tx_nonce, block_identifier=block_identifier) return tx_hash, safe_tx.tx_hash, tx + + def get_pending_multisig_transactions(self, older_than: int) -> List[SafeMultisigTx]: + """ + Get multisig txs that have not been mined after X seconds + :param older_than: Time in seconds for a tx to be considered pending, if 0 all will be returned + """ + return SafeMultisigTx.objects.filter( + Q(ethereum_tx__block=None) | Q(ethereum_tx=None) + ).filter( + created__lte=timezone.now() - timedelta(seconds=older_than), + ) diff --git a/safe_relay_service/relay/tasks.py b/safe_relay_service/relay/tasks.py index 48c70ac2..71a7a952 100644 --- a/safe_relay_service/relay/tasks.py +++ b/safe_relay_service/relay/tasks.py @@ -370,3 +370,24 @@ def find_erc_20_721_transfers_task() -> int: except LockError: pass return number_safes + + +@app.shared_task(soft_time_limit=60) +def check_pending_transactions() -> int: + """ + Find txs that have not been mined after a while + :return: Number of pending transactions + """ + number_txs = 0 + try: + redis = RedisRepository().redis + with redis.lock('tasks:check_pending_transactions', blocking_timeout=1, timeout=60): + tx_not_mined_alert = settings.SAFE_TX_NOT_MINED_ALERT_MINUTES + txs = TransactionServiceProvider().get_pending_multisig_transactions(older_than=tx_not_mined_alert * 60) + for tx in txs: + logger.error('Tx with tx-hash=%s and safe-tx-hash=%s has not been mined after a while, created=%s', + tx.ethereum_tx_id, tx.safe_tx_hash, tx.created) + number_txs += 1 + except LockError: + pass + return number_txs diff --git a/safe_relay_service/relay/tests/test_commands.py b/safe_relay_service/relay/tests/test_commands.py index 2d303954..aadfdc39 100644 --- a/safe_relay_service/relay/tests/test_commands.py +++ b/safe_relay_service/relay/tests/test_commands.py @@ -41,9 +41,9 @@ def test_setup_internal_txs(self): call_command('setup_internal_txs', stdout=buf) self.assertIn('Generated 1 SafeTxStatus', buf.getvalue()) - def test_setup_safe_relay(self): - from ..management.commands.setup_safe_relay import Command + def test_setup_service(self): + from ..management.commands.setup_service import Command number_tasks = len(Command.tasks) self.assertEqual(PeriodicTask.objects.all().count(), 0) - call_command('setup_safe_relay') + call_command('setup_service') self.assertEqual(PeriodicTask.objects.all().count(), number_tasks) diff --git a/safe_relay_service/relay/tests/test_tasks.py b/safe_relay_service/relay/tests/test_tasks.py index 34b4b0c9..58863aa8 100644 --- a/safe_relay_service/relay/tests/test_tasks.py +++ b/safe_relay_service/relay/tests/test_tasks.py @@ -8,12 +8,13 @@ from ..models import SafeContract, SafeFunding from ..services import Erc20EventsServiceProvider, InternalTxServiceProvider from ..tasks import (check_balance_of_accounts_task, - check_deployer_funded_task, deploy_create2_safe_task, - deploy_safes_task, find_erc_20_721_transfers_task, - find_internal_txs_task, fund_deployer_task) + check_deployer_funded_task, check_pending_transactions, + deploy_create2_safe_task, deploy_safes_task, + find_erc_20_721_transfers_task, find_internal_txs_task, + fund_deployer_task) from .factories import (SafeContractFactory, SafeCreation2Factory, SafeCreationFactory, SafeFundingFactory, - SafeTxStatusFactory) + SafeMultisigTxFactory, SafeTxStatusFactory) from .relay_test_case import RelayTestCaseMixin from .test_internal_tx_service import EthereumClientMock @@ -81,3 +82,15 @@ def test_find_erc_20_721_transfers_task(self): SafeTxStatusFactory(safe=safe) self.assertEqual(find_erc_20_721_transfers_task.delay().get(), 1) Erc20EventsServiceProvider.del_singleton() + + def test_check_pending_transactions(self): + not_mined_alert_minutes = settings.SAFE_TX_NOT_MINED_ALERT_MINUTES + self.assertEqual(check_pending_transactions.delay().get(), 0) + + SafeMultisigTxFactory(created=timezone.now() - timedelta(minutes=not_mined_alert_minutes - 1), + ethereum_tx__block=None) + self.assertEqual(check_pending_transactions.delay().get(), 0) + + SafeMultisigTxFactory(created=timezone.now() - timedelta(minutes=not_mined_alert_minutes + 1), + ethereum_tx__block=None) + self.assertEqual(check_pending_transactions.delay().get(), 1) diff --git a/safe_relay_service/relay/tests/test_transaction_service.py b/safe_relay_service/relay/tests/test_transaction_service.py index 53f0e150..57610227 100644 --- a/safe_relay_service/relay/tests/test_transaction_service.py +++ b/safe_relay_service/relay/tests/test_transaction_service.py @@ -1,4 +1,7 @@ +from datetime import timedelta + from django.test import TestCase +from django.utils import timezone from eth_account import Account from hexbytes import HexBytes @@ -17,7 +20,7 @@ NotEnoughFundsForMultisigTx, RefundMustBeEnabled, SignaturesNotSorted) -from .factories import SafeContractFactory +from .factories import SafeContractFactory, SafeMultisigTxFactory from .relay_test_case import RelayTestCaseMixin @@ -370,3 +373,18 @@ def test_estimate_tx_for_all_tokent(self): self.assertAlmostEqual(estimation_token.gas_price, estimation_ether.gas_price // 2, delta=1.0) self.assertGreater(estimation_token.base_gas, estimation_ether.base_gas) self.assertEqual(estimation_token.gas_token, valid_token.address) + + def test_get_pending_multisig_transactions(self): + self.assertFalse(self.transaction_service.get_pending_multisig_transactions(0)) + + SafeMultisigTxFactory(created=timezone.now()) + self.assertFalse(self.transaction_service.get_pending_multisig_transactions(0)) + + SafeMultisigTxFactory(created=timezone.now(), ethereum_tx__block=None) + self.assertEqual(self.transaction_service.get_pending_multisig_transactions(0).count(), 1) + self.assertFalse(self.transaction_service.get_pending_multisig_transactions(30)) + + SafeMultisigTxFactory(created=timezone.now() - timedelta(seconds=60), ethereum_tx__block=None) + self.assertEqual(self.transaction_service.get_pending_multisig_transactions(30).count(), 1) + SafeMultisigTxFactory(created=timezone.now() - timedelta(minutes=60), ethereum_tx__block=None) + self.assertEqual(self.transaction_service.get_pending_multisig_transactions(30).count(), 2) diff --git a/safe_relay_service/relay/views.py b/safe_relay_service/relay/views.py index 223cb247..5fbd5a89 100644 --- a/safe_relay_service/relay/views.py +++ b/safe_relay_service/relay/views.py @@ -103,8 +103,9 @@ def get(self, request, format=None): 'SAFE_FUNDER_MAX_ETH': settings.SAFE_FUNDER_MAX_ETH, 'SAFE_FUNDER_PUBLIC_KEY': safe_funder_public_key, 'SAFE_FUNDING_CONFIRMATIONS': settings.SAFE_FUNDING_CONFIRMATIONS, - 'SAFE_PROXY_FACTORY_V1_0_0_ADDRESS': settings.SAFE_PROXY_FACTORY_V1_0_0_ADDRESS, 'SAFE_PROXY_FACTORY_ADDRESS': settings.SAFE_PROXY_FACTORY_ADDRESS, + 'SAFE_PROXY_FACTORY_V1_0_0_ADDRESS': settings.SAFE_PROXY_FACTORY_V1_0_0_ADDRESS, + 'SAFE_TX_NOT_MINED_ALERT_MINUTES': settings.SAFE_TX_NOT_MINED_ALERT_MINUTES, 'SAFE_TX_SENDER_PUBLIC_KEY': safe_sender_public_key, 'SAFE_V0_0_1_CONTRACT_ADDRESS': settings.SAFE_V0_0_1_CONTRACT_ADDRESS, 'SAFE_V1_0_0_CONTRACT_ADDRESS': settings.SAFE_V1_0_0_CONTRACT_ADDRESS, From 947e36294daddc5115ded82b38227edbbc491d67 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 13 Dec 2019 16:50:52 +0100 Subject: [PATCH 22/38] Fix problem with proxies --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f45c9455..a078f9e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.8.0 +gnosis-py==1.8.1 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 From 7a34b2492a78027aa1131360c445d2de3ec1aaee Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Mon, 16 Dec 2019 19:19:25 +0100 Subject: [PATCH 23/38] Fix typo preventing v2 Safes to be deployed --- .../relay/services/safe_creation_service.py | 17 +++++++++-------- safe_relay_service/relay/tests/test_views_v2.py | 11 +++++++++++ safe_relay_service/relay/tests/test_views_v3.py | 11 +++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index a722da9d..6d518789 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -189,7 +189,7 @@ def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: :param safe_address: :return: tx_hash """ - safe_creation2 = SafeCreation2.objects.get(safe=safe_address) + safe_creation2: SafeCreation2 = SafeCreation2.objects.get(safe=safe_address) if safe_creation2.tx_hash: logger.info('Safe=%s has already been deployed with tx-hash=%s', safe_address, safe_creation2.tx_hash) @@ -216,13 +216,14 @@ def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: with EthereumNonceLock(self.redis, self.ethereum_client, self.funder_account.address, timeout=60 * 2) as tx_nonce: - ethereum_tx_sent = self.proxy_factory.deploy_proxy_contract_with_nonce(self.funder_account, - self.safe_contract_address, - setup_data, - safe_creation2.salt_nonce, - safe_creation2.gas_estimated, - safe_creation2.gas_price_estimated, - nonce=tx_nonce) + proxy_factory = ProxyFactory(safe_creation2.proxy_factory, self.ethereum_client) + ethereum_tx_sent = proxy_factory.deploy_proxy_contract_with_nonce(self.funder_account, + safe_creation2.master_copy, + setup_data, + safe_creation2.salt_nonce, + safe_creation2.gas_estimated, + safe_creation2.gas_price_estimated, + nonce=tx_nonce) EthereumTx.objects.create_from_tx(ethereum_tx_sent.tx, ethereum_tx_sent.tx_hash) safe_creation2.tx_hash = ethereum_tx_sent.tx_hash safe_creation2.save() diff --git a/safe_relay_service/relay/tests/test_views_v2.py b/safe_relay_service/relay/tests/test_views_v2.py index 0333e99e..af73e979 100644 --- a/safe_relay_service/relay/tests/test_views_v2.py +++ b/safe_relay_service/relay/tests/test_views_v2.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.urls import reverse from eth_account import Account @@ -10,6 +11,7 @@ from gnosis.eth.constants import NULL_ADDRESS from gnosis.eth.utils import get_eth_address_with_invalid_checksum +from gnosis.safe import Safe from gnosis.safe.tests.utils import generate_salt_nonce from safe_relay_service.tokens.tests.factories import TokenFactory @@ -78,6 +80,7 @@ def test_safe_creation(self): self.assertGreater(int(response_json['gasEstimated']), 0) self.assertGreater(int(response_json['gasPriceEstimated']), 0) self.assertGreater(len(response_json['setupData']), 2) + self.assertEqual(response_json['masterCopy'], settings.SAFE_V1_0_0_CONTRACT_ADDRESS) self.assertTrue(SafeContract.objects.filter(address=safe_address)) self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]])) @@ -86,6 +89,14 @@ def test_safe_creation(self): # Payment includes deployment gas + gas to send eth to the deployer self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost()) + # Deploy the Safe to check it + self.send_ether(safe_address, int(response_json['payment'])) + safe_creation2 = SafeCreationV1_0_0ServiceProvider().deploy_create2_safe_tx(safe_address) + self.ethereum_client.get_transaction_receipt(safe_creation2.tx_hash, timeout=20) + safe = Safe(safe_address, self.ethereum_client) + self.assertEqual(safe.retrieve_master_copy_address(), response_json['masterCopy']) + self.assertEqual(safe.retrieve_owners(), owners) + # Test exception when same Safe is created response = self.client.post(reverse('v2:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) diff --git a/safe_relay_service/relay/tests/test_views_v3.py b/safe_relay_service/relay/tests/test_views_v3.py index 623eaa9f..095652d6 100644 --- a/safe_relay_service/relay/tests/test_views_v3.py +++ b/safe_relay_service/relay/tests/test_views_v3.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.urls import reverse from eth_account import Account @@ -9,6 +10,7 @@ from rest_framework.test import APITestCase from gnosis.eth.constants import NULL_ADDRESS +from gnosis.safe import Safe from gnosis.safe.tests.utils import generate_salt_nonce from safe_relay_service.tokens.tests.factories import TokenFactory @@ -76,6 +78,7 @@ def test_safe_creation(self): self.assertGreater(int(response_json['gasEstimated']), 0) self.assertGreater(int(response_json['gasPriceEstimated']), 0) self.assertGreater(len(response_json['setupData']), 2) + self.assertEqual(response_json['masterCopy'], settings.SAFE_CONTRACT_ADDRESS) self.assertTrue(SafeContract.objects.filter(address=safe_address)) self.assertTrue(SafeCreation2.objects.filter(owners__contains=[owners[0]])) @@ -84,6 +87,14 @@ def test_safe_creation(self): # Payment includes deployment gas + gas to send eth to the deployer self.assertEqual(safe_creation.payment, safe_creation.wei_estimated_deploy_cost()) + # Deploy the Safe to check it + self.send_ether(safe_address, int(response_json['payment'])) + safe_creation2 = SafeCreationServiceProvider().deploy_create2_safe_tx(safe_address) + self.ethereum_client.get_transaction_receipt(safe_creation2.tx_hash, timeout=20) + safe = Safe(safe_address, self.ethereum_client) + self.assertEqual(safe.retrieve_master_copy_address(), response_json['masterCopy']) + self.assertEqual(safe.retrieve_owners(), owners) + # Test exception when same Safe is created response = self.client.post(reverse('v3:safe-creation'), data, format='json') self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) From 5594152660a69d0867e56e438b2fc04732e895ce Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 17 Dec 2019 15:36:19 +0100 Subject: [PATCH 24/38] Show deprecation warnings on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cfcdbe11..b0b23721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ install: before_script: - psql -c 'create database travisci;' -U postgres script: - - coverage run --source=$SOURCE_FOLDER -m py.test -W ignore::DeprecationWarning -rxXs + - coverage run --source=$SOURCE_FOLDER -m py.test -rxXs deploy: - provider: script script: bash scripts/deploy_docker.sh staging From 6813bb91366d5aaaa0836bc251cfa8b9ea32928f Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 17 Dec 2019 16:19:21 +0100 Subject: [PATCH 25/38] Update to web3 v5 --- requirements.txt | 4 ++-- safe_relay_service/gas_station/gas_station.py | 4 ++-- .../relay/management/commands/deploy_safe_contracts.py | 4 ++-- .../relay/management/commands/test_relay_server.py | 8 ++++---- safe_relay_service/relay/services/funding_service.py | 4 ++-- .../relay/services/safe_creation_service.py | 2 +- .../relay/services/transaction_service.py | 6 +++--- safe_relay_service/relay/tests/factories.py | 10 +++++----- .../relay/tests/test_transaction_service.py | 2 +- safe_relay_service/relay/tests/test_views.py | 2 +- safe_relay_service/relay/views.py | 4 ++-- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index a078f9e2..ff60f8db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==1.0.7 gevent==1.4.0 -gnosis-py==1.8.1 +gnosis-py==2.0.0 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 @@ -25,4 +25,4 @@ packaging>=19.0 psycopg2-binary==2.8.4 redis==3.3.11 requests==2.22.0 -web3==4.9.2 +web3==5.4.0 diff --git a/safe_relay_service/gas_station/gas_station.py b/safe_relay_service/gas_station/gas_station.py index 39167ba7..ef26cee2 100644 --- a/safe_relay_service/gas_station/gas_station.py +++ b/safe_relay_service/gas_station/gas_station.py @@ -53,10 +53,10 @@ def __init__(self, self.w3 = Web3(HTTPProvider(http_provider_uri)) try: if self.w3.net.version != 1: - self.w3.middleware_stack.inject(geth_poa_middleware, layer=0) + self.w3.middleware_onion.inject(geth_poa_middleware, layer=0) # For tests using dummy connections (like IPC) except (ConnectionError, FileNotFoundError): - self.w3.middleware_stack.inject(geth_poa_middleware, layer=0) + self.w3.middleware_onion.inject(geth_poa_middleware, layer=0) def _get_block_cache_key(self, block_number): return 'block:%d' % block_number diff --git a/safe_relay_service/relay/management/commands/deploy_safe_contracts.py b/safe_relay_service/relay/management/commands/deploy_safe_contracts.py index 1eec628f..6c033064 100644 --- a/safe_relay_service/relay/management/commands/deploy_safe_contracts.py +++ b/safe_relay_service/relay/management/commands/deploy_safe_contracts.py @@ -11,7 +11,7 @@ class Command(BaseCommand): help = 'Deploys master copy using first unlocked account on the node if `ganache -d` is found and contract ' \ 'is not deployed. If not you need to set a private key using `--deployer-key`' GANACHE_FIRST_ACCOUNT_KEY = '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d' - DEFAULT_ACCOUNT = Account.privateKeyToAccount(GANACHE_FIRST_ACCOUNT_KEY) + DEFAULT_ACCOUNT = Account.from_key(GANACHE_FIRST_ACCOUNT_KEY) def add_arguments(self, parser): # Positional arguments @@ -20,7 +20,7 @@ def add_arguments(self, parser): def handle(self, *args, **options): ethereum_client = EthereumClientProvider() deployer_key = options['deployer_key'] - deployer_account = Account.privateKeyToAccount(deployer_key) if deployer_key else self.DEFAULT_ACCOUNT + deployer_account = Account.from_key(deployer_key) if deployer_key else self.DEFAULT_ACCOUNT master_copies_with_deploy_fn = { settings.SAFE_CONTRACT_ADDRESS: Safe.deploy_master_contract, diff --git a/safe_relay_service/relay/management/commands/test_relay_server.py b/safe_relay_service/relay/management/commands/test_relay_server.py index 64fc0f2e..c3e91241 100644 --- a/safe_relay_service/relay/management/commands/test_relay_server.py +++ b/safe_relay_service/relay/management/commands/test_relay_server.py @@ -24,7 +24,7 @@ def send_eth(w3, account, to, value, nonce=None): 'pending'), } - signed_tx = w3.eth.account.signTransaction(tx, private_key=account.privateKey) + signed_tx = w3.eth.account.signTransaction(tx, private_key=account.key) return w3.eth.sendRawTransaction(signed_tx.rawTransaction) @@ -33,7 +33,7 @@ def send_token(w3, account, to, amount_to_send, token_address, nonce=None): nonce = nonce if nonce is not None else w3.eth.getTransactionCount(account.address, 'pending') tx = erc20_contract.functions.transfer(to, amount_to_send).buildTransaction({'from': account.address, 'nonce': nonce}) - signed_tx = w3.eth.account.signTransaction(tx, private_key=account.privateKey) + signed_tx = w3.eth.account.signTransaction(tx, private_key=account.key) return w3.eth.sendRawTransaction(signed_tx.rawTransaction) @@ -76,7 +76,7 @@ def handle(self, *args, **options): multiple_txs = options['multiple_txs'] self.w3 = Web3(HTTPProvider(options['node_url'])) - self.main_account = Account.privateKeyToAccount(options['private_key']) + self.main_account = Account.from_key(options['private_key']) self.main_account_nonce = self.w3.eth.getTransactionCount(self.main_account.address, 'pending') main_account_balance = self.w3.eth.getBalance(self.main_account.address) self.stdout.write(self.style.SUCCESS('Using %s as main account with balance=%d' % (self.main_account.address, @@ -92,7 +92,7 @@ def handle(self, *args, **options): accounts = [Account.create() for _ in range(3)] for account in accounts: self.stdout.write(self.style.SUCCESS('Created account=%s with key=%s' % (account.address, - account.privateKey.hex()))) + account.key.hex()))) accounts.append(self.main_account) accounts.sort(key=lambda acc: acc.address.lower()) owners = [account.address for account in accounts] diff --git a/safe_relay_service/relay/services/funding_service.py b/safe_relay_service/relay/services/funding_service.py index dd71ca14..8946d423 100644 --- a/safe_relay_service/relay/services/funding_service.py +++ b/safe_relay_service/relay/services/funding_service.py @@ -44,7 +44,7 @@ def __init__(self, ethereum_client: EthereumClient, gas_station: GasStation, red self.ethereum_client = ethereum_client self.gas_station = gas_station self.redis = redis - self.funder_account = Account.privateKeyToAccount(funder_private_key) + self.funder_account = Account.from_key(funder_private_key) self.max_eth_to_send = max_eth_to_send def send_eth_to(self, to: str, value: int, gas: int = 22000, gas_price=None, @@ -57,7 +57,7 @@ def send_eth_to(self, to: str, value: int, gas: int = 22000, gas_price=None, with EthereumNonceLock(self.redis, self.ethereum_client, self.funder_account.address, timeout=60 * 2) as tx_nonce: - return self.ethereum_client.send_eth_to(self.funder_account.privateKey, to, gas_price, value, + return self.ethereum_client.send_eth_to(self.funder_account.key, to, gas_price, value, gas=gas, retry=retry, block_identifier=block_identifier, diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index 6d518789..2e7e8720 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -101,7 +101,7 @@ def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, red self.safe_contract_address = safe_contract_address self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.default_callback_handler = default_callback_handler - self.funder_account = Account.privateKeyToAccount(safe_funder_private_key) + self.funder_account = Account.from_key(safe_funder_private_key) self.safe_fixed_creation_cost = safe_fixed_creation_cost def _get_token_eth_value_or_raise(self, address: str) -> float: diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index 81d9174b..9919c4e6 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -124,7 +124,7 @@ def __init__(self, gas_station: GasStation, ethereum_client: EthereumClient, red self.redis = redis self.safe_valid_contract_addresses = safe_valid_contract_addresses self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) - self.tx_sender_account = Account.privateKeyToAccount(tx_sender_private_key) + self.tx_sender_account = Account.from_key(tx_sender_private_key) @staticmethod def _check_refund_receiver(refund_receiver: str) -> bool: @@ -393,8 +393,8 @@ def _send_multisig_tx(self, # We use fast tx gas price, if not txs could be stuck tx_gas_price = self._get_configured_gas_price() - tx_sender_private_key = self.tx_sender_account.privateKey - tx_sender_address = Account.privateKeyToAccount(tx_sender_private_key).address + tx_sender_private_key = self.tx_sender_account.key + tx_sender_address = Account.from_key(tx_sender_private_key).address safe_tx = safe.build_multisig_tx( to, diff --git a/safe_relay_service/relay/tests/factories.py b/safe_relay_service/relay/tests/factories.py index 95859dca..0bd85ccc 100644 --- a/safe_relay_service/relay/tests/factories.py +++ b/safe_relay_service/relay/tests/factories.py @@ -43,7 +43,7 @@ class Meta: owners = factory.LazyFunction(lambda: [Account.create().address, Account.create().address]) threshold = 2 payment = factory.fuzzy.FuzzyInteger(100, 1000) - tx_hash = factory.Sequence(lambda n: Web3.sha3(n)) + tx_hash = factory.Sequence(lambda n: Web3.keccak(n)) gas = factory.fuzzy.FuzzyInteger(100000, 200000) gas_price = factory.fuzzy.FuzzyInteger(Web3.toWei(1, 'gwei'), Web3.toWei(20, 'gwei')) payment_token = None @@ -74,7 +74,7 @@ class Meta: setup_data = factory.Sequence(lambda n: HexBytes('%x' % (n + 1000))) gas_estimated = factory.fuzzy.FuzzyInteger(100000, 200000) gas_price_estimated = factory.fuzzy.FuzzyInteger(Web3.toWei(1, 'gwei'), Web3.toWei(20, 'gwei')) - tx_hash = factory.Sequence(lambda n: Web3.sha3(text='safe-creation-2-%d' % n)) + tx_hash = factory.Sequence(lambda n: Web3.keccak(text='safe-creation-2-%d' % n)) block_number = None @@ -93,7 +93,7 @@ class Meta: gas_limit = factory.fuzzy.FuzzyInteger(100000000, 200000000) gas_used = factory.fuzzy.FuzzyInteger(100000, 500000) timestamp = factory.LazyFunction(timezone.now) - block_hash = factory.Sequence(lambda n: Web3.sha3(text='block%d' % n)) + block_hash = factory.Sequence(lambda n: Web3.keccak(text='block%d' % n)) class EthereumTxFactory(factory.DjangoModelFactory): @@ -101,7 +101,7 @@ class Meta: model = EthereumTx block = factory.SubFactory(EthereumBlockFactory) - tx_hash = factory.Sequence(lambda n: Web3.sha3(text='ethereum_tx_hash%d' % n)) + tx_hash = factory.Sequence(lambda n: Web3.keccak(text='ethereum_tx_hash%d' % n)) _from = factory.LazyFunction(lambda: Account.create().address) gas = factory.fuzzy.FuzzyInteger(1000, 5000) gas_price = factory.fuzzy.FuzzyInteger(1, 100) @@ -127,7 +127,7 @@ class Meta: gas_token = None refund_receiver = factory.LazyFunction(lambda: Account.create().address) nonce = factory.Sequence(lambda n: n) - safe_tx_hash = factory.Sequence(lambda n: Web3.sha3(text='safe_tx_hash%d' % n)) + safe_tx_hash = factory.Sequence(lambda n: Web3.keccak(text='safe_tx_hash%d' % n)) class InternalTxFactory(factory.DjangoModelFactory): diff --git a/safe_relay_service/relay/tests/test_transaction_service.py b/safe_relay_service/relay/tests/test_transaction_service.py index 57610227..f5d1bccc 100644 --- a/safe_relay_service/relay/tests/test_transaction_service.py +++ b/safe_relay_service/relay/tests/test_transaction_service.py @@ -113,7 +113,7 @@ def test_create_multisig_tx(self): NULL_ADDRESS, NULL_ADDRESS, 0 ).buildTransaction({'from': self.ethereum_test_account.address}) - tx_hash = self.ethereum_client.send_unsigned_transaction(proxy_create_tx, private_key=self.ethereum_test_account.privateKey) + tx_hash = self.ethereum_client.send_unsigned_transaction(proxy_create_tx, private_key=self.ethereum_test_account.key) tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash, timeout=60) proxy_address = tx_receipt.contractAddress with self.assertRaises(InvalidMasterCopyAddress): diff --git a/safe_relay_service/relay/tests/test_views.py b/safe_relay_service/relay/tests/test_views.py index dff934c7..6cee587a 100644 --- a/safe_relay_service/relay/tests/test_views.py +++ b/safe_relay_service/relay/tests/test_views.py @@ -324,7 +324,7 @@ def test_safe_multisig_tx_post_gas_token(self): ).safe_tx_hash signatures = [w3.eth.account.signHash(multisig_tx_hash, private_key) - for private_key in [owner_account.privateKey]] + for private_key in [owner_account.key]] signatures_json = [{'v': s['v'], 'r': s['r'], 's': s['s']} for s in signatures] data = { diff --git a/safe_relay_service/relay/views.py b/safe_relay_service/relay/views.py index 5fbd5a89..49590a56 100644 --- a/safe_relay_service/relay/views.py +++ b/safe_relay_service/relay/views.py @@ -77,9 +77,9 @@ class AboutView(APIView): renderer_classes = (JSONRenderer,) def get(self, request, format=None): - safe_funder_public_key = Account.privateKeyToAccount(settings.SAFE_FUNDER_PRIVATE_KEY).address \ + safe_funder_public_key = Account.from_key(settings.SAFE_FUNDER_PRIVATE_KEY).address \ if settings.SAFE_FUNDER_PRIVATE_KEY else None - safe_sender_public_key = Account.privateKeyToAccount(settings.SAFE_TX_SENDER_PRIVATE_KEY).address \ + safe_sender_public_key = Account.from_key(settings.SAFE_TX_SENDER_PRIVATE_KEY).address \ if settings.SAFE_TX_SENDER_PRIVATE_KEY else None content = { 'name': 'Safe Relay Service', From 354a36a59edbb66fa22b6ca255a9e11e0610f1a8 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 17 Dec 2019 16:25:32 +0100 Subject: [PATCH 26/38] Update dependencies --- requirements-test.txt | 2 +- requirements.txt | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 0b28cbd6..f75172ab 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,5 @@ -r requirements.txt -pytest==5.3.1 +pytest==5.3.2 pytest-django==3.7.0 pytest-sugar==0.9.2 coverage==4.5.3 diff --git a/requirements.txt b/requirements.txt index ff60f8db..f3bbc462 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,19 @@ Django==2.2.8 -cachetools==3.1.1 -celery==4.3.0 +cachetools==4.0.0 +celery==4.4.0 django-authtools==1.7.0 django-celery-beat==1.5.0 django-environ==0.4.5 django-filter==2.2.0 -django-model-utils==3.2.0 -django-redis==4.10.0 +django-model-utils==4.0.0 +django-redis==4.11.0 djangorestframework-camel-case==1.1.2 -djangorestframework==3.10.3 +djangorestframework==3.11.0 docutils==0.14 drf-yasg[validation]==1.17.0 -eth-abi==1.3.0 ethereum==2.3.2 factory-boy==2.12.0 -faker==1.0.7 +faker==3.0.0 gevent==1.4.0 gnosis-py==2.0.0 gunicorn==20.0.4 From bc22d89a6ec555dcc9dacda79474acfdb6474326 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 17 Dec 2019 16:41:16 +0100 Subject: [PATCH 27/38] Fix typo --- safe_relay_service/relay/services/transaction_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index 9919c4e6..f608fc80 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -387,8 +387,8 @@ def _send_multisig_tx(self, safe_base_gas_estimation = safe.estimate_tx_base_gas(to, value, data, operation, gas_token, safe_tx_gas_estimation) if safe_tx_gas < safe_tx_gas_estimation or base_gas < safe_base_gas_estimation: - raise InvalidGasEstimation("Gas should be at least equal to safe-tx-gas=%d and data-gas=%d. Current is " - "safe-tx-gas=%d and data-gas=%d" % + raise InvalidGasEstimation("Gas should be at least equal to safe-tx-gas=%d and base-gas=%d. Current is " + "safe-tx-gas=%d and base-gas=%d" % (safe_tx_gas_estimation, safe_base_gas_estimation, safe_tx_gas, base_gas)) # We use fast tx gas price, if not txs could be stuck From eb8c931ad2ff99c548f56e7b99e4dde2083fbf77 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 18 Dec 2019 13:56:57 +0100 Subject: [PATCH 28/38] Update gnosis-py --- requirements.txt | 2 +- safe_relay_service/tokens/price_oracles.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index f3bbc462..27fff414 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==3.0.0 gevent==1.4.0 -gnosis-py==2.0.0 +gnosis-py==2.0.1 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index 1d4e4c1a..b710762d 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -113,7 +113,7 @@ def get_price(self, ticker: str) -> float: :return: price """ ethereum_client = EthereumClientProvider() - uniswap = UniswapOracle(ethereum_client.w3, self.uniswap_exchange_address) + uniswap = UniswapOracle(ethereum_client, self.uniswap_exchange_address) try: return uniswap.get_price(ticker) except OracleException as e: @@ -132,7 +132,7 @@ def get_price(self, ticker: str) -> float: :return: price """ ethereum_client = EthereumClientProvider() - kyber = KyberOracle(ethereum_client.w3, self.kyber_network_proxy_address) + kyber = KyberOracle(ethereum_client, self.kyber_network_proxy_address) try: return kyber.get_price(ticker, self.weth_token_address) except OracleException as e: From 21ba058a2f205a44a47d8e41934c20dcf6cc3724 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 19 Dec 2019 14:38:33 +0100 Subject: [PATCH 29/38] Cache token list --- safe_relay_service/relay/views.py | 6 ++---- safe_relay_service/tokens/views.py | 4 ++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/safe_relay_service/relay/views.py b/safe_relay_service/relay/views.py index 49590a56..c1e7d73f 100644 --- a/safe_relay_service/relay/views.py +++ b/safe_relay_service/relay/views.py @@ -29,8 +29,7 @@ from .serializers import ( ERC20Serializer, ERC721Serializer, EthereumTxWithInternalTxsSerializer, InternalTxWithEthereumTxSerializer, SafeBalanceResponseSerializer, - SafeContractSerializer, SafeCreationEstimateResponseSerializer, - SafeCreationEstimateSerializer, SafeCreationResponseSerializer, + SafeContractSerializer, SafeCreationResponseSerializer, SafeCreationSerializer, SafeFundingResponseSerializer, SafeMultisigEstimateTxResponseSerializer, SafeMultisigTxResponseSerializer, SafeRelayMultisigTxSerializer, SafeResponseSerializer, @@ -39,8 +38,7 @@ from .services.funding_service import FundingServiceException from .services.safe_creation_service import (SafeCreationServiceException, SafeCreationServiceProvider) -from .services.transaction_service import (SafeMultisigTxExists, - TransactionServiceException, +from .services.transaction_service import (TransactionServiceException, TransactionServiceProvider) from .tasks import fund_deployer_task diff --git a/safe_relay_service/tokens/views.py b/safe_relay_service/tokens/views.py index 25555396..5e1d76b0 100644 --- a/safe_relay_service/tokens/views.py +++ b/safe_relay_service/tokens/views.py @@ -28,3 +28,7 @@ class TokensView(ListAPIView): ordering_fields = '__all__' ordering = ('relevance', 'name') queryset = Token.objects.all() + + @method_decorator(cache_page(60 * 5)) # Cache 5 minutes + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) From d25d7d9b5bea6aa2df6d88c01255480991d5f295 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 8 Jan 2020 17:40:21 +0100 Subject: [PATCH 30/38] Set version 3.8.0 --- safe_relay_service/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_relay_service/version.py b/safe_relay_service/version.py index b1b90bea..ec69ac45 100644 --- a/safe_relay_service/version.py +++ b/safe_relay_service/version.py @@ -1,2 +1,2 @@ -__version__ = '3.7.0' +__version__ = '3.8.0' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) From 6f015c3b46d109b79c5134052f96fd23cbab953a Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 8 Jan 2020 18:28:49 +0100 Subject: [PATCH 31/38] Update test relay server script - Support V1.1.1 endpoints --- .../management/commands/test_relay_server.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/safe_relay_service/relay/management/commands/test_relay_server.py b/safe_relay_service/relay/management/commands/test_relay_server.py index c3e91241..4cb1f752 100644 --- a/safe_relay_service/relay/management/commands/test_relay_server.py +++ b/safe_relay_service/relay/management/commands/test_relay_server.py @@ -9,6 +9,7 @@ from eth_account import Account from web3 import HTTPProvider, Web3 +from gnosis.eth import EthereumClient from gnosis.eth.contracts import get_erc20_contract from gnosis.safe import SafeTx from gnosis.safe.tests.utils import generate_valid_s @@ -40,6 +41,7 @@ def send_token(w3, account, to, amount_to_send, token_address, nonce=None): class Command(BaseCommand): base_url: str w3: Web3 + ethereum_client: EthereumClient main_account: Account main_account_nonce: int @@ -50,10 +52,13 @@ def add_arguments(self, parser): # Positional arguments parser.add_argument('base_url', help='Base url of relay (e.g. http://safe-relay.gnosistest.com)') parser.add_argument('private_key', help='Private key') - parser.add_argument('--node_url', default='http://localhost:8545', + parser.add_argument('--node-url', default='http://localhost:8545', help='Ethereum node in the same net that the relay') parser.add_argument('--payment-token', help='Use payment token for creating/testing') - parser.add_argument('--create2', help='Use CREATE2 for safe creation', action='store_true', default=False) + parser.add_argument('--v2', help='Use v2 endpoints for safe V1.0.0 creation', action='store_true', + default=False) + parser.add_argument('--v3', help='Use v3 endpoints for safe V1.1.1 creation', action='store_true', + default=True) parser.add_argument('--multiple-txs', help='Test sending multiple txs at the same time', action='store_true', default=False) @@ -71,11 +76,13 @@ def get_signal_url(self, address): def handle(self, *args, **options): self.base_url = options['base_url'] - create2 = options['create2'] + v2 = options['v2'] + v3 = options['v3'] payment_token = options['payment_token'] multiple_txs = options['multiple_txs'] - self.w3 = Web3(HTTPProvider(options['node_url'])) + self.ethereum_client = EthereumClient(options['node_url']) + self.w3 = self.ethereum_client.w3 self.main_account = Account.from_key(options['private_key']) self.main_account_nonce = self.w3.eth.getTransactionCount(self.main_account.address, 'pending') main_account_balance = self.w3.eth.getBalance(self.main_account.address) @@ -84,10 +91,13 @@ def handle(self, *args, **options): about_url = urljoin(self.base_url, reverse('v1:about')) about_json = requests.get(about_url).json() - if create2: - master_copy_address = about_json['settings']['SAFE_CONTRACT_ADDRESS'] - else: + if v2: master_copy_address = about_json['settings']['SAFE_V1_0_0_CONTRACT_ADDRESS'] + elif v3: + master_copy_address = about_json['settings']['SAFE_CONTRACT_ADDRESS'] + else: # Old not CREATE2 + master_copy_address = about_json['settings']['SAFE_V0_0_1_CONTRACT_ADDRESS'] + self.stdout.write(self.style.SUCCESS(f'Using master-copy={master_copy_address}')) accounts = [Account.create() for _ in range(3)] for account in accounts: @@ -101,8 +111,7 @@ def handle(self, *args, **options): safe_addresses = [] self.stdout.write(self.style.SUCCESS('Creating multiple safes')) for _ in range(10): - safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, - create2=create2) + safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, v2=v2, v3=v3) self.fund_safe(safe_address, payment, payment_token, wait_for_receipt=False) safe_addresses.append(safe_address) @@ -122,16 +131,19 @@ def handle(self, *args, **options): timeout=500).status == 1, 'Error on tx-hash=%s' % tx_hash self.stdout.write(self.style.SUCCESS('Success with tx-hash=%s' % tx_hash)) else: - safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, create2=create2) + safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, v2=v2, v3=v3) self.fund_safe(safe_address, payment, payment_token) safe_info = self.check_safe_deployed(safe_address, owners, master_copy_address) safe_version = safe_info['version'] self.send_safe_tx(safe_address, safe_version, accounts, payment_token) def create_safe(self, owners: List[Account], threshold: int = 2, - payment_token: Optional[None] = None, create2: bool = True): - if create2: - creation_url = urljoin(self.base_url, reverse('v2:safe-creation')) + payment_token: Optional[None] = None, v2: bool = False, v3: bool = True): + if v2 or v3: + if v2: + creation_url = urljoin(self.base_url, reverse('v2:safe-creation')) + else: + creation_url = urljoin(self.base_url, reverse('v3:safe-creation')) data = { 'saltNonce': generate_valid_s(), 'owners': owners, From c82eceef5cf2e04d8e9fd5aa004bf0f249cbfb34 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Thu, 9 Jan 2020 13:41:15 +0100 Subject: [PATCH 32/38] Refactor relay server tester --- .../management/commands/test_relay_server.py | 102 ++++++------------ safe_relay_service/relay/models.py | 3 + safe_relay_service/relay/tasks.py | 3 +- .../tokens/tests/test_exchanges.py | 2 +- 4 files changed, 41 insertions(+), 69 deletions(-) diff --git a/safe_relay_service/relay/management/commands/test_relay_server.py b/safe_relay_service/relay/management/commands/test_relay_server.py index 4cb1f752..30236195 100644 --- a/safe_relay_service/relay/management/commands/test_relay_server.py +++ b/safe_relay_service/relay/management/commands/test_relay_server.py @@ -7,37 +7,13 @@ import requests from eth_account import Account -from web3 import HTTPProvider, Web3 +from web3 import Web3 from gnosis.eth import EthereumClient -from gnosis.eth.contracts import get_erc20_contract from gnosis.safe import SafeTx from gnosis.safe.tests.utils import generate_valid_s -def send_eth(w3, account, to, value, nonce=None): - tx = { - 'to': to, - 'value': value, - 'gas': 23000, - 'gasPrice': w3.eth.gasPrice, - 'nonce': nonce if nonce is not None else w3.eth.getTransactionCount(account.address, - 'pending'), - } - - signed_tx = w3.eth.account.signTransaction(tx, private_key=account.key) - return w3.eth.sendRawTransaction(signed_tx.rawTransaction) - - -def send_token(w3, account, to, amount_to_send, token_address, nonce=None): - erc20_contract = get_erc20_contract(w3, token_address) - nonce = nonce if nonce is not None else w3.eth.getTransactionCount(account.address, 'pending') - tx = erc20_contract.functions.transfer(to, amount_to_send).buildTransaction({'from': account.address, - 'nonce': nonce}) - signed_tx = w3.eth.account.signTransaction(tx, private_key=account.key) - return w3.eth.sendRawTransaction(signed_tx.rawTransaction) - - class Command(BaseCommand): base_url: str w3: Web3 @@ -55,10 +31,8 @@ def add_arguments(self, parser): parser.add_argument('--node-url', default='http://localhost:8545', help='Ethereum node in the same net that the relay') parser.add_argument('--payment-token', help='Use payment token for creating/testing') - parser.add_argument('--v2', help='Use v2 endpoints for safe V1.0.0 creation', action='store_true', - default=False) - parser.add_argument('--v3', help='Use v3 endpoints for safe V1.1.1 creation', action='store_true', - default=True) + parser.add_argument('--v2', help='Use v2 endpoints for safe V1.0.0 creation. By default V1.1.1 will be used', + action='store_true', default=False) parser.add_argument('--multiple-txs', help='Test sending multiple txs at the same time', action='store_true', default=False) @@ -77,7 +51,6 @@ def get_signal_url(self, address): def handle(self, *args, **options): self.base_url = options['base_url'] v2 = options['v2'] - v3 = options['v3'] payment_token = options['payment_token'] multiple_txs = options['multiple_txs'] @@ -86,17 +59,16 @@ def handle(self, *args, **options): self.main_account = Account.from_key(options['private_key']) self.main_account_nonce = self.w3.eth.getTransactionCount(self.main_account.address, 'pending') main_account_balance = self.w3.eth.getBalance(self.main_account.address) - self.stdout.write(self.style.SUCCESS('Using %s as main account with balance=%d' % (self.main_account.address, - main_account_balance))) + main_account_balance_eth = self.w3.fromWei(main_account_balance, 'ether') + self.stdout.write(self.style.SUCCESS(f'Using {self.main_account.address} as main account with ' + f'balance={main_account_balance_eth}')) about_url = urljoin(self.base_url, reverse('v1:about')) about_json = requests.get(about_url).json() if v2: master_copy_address = about_json['settings']['SAFE_V1_0_0_CONTRACT_ADDRESS'] - elif v3: + else: master_copy_address = about_json['settings']['SAFE_CONTRACT_ADDRESS'] - else: # Old not CREATE2 - master_copy_address = about_json['settings']['SAFE_V0_0_1_CONTRACT_ADDRESS'] self.stdout.write(self.style.SUCCESS(f'Using master-copy={master_copy_address}')) accounts = [Account.create() for _ in range(3)] @@ -111,7 +83,7 @@ def handle(self, *args, **options): safe_addresses = [] self.stdout.write(self.style.SUCCESS('Creating multiple safes')) for _ in range(10): - safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, v2=v2, v3=v3) + safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, v2=v2) self.fund_safe(safe_address, payment, payment_token, wait_for_receipt=False) safe_addresses.append(safe_address) @@ -131,37 +103,28 @@ def handle(self, *args, **options): timeout=500).status == 1, 'Error on tx-hash=%s' % tx_hash self.stdout.write(self.style.SUCCESS('Success with tx-hash=%s' % tx_hash)) else: - safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, v2=v2, v3=v3) + safe_address, payment = self.create_safe(owners, threshold=2, payment_token=payment_token, v2=v2) self.fund_safe(safe_address, payment, payment_token) safe_info = self.check_safe_deployed(safe_address, owners, master_copy_address) safe_version = safe_info['version'] self.send_safe_tx(safe_address, safe_version, accounts, payment_token) - def create_safe(self, owners: List[Account], threshold: int = 2, - payment_token: Optional[None] = None, v2: bool = False, v3: bool = True): - if v2 or v3: - if v2: - creation_url = urljoin(self.base_url, reverse('v2:safe-creation')) - else: - creation_url = urljoin(self.base_url, reverse('v3:safe-creation')) - data = { - 'saltNonce': generate_valid_s(), - 'owners': owners, - 'threshold': threshold, - } - if payment_token: - data['paymentToken'] = payment_token + def create_safe(self, owners: List[Account], threshold: int = 2, payment_token: Optional[None] = None, + v2: bool = False): + if v2: + creation_url = urljoin(self.base_url, reverse('v2:safe-creation')) else: - creation_url = urljoin(self.base_url, reverse('v1:safe-creation')) - data = { - 's': generate_valid_s(), - 'owners': owners, - 'threshold': threshold, - } - if payment_token: - data['paymentToken'] = payment_token + creation_url = urljoin(self.base_url, reverse('v3:safe-creation')) + data = { + 'saltNonce': generate_valid_s(), + 'owners': owners, + 'threshold': threshold, + } + if payment_token: + data['paymentToken'] = payment_token + self.stdout.write(self.style.SUCCESS(f'Calling creation url {creation_url}')) r = requests.post(creation_url, json=data) - assert r.ok, "Error creating safe %s" % r.content + assert r.ok, f"Error creating safe {r.content} using url f{creation_url}" safe_address, payment = r.json()['safe'], int(r.json()['payment']) return safe_address, payment @@ -169,16 +132,18 @@ def create_safe(self, owners: List[Account], threshold: int = 2, def fund_safe(self, safe_address, payment, payment_token: Optional[None] = None, wait_for_receipt: bool = True): self.stdout.write(self.style.SUCCESS('Created safe=%s, need payment=%d' % (safe_address, payment))) if payment_token: - tx_hash = send_token(self.w3, self.main_account, safe_address, int(payment * 1.4), payment_token, - nonce=self.main_account_nonce) + tx_hash = self.ethereum_client.erc20.send_tokens(safe_address, int(payment * 1.4), payment_token, + self.main_account.key, nonce=self.main_account_nonce) self.main_account_nonce += 1 self.stdout.write(self.style.SUCCESS('Sent payment of payment-token=%s, waiting for ' 'receipt with tx-hash=%s' % (payment_token, tx_hash.hex()))) self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=500) - tx_hash = send_eth(self.w3, self.main_account, safe_address, payment * 2, nonce=self.main_account_nonce) + tx_hash = self.ethereum_client.send_eth_to(self.main_account.key, safe_address, + self.ethereum_client.w3.eth.gasPrice, payment * 2, + nonce=self.main_account_nonce) self.main_account_nonce += 1 - self.stdout.write(self.style.SUCCESS('Sent payment * 2, waiting for receipt with tx-hash=%s' % tx_hash.hex())) + self.stdout.write(self.style.SUCCESS(f'Sent payment * 2, waiting for receipt with tx-hash={tx_hash.hex()}')) if wait_for_receipt: self.w3.eth.waitForTransactionReceipt(tx_hash, timeout=500) self.stdout.write(self.style.SUCCESS('Payment sent and mined. Waiting for safe to be deployed')) @@ -193,7 +158,7 @@ def check_safe_deployed(self, safe_address, owners, master_copy_address) -> Dict break time.sleep(10) # Check safe was created successfully - self.stdout.write(self.style.SUCCESS('Safe=%s was deployed' % safe_address)) + self.stdout.write(self.style.SUCCESS(f'Safe={safe_address} was deployed')) r = requests.get(self.get_safe_url(safe_address)) assert r.ok, "Safe deployed is not working" safe_info = r.json() @@ -201,6 +166,7 @@ def check_safe_deployed(self, safe_address, owners, master_copy_address) -> Dict assert safe_info['threshold'] == 2 assert safe_info['nonce'] == 0 assert safe_info['masterCopy'] == master_copy_address + self.stdout.write(self.style.SUCCESS(safe_info)) return safe_info @@ -262,8 +228,10 @@ def send_safe_tx(self, safe_address: str, safe_version: str, accounts: List[Acco def send_multiple_txs(self, safe_address: str, safe_version: str, accounts: List[Account], payment_token: Optional[str] = None, number_txs: int = 100) -> List[bytes]: - tx_hash = send_eth(self.w3, self.main_account, safe_address, self.w3.toWei(1, 'ether'), - nonce=self.main_account_nonce) + tx_hash = self.ethereum_client.send_eth_to(self.main_account.key, safe_address, + self.ethereum_client.w3.eth.gasPrice, + self.w3.toWei(1, 'ether'), + nonce=self.main_account_nonce) self.main_account_nonce += 1 self.stdout.write(self.style.SUCCESS('Sent 1 ether for testing sending multiple txs, ' diff --git a/safe_relay_service/relay/models.py b/safe_relay_service/relay/models.py index f4af8d61..be73c405 100644 --- a/safe_relay_service/relay/models.py +++ b/safe_relay_service/relay/models.py @@ -287,6 +287,9 @@ class EthereumBlock(models.Model): timestamp = models.DateTimeField() block_hash = Sha3HashField(unique=True) + def __str__(self): + return f'Block={self.number} on {self.timestamp}' + class EthereumTxManager(models.Manager): def create_from_tx(self, tx: Dict[str, Any], tx_hash: Union[bytes, str], gas_used: Optional[int] = None, diff --git a/safe_relay_service/relay/tasks.py b/safe_relay_service/relay/tasks.py index 71a7a952..3bc24da0 100644 --- a/safe_relay_service/relay/tasks.py +++ b/safe_relay_service/relay/tasks.py @@ -292,7 +292,8 @@ def check_create2_deployed_safes_task() -> None: safe_creation2.block_number = block_number safe_creation2.save() else: - # If safe was not included in any block after 35 minutes (mempool limit is 30), we try to deploy it again + # If safe was not included in any block after 35 minutes (mempool limit is 30) + # we try to deploy it again if safe_creation2.modified + timedelta(minutes=35) < timezone.now(): logger.info('Safe=%s with tx-hash=%s was not deployed after 10 minutes', safe_address, safe_creation2.tx_hash) diff --git a/safe_relay_service/tokens/tests/test_exchanges.py b/safe_relay_service/tokens/tests/test_exchanges.py index be87b91d..11b4a46f 100644 --- a/safe_relay_service/tokens/tests/test_exchanges.py +++ b/safe_relay_service/tokens/tests/test_exchanges.py @@ -41,7 +41,7 @@ def test_dutchx(self): # Gno address is 0x6810e776880C02933D47DB1b9fc05908e5386b96 self.exchange_helper(exchange, [# 'WETH-0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', # DAI - # '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359-WETH', # DAI + '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359-WETH', # DAI # 'WETH-0x6810e776880C02933D47DB1b9fc05908e5386b96' '0x6810e776880C02933D47DB1b9fc05908e5386b96-WETH', ], From ffe0b9e10581c73aae1a3766ffa04ca1cee34504 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 10 Jan 2020 15:31:27 +0100 Subject: [PATCH 33/38] Fix storing tx_hash as safe_tx_hash --- docker/web/run_web.sh | 1 + .../management/commands/fix_safe_tx_hash.py | 16 ++++++++++++++++ safe_relay_service/relay/models.py | 9 ++++++++- .../relay/services/transaction_service.py | 2 +- .../tokens/tests/test_exchanges.py | 1 + 5 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 safe_relay_service/relay/management/commands/fix_safe_tx_hash.py diff --git a/docker/web/run_web.sh b/docker/web/run_web.sh index a8d1d7c2..0c982faf 100755 --- a/docker/web/run_web.sh +++ b/docker/web/run_web.sh @@ -4,6 +4,7 @@ set -euo pipefail echo "==> Migrating Django models..." python manage.py migrate --noinput +python manage.py fix_safe_tx_hash echo "==> Setup Gas Station..." python manage.py setup_gas_station echo "==> Setup Safe Relay Task..." diff --git a/safe_relay_service/relay/management/commands/fix_safe_tx_hash.py b/safe_relay_service/relay/management/commands/fix_safe_tx_hash.py new file mode 100644 index 00000000..d825c2ec --- /dev/null +++ b/safe_relay_service/relay/management/commands/fix_safe_tx_hash.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from django.db.models import F + +from ...models import SafeMultisigTx + + +class Command(BaseCommand): + help = 'Fix SafeTxHash typo' + + def handle(self, *args, **options): + for safe_multisig_tx in SafeMultisigTx.objects.filter(ethereum_tx_id=F('safe_tx_hash')): + safe_tx = safe_multisig_tx.get_safe_tx() + self.stdout.write(self.style.SUCCESS(f'Fixing safe-multisig-tx with ' + f'tx-hash={safe_multisig_tx.ethereum_tx_id}')) + safe_multisig_tx.safe_tx_hash = safe_tx.safe_tx_hash + safe_multisig_tx.save(update_fields=['safe_tx_hash']) diff --git a/safe_relay_service/relay/models.py b/safe_relay_service/relay/models.py index be73c405..7ba9ae97 100644 --- a/safe_relay_service/relay/models.py +++ b/safe_relay_service/relay/models.py @@ -15,7 +15,7 @@ from gnosis.eth.constants import ERC20_721_TRANSFER_TOPIC from gnosis.eth.django.models import (EthereumAddressField, Sha3HashField, Uint256Field) -from gnosis.safe import SafeOperation +from gnosis.safe import SafeOperation, SafeTx from .models_raw import SafeContractManagerRaw, SafeContractQuerySetRaw @@ -424,6 +424,13 @@ def __str__(self): return '{} - {} - Safe {}'.format(self.ethereum_tx.tx_hash, SafeOperation(self.operation).name, self.safe.address) + def get_safe_tx(self) -> SafeTx: + return SafeTx(None, self.safe_id, self.to, self.value, self.data.tobytes() if self.data else b'', + self.operation, self.safe_tx_gas, self.data_gas, self.gas_price, self.gas_token, + self.refund_receiver, + signatures=self.signatures.tobytes() if self.signatures else b'', + safe_nonce=self.nonce) + class InternalTxManager(models.Manager): def get_or_create_from_trace(self, trace: Dict[str, Any], ethereum_tx: EthereumTx): diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index f608fc80..7c002e3c 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -421,7 +421,7 @@ def _send_multisig_tx(self, timeout=60 * 2) as tx_nonce: tx_hash, tx = safe_tx.execute(tx_sender_private_key, tx_gas=tx_gas, tx_gas_price=tx_gas_price, tx_nonce=tx_nonce, block_identifier=block_identifier) - return tx_hash, safe_tx.tx_hash, tx + return tx_hash, safe_tx.safe_tx_hash, tx def get_pending_multisig_transactions(self, older_than: int) -> List[SafeMultisigTx]: """ diff --git a/safe_relay_service/tokens/tests/test_exchanges.py b/safe_relay_service/tokens/tests/test_exchanges.py index 11b4a46f..a10a6210 100644 --- a/safe_relay_service/tokens/tests/test_exchanges.py +++ b/safe_relay_service/tokens/tests/test_exchanges.py @@ -35,6 +35,7 @@ def test_binance(self): exchange = Binance() self.exchange_helper(exchange, ['BTCUSDT', 'ETHUSDT'], ['BADTICKER']) + @pytest.mark.xfail def test_dutchx(self): exchange = DutchX() # Dai address is 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 From 2fa44b2f893884f1eff0cd29a4c88b0714bcd38d Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 10 Jan 2020 16:08:48 +0100 Subject: [PATCH 34/38] Set version 3.8.1 --- safe_relay_service/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_relay_service/version.py b/safe_relay_service/version.py index ec69ac45..def2ae0c 100644 --- a/safe_relay_service/version.py +++ b/safe_relay_service/version.py @@ -1,2 +1,2 @@ -__version__ = '3.8.0' +__version__ = '3.8.1' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) From a6ce340d54441e1d552e503ef5aaf600c1fadf4f Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 10 Jan 2020 16:19:06 +0100 Subject: [PATCH 35/38] Remove not needed fix --- docker/web/run_web.sh | 1 - .../management/commands/fix_safe_tx_hash.py | 16 ---------------- 2 files changed, 17 deletions(-) delete mode 100644 safe_relay_service/relay/management/commands/fix_safe_tx_hash.py diff --git a/docker/web/run_web.sh b/docker/web/run_web.sh index 0c982faf..a8d1d7c2 100755 --- a/docker/web/run_web.sh +++ b/docker/web/run_web.sh @@ -4,7 +4,6 @@ set -euo pipefail echo "==> Migrating Django models..." python manage.py migrate --noinput -python manage.py fix_safe_tx_hash echo "==> Setup Gas Station..." python manage.py setup_gas_station echo "==> Setup Safe Relay Task..." diff --git a/safe_relay_service/relay/management/commands/fix_safe_tx_hash.py b/safe_relay_service/relay/management/commands/fix_safe_tx_hash.py deleted file mode 100644 index d825c2ec..00000000 --- a/safe_relay_service/relay/management/commands/fix_safe_tx_hash.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.core.management.base import BaseCommand -from django.db.models import F - -from ...models import SafeMultisigTx - - -class Command(BaseCommand): - help = 'Fix SafeTxHash typo' - - def handle(self, *args, **options): - for safe_multisig_tx in SafeMultisigTx.objects.filter(ethereum_tx_id=F('safe_tx_hash')): - safe_tx = safe_multisig_tx.get_safe_tx() - self.stdout.write(self.style.SUCCESS(f'Fixing safe-multisig-tx with ' - f'tx-hash={safe_multisig_tx.ethereum_tx_id}')) - safe_multisig_tx.safe_tx_hash = safe_tx.safe_tx_hash - safe_multisig_tx.save(update_fields=['safe_tx_hash']) From 21d1c2b8081c6209ea8148ba329035645574227c Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 14 Jan 2020 11:13:57 +0100 Subject: [PATCH 36/38] Fix KyberOracle --- requirements-test.txt | 2 +- requirements.txt | 4 ++-- safe_relay_service/tokens/price_oracles.py | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index f75172ab..6ec15fe4 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,5 @@ -r requirements.txt pytest==5.3.2 -pytest-django==3.7.0 +pytest-django==3.8.0 pytest-sugar==0.9.2 coverage==4.5.3 diff --git a/requirements.txt b/requirements.txt index 27fff414..533566ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,11 +15,11 @@ ethereum==2.3.2 factory-boy==2.12.0 faker==3.0.0 gevent==1.4.0 -gnosis-py==2.0.1 +gnosis-py==2.0.3 gunicorn==20.0.4 hexbytes==0.2.0 lxml==4.4.1 -numpy==1.17.4 +numpy==1.18.1 packaging>=19.0 psycopg2-binary==2.8.4 redis==3.3.11 diff --git a/safe_relay_service/tokens/price_oracles.py b/safe_relay_service/tokens/price_oracles.py index b710762d..e7048d9a 100644 --- a/safe_relay_service/tokens/price_oracles.py +++ b/safe_relay_service/tokens/price_oracles.py @@ -103,7 +103,7 @@ def get_price(self, ticker) -> float: class Uniswap(PriceOracle): - def __init__(self, uniswap_exchange_address: str): + def __init__(self, uniswap_exchange_address: str, **kwargs): self.uniswap_exchange_address = uniswap_exchange_address @cached(cache=TTLCache(maxsize=1024, ttl=60)) @@ -121,9 +121,8 @@ def get_price(self, ticker: str) -> float: class Kyber(PriceOracle): - def __init__(self, kyber_network_proxy_address: str, weth_token_address: str): + def __init__(self, kyber_network_proxy_address: str, **kwargs): self.kyber_network_proxy_address = kyber_network_proxy_address - self.weth_token_address = weth_token_address @cached(cache=TTLCache(maxsize=1024, ttl=60)) def get_price(self, ticker: str) -> float: @@ -134,7 +133,7 @@ def get_price(self, ticker: str) -> float: ethereum_client = EthereumClientProvider() kyber = KyberOracle(ethereum_client, self.kyber_network_proxy_address) try: - return kyber.get_price(ticker, self.weth_token_address) + return kyber.get_price(ticker) except OracleException as e: raise CannotGetTokenPriceFromApi from e From fb1f6ca8a4609c86c0ef64485e38146c16d8469d Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 15 Jan 2020 11:15:50 +0100 Subject: [PATCH 37/38] Update and check pending txs - Remove tracing --- .../management/commands/setup_service.py | 20 ++++++++++-- .../relay/services/transaction_service.py | 32 +++++++++++++++++-- safe_relay_service/relay/tasks.py | 22 +++++++++++++ safe_relay_service/relay/tests/test_tasks.py | 19 +++++++++-- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/safe_relay_service/relay/management/commands/setup_service.py b/safe_relay_service/relay/management/commands/setup_service.py index 811cb5a1..e908f878 100644 --- a/safe_relay_service/relay/management/commands/setup_service.py +++ b/safe_relay_service/relay/management/commands/setup_service.py @@ -19,6 +19,10 @@ def create_task(self) -> Tuple[PeriodicTask, bool]: 'interval': interval }) + def delete_task(self) -> int: + deleted, _ = PeriodicTask.objects.filter(task=self.name).delete() + return deleted + class Command(BaseCommand): help = 'Setup service required tasks' @@ -28,14 +32,19 @@ class Command(BaseCommand): 'Check Balance of realy accounts', 1, IntervalSchedule.HOURS), CeleryTaskConfiguration('safe_relay_service.relay.tasks.check_create2_deployed_safes_task', 'Check and deploy Create2 Safes', 1, IntervalSchedule.MINUTES), - CeleryTaskConfiguration('safe_relay_service.relay.tasks.find_internal_txs_task', - 'Process Internal Txs for Safes', 2, IntervalSchedule.MINUTES), CeleryTaskConfiguration('safe_relay_service.relay.tasks.find_erc_20_721_transfers_task', 'Process ERC20/721 transfers for Safes', 2, IntervalSchedule.MINUTES), CeleryTaskConfiguration('safe_relay_service.relay.tasks.check_pending_transactions', 'Check transactions not mined after a while', 10, IntervalSchedule.MINUTES), + CeleryTaskConfiguration('safe_relay_service.relay.tasks.check_and_update_pending_transactions', + 'Check and update transactions when mined', 1, IntervalSchedule.MINUTES), ] + tasks_to_delete = [ + CeleryTaskConfiguration('safe_relay_service.relay.tasks.find_internal_txs_task', + 'Process Internal Txs for Safes', 2, IntervalSchedule.MINUTES), + ] + def handle(self, *args, **options): for task in self.tasks: _, created = task.create_task() @@ -43,3 +52,10 @@ def handle(self, *args, **options): self.stdout.write(self.style.SUCCESS('Created Periodic Task %s' % task.name)) else: self.stdout.write(self.style.SUCCESS('Task %s was already created' % task.name)) + + for task in self.tasks_to_delete: + deleted = task.delete_task() + if deleted: + self.stdout.write(self.style.SUCCESS('Deleted Periodic Task %s' % task.name)) + else: + self.stdout.write(self.style.SUCCESS('Task %s was already deleted' % task.name)) diff --git a/safe_relay_service/relay/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index 7c002e3c..633f489f 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -21,7 +21,7 @@ from safe_relay_service.tokens.models import Token from safe_relay_service.tokens.price_oracles import CannotGetTokenPriceFromApi -from ..models import EthereumTx, SafeContract, SafeMultisigTx +from ..models import EthereumBlock, EthereumTx, SafeContract, SafeMultisigTx from ..repositories.redis_repository import EthereumNonceLock, RedisRepository logger = getLogger(__name__) @@ -425,7 +425,7 @@ def _send_multisig_tx(self, def get_pending_multisig_transactions(self, older_than: int) -> List[SafeMultisigTx]: """ - Get multisig txs that have not been mined after X seconds + Get multisig txs that have not been mined after `older_than` seconds :param older_than: Time in seconds for a tx to be considered pending, if 0 all will be returned """ return SafeMultisigTx.objects.filter( @@ -433,3 +433,31 @@ def get_pending_multisig_transactions(self, older_than: int) -> List[SafeMultisi ).filter( created__lte=timezone.now() - timedelta(seconds=older_than), ) + + # TODO Refactor and test + def create_or_update_ethereum_tx(self, tx_hash: str) -> EthereumTx: + try: + ethereum_tx = EthereumTx.objects.get(tx_hash=tx_hash) + if ethereum_tx.block is None: + tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash) + if tx_receipt: + ethereum_tx.block = self.get_or_create_ethereum_block(tx_receipt.blockNumber) + ethereum_tx.gas_used = tx_receipt.gasUsed + ethereum_tx.save() + return ethereum_tx + except EthereumTx.DoesNotExist: + tx = self.ethereum_client.get_transaction(tx_hash) + if tx: + if tx_receipt: + tx_receipt = self.ethereum_client.get_transaction_receipt(tx_hash) + ethereum_block = self.get_or_create_ethereum_block(tx_receipt.blockNumber) + return EthereumTx.objects.create_from_tx(tx, tx_hash, tx_receipt.gasUsed, ethereum_block) + return EthereumTx.objects.create_from_tx(tx, tx_hash) + + # TODO Refactor and test + def get_or_create_ethereum_block(self, block_number: int): + try: + return EthereumBlock.objects.get(number=block_number) + except EthereumBlock.DoesNotExist: + block = self.ethereum_client.get_block(block_number) + return EthereumBlock.objects.create_from_block(block) diff --git a/safe_relay_service/relay/tasks.py b/safe_relay_service/relay/tasks.py index 3bc24da0..3648262f 100644 --- a/safe_relay_service/relay/tasks.py +++ b/safe_relay_service/relay/tasks.py @@ -392,3 +392,25 @@ def check_pending_transactions() -> int: except LockError: pass return number_txs + + +@app.shared_task(soft_time_limit=60) +def check_and_update_pending_transactions() -> int: + """ + Check if pending txs have been mined and update them + :return: Number of pending transactions + """ + number_txs = 0 + try: + redis = RedisRepository().redis + with redis.lock('tasks:check_and_update_pending_transactions', blocking_timeout=1, timeout=60): + transaction_service = TransactionServiceProvider() + txs = TransactionServiceProvider().get_pending_multisig_transactions(older_than=15) + for tx in txs: + ethereum_tx = transaction_service.create_or_update_ethereum_tx(tx.ethereum_tx_id) + if ethereum_tx and ethereum_tx.block_id: + logger.info('Updated tx with tx-hash=%s and block=%d', ethereum_tx.tx_hash, ethereum_tx.block_id) + number_txs += 1 + except LockError: + pass + return number_txs diff --git a/safe_relay_service/relay/tests/test_tasks.py b/safe_relay_service/relay/tests/test_tasks.py index 58863aa8..bf11638e 100644 --- a/safe_relay_service/relay/tests/test_tasks.py +++ b/safe_relay_service/relay/tests/test_tasks.py @@ -5,9 +5,12 @@ from django.test import TestCase from django.utils import timezone -from ..models import SafeContract, SafeFunding +from eth_account import Account + +from ..models import EthereumTx, SafeContract, SafeFunding from ..services import Erc20EventsServiceProvider, InternalTxServiceProvider -from ..tasks import (check_balance_of_accounts_task, +from ..tasks import (check_and_update_pending_transactions, + check_balance_of_accounts_task, check_deployer_funded_task, check_pending_transactions, deploy_create2_safe_task, deploy_safes_task, find_erc_20_721_transfers_task, find_internal_txs_task, @@ -94,3 +97,15 @@ def test_check_pending_transactions(self): SafeMultisigTxFactory(created=timezone.now() - timedelta(minutes=not_mined_alert_minutes + 1), ethereum_tx__block=None) self.assertEqual(check_pending_transactions.delay().get(), 1) + + def test_check_and_update_pending_transactions(self): + SafeMultisigTxFactory(created=timezone.now() - timedelta(seconds=16), + ethereum_tx__block=None) + self.assertEqual(check_and_update_pending_transactions.delay().get(), 0) + + tx_hash = self.send_ether(Account.create().address, 1) + SafeMultisigTxFactory(created=timezone.now() - timedelta(seconds=16), + ethereum_tx__tx_hash=tx_hash, + ethereum_tx__block=None) + self.assertEqual(check_and_update_pending_transactions.delay().get(), 1) + self.assertGreaterEqual(EthereumTx.objects.get(tx_hash=tx_hash).block_id, 0) From 4f2e48bf00956712f4bffe024274e7e215ce7266 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 15 Jan 2020 11:52:49 +0100 Subject: [PATCH 38/38] Set version 3.8.2 --- safe_relay_service/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_relay_service/version.py b/safe_relay_service/version.py index def2ae0c..723bebec 100644 --- a/safe_relay_service/version.py +++ b/safe_relay_service/version.py @@ -1,2 +1,2 @@ -__version__ = '3.8.1' +__version__ = '3.8.2' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])