diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..4de61913 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,117 @@ +version: 2.1 + +templates: + tagged-filter: &tagged-filter + tags: + only: /^[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)?(\.dev[0-9]+)?/ + +executors: + ubuntu-builder: + docker: + - image: trustlines/builder:master61 + resource_class: + medium + working_directory: ~/repo + +jobs: + deploy-docker-image: + executor: ubuntu-builder + environment: + DOCKER_REPO: trustlines/relay + LOCAL_IMAGE: relay + working_directory: ~/repo + steps: + - checkout + - setup_remote_docker: + version: 20.10.7 + - attach_workspace: + at: '~' + - run: + name: Login to dockerhub + command: | + echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USER" --password-stdin + - run: + name: Upload tagged version + environment: + DOCKERHUB_PROJECT: safe-relay-service + command: | + bash scripts/deploy_docker.sh staging + + deploy-docker-image-release: + executor: ubuntu-builder + environment: + DOCKER_REPO: trustlines/relay + LOCAL_IMAGE: relay + working_directory: ~/repo + steps: + - checkout + - setup_remote_docker: + version: 20.10.7 + - attach_workspace: + at: "~" + - run: + name: Login to dockerhub + command: | + echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USER" --password-stdin + - run: + name: Upload tagged release + environment: + DOCKERHUB_PROJECT: safe-relay-service + command: | + bash scripts/deploy_docker.sh $CIRCLE_TAG + +# run-end2end-tests: +# executor: ubuntu-builder +# environment: +# DOCKER_REPO: trustlines/relay +# LOCAL_IMAGE: relay +# working_directory: ~ +# steps: +# - config-path +# - setup_remote_docker: +# version: 20.10.7 +# - attach_workspace: +# at: '~' +# - run: +# name: Checkout end2end repo +# command: | +# git clone https://github.com/trustlines-protocol/end2end.git +# - run: +# name: Load docker image +# command: | +# docker load --input ~/images/$LOCAL_IMAGE.tar +# - run: +# name: run end2end tests +# command: | +# docker tag $LOCAL_IMAGE $DOCKER_REPO +# cd end2end +# ./run-e2e.sh +# - run: +# name: copy out the end2end coverage file from remote docker to host +# command: | +# scp circleci@remote-docker:project/end2end/end2end-coverage/coverage.xml /home/circleci/repo/coverage.xml +# - run: +# name: upload end2end codecov +# command: | +# cd ~/repo +# codecov --file coverage.xml + + + +workflows: + version: 2 + default: + jobs: + + - deploy-docker-image: + filters: + branches: + only: safe-relay-fork + context: docker-credentials + + - deploy-docker-image-release: + filters: + <<: *tagged-filter + branches: + ignore: /.*/ + context: docker-credentials diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index bcfa0e56..935bd606 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -2,6 +2,23 @@ name: Python CI on: [push, pull_request] jobs: + linting: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install pre-commit + - name: Run pre-commit + run: pre-commit run --all-files + test-app: runs-on: ubuntu-latest strategy: @@ -35,31 +52,17 @@ jobs: docker run --detach --publish 8545:8545 --network-alias ganache -e DOCKER=true trufflesuite/ganache:latest --defaultBalanceEther 10000 --gasLimit 10000000 -a 30 --chain.chainId 1337 --chain.networkId 1337 -d - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 - name: Cache pip - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-test.txt') }}-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - uses: actions/cache@v3 - name: Cache pre-commit - with: - path: ~/.cache/pre-commit - key: ${{ runner.os }}-precommit-${{ hashFiles('**/.pre-commit-config.yaml') }} - restore-keys: | - ${{ runner.os }}-precommit- + cache: 'pip' + cache-dependency-path: 'requirements*.txt' - name: Install dependencies run: | pip install wheel - pip install -r requirements-test.txt coveralls pre-commit + pip install -r requirements-test.txt coveralls env: PIP_USE_MIRRORS: true - - name: Run pre-commit - run: pre-commit run --all-files - name: Run tests and coverage run: | python manage.py check @@ -67,12 +70,13 @@ jobs: coverage run --source=$SOURCE_FOLDER -m py.test -rxXs env: SOURCE_FOLDER: safe_relay_service + CELERY_BROKER_URL: redis://localhost:6379/0 DJANGO_SETTINGS_MODULE: config.settings.test DATABASE_URL: psql://postgres:postgres@localhost/postgres ETHEREUM_NODE_URL: http://localhost:8545 ETHEREUM_TRACING_NODE_URL: http://localhost:8545 + ETH_HASH_BACKEND: pysha3 REDIS_URL: redis://localhost:6379/0 - CELERY_BROKER_URL: redis://localhost:6379/0 - name: Send results to coveralls continue-on-error: true # Ignore coveralls problems run: coveralls --service=github @@ -80,12 +84,14 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Required for coveralls docker-deploy: runs-on: ubuntu-latest - needs: test-app + needs: + - linting + - test-app if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/') steps: - uses: actions/checkout@v3 - name: Dockerhub login - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} @@ -104,23 +110,3 @@ jobs: run: bash scripts/deploy_docker.sh ${GITHUB_REF##*/} env: DOCKERHUB_PROJECT: safe-relay-service - autodeploy: - runs-on: ubuntu-latest - needs: [docker-deploy] - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/') - steps: - - uses: actions/checkout@v3 - - name: Deploy Staging - if: github.ref == 'refs/heads/master' - run: bash scripts/autodeploy.sh - env: - AUTODEPLOY_URL: ${{ secrets.AUTODEPLOY_URL }} - AUTODEPLOY_TOKEN: ${{ secrets.AUTODEPLOY_TOKEN }} - TARGET_BRANCH: "staging" - - name: Deploy Develop - if: github.ref == 'refs/heads/develop' - run: bash scripts/autodeploy.sh - env: - AUTODEPLOY_URL: ${{ secrets.AUTODEPLOY_URL }} - AUTODEPLOY_TOKEN: ${{ secrets.AUTODEPLOY_TOKEN }} - TARGET_BRANCH: "develop" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3a418b27..c0b88c79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,15 +6,15 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.10.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.3.0 hooks: - id: check-docstring-first - id: check-merge-conflict diff --git a/README.md b/README.md index af0062e1..e5827451 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,6 @@ How is this possible? The **Transaction Relay Service** acts as a proxy, paying back due to the transaction architecture we use. It also enables the user to pay for ethereum transactions using **ERC20 tokens**. -Docs ----- -Docs are available on [Gnosis Docs](https://docs.gnosis.io/safe/docs/services_relay/) -You can open the diagrams explaining _Pre CREATE2_ deployment under `docs/` with [Staruml](http://staruml.io/) - Setup for development (using ganache) ------------------------------------- This is the recommended configuration for developing and testing the Relay service. `docker-compose` is required for diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3bec02ee..63d24ad8 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -29,7 +29,7 @@ services: command: docker/web/celery/worker/run.sh ganache: - image: trufflesuite/ganache-cli - command: -d --defaultBalanceEther 10000 --gasLimit 10000000 -a 30 --chain.chainId 1337 --chain.networkId 1337 + image: trufflesuite/ganache:latest + command: --defaultBalanceEther 10000 --gasLimit 10000000 -a 30 --chain.chainId 1337 --chain.networkId 1337 -d ports: - "8545:8545" diff --git a/docker-compose.yml b/docker-compose.yml index 54a3bedd..63ead8e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ volumes: services: nginx: - image: nginx:1.21-alpine + image: nginx:1-alpine hostname: nginx ports: - "8000:8000" @@ -16,7 +16,7 @@ services: - web redis: - image: redis:5-alpine + image: redis:7-alpine ports: - "6379:6379" @@ -28,6 +28,7 @@ services: POSTGRES_PASSWORD: postgres web: + platform: linux/amd64 build: context: . dockerfile: docker/web/Dockerfile @@ -44,6 +45,7 @@ services: command: docker/web/run_web.sh worker: &worker + platform: linux/amd64 build: context: . dockerfile: docker/web/Dockerfile @@ -55,6 +57,7 @@ services: command: docker/web/celery/worker/run.sh scheduler: + platform: linux/amd64 <<: *worker command: docker/web/celery/scheduler/run.sh diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index 6fd7140f..522d65ff 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -1,23 +1,24 @@ FROM python:3.10-slim -ENV PYTHONUNBUFFERED 1 -WORKDIR /app +ARG APP_HOME=/app +WORKDIR ${APP_HOME} +ENV PYTHONUNBUFFERED=1 -# Signal handling for PID1 https://github.com/krallin/tini -ENV TINI_VERSION v0.19.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini +# https://eth-hash.readthedocs.io/en/latest/quickstart.html#specify-backend-by-environment-variable +# `pysha3` is way faster than `pycryptodome` for CPython +ENV ETH_HASH_BACKEND=pysha3 COPY requirements.txt ./ RUN set -ex \ && buildDeps=" \ build-essential \ + git \ libssl-dev \ - libgmp-dev \ - pkg-config \ + libpq-dev \ " \ && apt-get update \ - && apt-get install -y --no-install-recommends $buildDeps \ + && apt-get install -y --no-install-recommends $buildDeps tmux postgresql-client \ + && pip install -U --no-cache-dir wheel setuptools pip \ && pip install --no-cache-dir -r requirements.txt \ && apt-get purge -y --auto-remove $buildDeps \ && rm -rf /var/lib/apt/lists/* \ @@ -28,5 +29,3 @@ RUN set -ex \ COPY . . RUN DJANGO_SETTINGS_MODULE=config.settings.local DJANGO_DOT_ENV_FILE=.env.local python manage.py collectstatic --noinput - -ENTRYPOINT ["/tini", "--"] diff --git a/requirements-test.txt b/requirements-test.txt index 1a46ef8b..43bb78dd 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,11 +1,11 @@ -r requirements.txt -coverage==6.3.2 -django-stubs==1.10.1 +coverage==6.5.0 +django-stubs==1.13.0 factory-boy==3.2.1 -faker==13.3.3 -mypy==0.942 -pytest==7.1.1 +faker==15.1.1 +mypy==0.982 +pytest==7.1.3 pytest-celery==0.0.0 pytest-django==4.5.2 -pytest-env==0.6.2 -pytest-sugar==0.9.4 +pytest-env==0.8.1 +pytest-sugar==0.9.6 diff --git a/requirements.txt b/requirements.txt index eeb9d981..8bb77002 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,26 @@ -cachetools==5.0.0 -celery==5.2.3 -django==3.2.12 -django-authtools==1.7.0 -django-celery-beat==2.2.1 +cachetools==5.2.0 +celery==5.2.7 +django==4.1.3 +django-authtools==2.0.0 +django-celery-beat==2.4.0 django-debug-toolbar django-debug-toolbar-force -django-environ==0.8.1 -django-filter==21.1 +django-environ==0.9.0 +django-filter==22.1 django-model-utils==4.2.0 django-redis==5.2.0 -django-timezone-field==4.2.3 -djangorestframework==3.13.1 +django-timezone-field==5.0 +djangorestframework==3.14.0 djangorestframework-camel-case==1.3.0 -docutils==0.18.1 -drf-yasg[validation]==1.20.0 -gnosis-py[django]==3.9.2 +docutils==0.19 +drf-yasg[validation]==1.21.4 gunicorn[gevent]==20.1.0 -hexbytes==0.2.2 -lxml==4.8.0 -numpy==1.22.3 +lxml==4.9.1 +numpy==1.23.4 packaging==21.3 psycogreen==1.0.2 -psycopg2-binary==2.9.3 -redis==4.2.1 -requests==2.27.1 -web3==5.28.0 +psycopg2==2.9.5 +redis==4.3.4 +requests==2.28.1 +safe-eth-py[django]==4.6.0 +web3==5.31.1 diff --git a/safe_relay_service/relay/management/commands/setup_service.py b/safe_relay_service/relay/management/commands/setup_service.py index 6a3b7467..eab30e5c 100644 --- a/safe_relay_service/relay/management/commands/setup_service.py +++ b/safe_relay_service/relay/management/commands/setup_service.py @@ -30,20 +30,20 @@ class Command(BaseCommand): CeleryTaskConfiguration( "safe_relay_service.relay.tasks.deploy_safes_task", "Deploy Safes", - 20, + 5, IntervalSchedule.SECONDS, ), CeleryTaskConfiguration( "safe_relay_service.relay.tasks.check_balance_of_accounts_task", - "Check Balance of realy accounts", + "Check Balance of relay accounts", 1, IntervalSchedule.HOURS, ), CeleryTaskConfiguration( "safe_relay_service.relay.tasks.check_create2_deployed_safes_task", "Check and deploy Create2 Safes", - 1, - IntervalSchedule.MINUTES, + 5, + IntervalSchedule.SECONDS, ), CeleryTaskConfiguration( "safe_relay_service.relay.tasks.find_erc_20_721_transfers_task", @@ -54,14 +54,14 @@ class Command(BaseCommand): CeleryTaskConfiguration( "safe_relay_service.relay.tasks.check_pending_transactions", "Check transactions not mined after a while", - 10, - IntervalSchedule.MINUTES, + 5, + IntervalSchedule.SECONDS, ), CeleryTaskConfiguration( "safe_relay_service.relay.tasks.check_and_update_pending_transactions", "Check and update transactions when mined", - 1, - IntervalSchedule.MINUTES, + 5, + IntervalSchedule.SECONDS, ), ] @@ -69,8 +69,8 @@ class Command(BaseCommand): CeleryTaskConfiguration( "safe_relay_service.relay.tasks.find_internal_txs_task", "Process Internal Txs for Safes", - 2, - IntervalSchedule.MINUTES, + 10, + IntervalSchedule.SECONDS, ), ] diff --git a/safe_relay_service/relay/serializers.py b/safe_relay_service/relay/serializers.py index fd144bd7..153ea6ae 100644 --- a/safe_relay_service/relay/serializers.py +++ b/safe_relay_service/relay/serializers.py @@ -54,7 +54,7 @@ class SafeCreation2Serializer( ThresholdValidatorSerializerMixin, serializers.Serializer ): salt_nonce = serializers.IntegerField( - min_value=0, max_value=2 ** 256 - 1 + min_value=0, max_value=2**256 - 1 ) # Uint256 owners = serializers.ListField(child=EthereumAddressField(), min_length=1) threshold = serializers.IntegerField(min_value=1) @@ -261,6 +261,7 @@ class SafeMultisigTxResponseSerializer(serializers.Serializer): nonce = serializers.IntegerField(min_value=0) safe_tx_hash = Sha3HashField() tx_hash = serializers.SerializerMethodField() + meta_tx_successful = serializers.BooleanField(required=False) transaction_hash = serializers.SerializerMethodField( method_name="get_tx_hash" ) # Retro compatibility diff --git a/safe_relay_service/relay/services/safe_creation_service.py b/safe_relay_service/relay/services/safe_creation_service.py index 065295fa..2b5a43e5 100644 --- a/safe_relay_service/relay/services/safe_creation_service.py +++ b/safe_relay_service/relay/services/safe_creation_service.py @@ -113,7 +113,7 @@ def __init__( self.proxy_factory = ProxyFactory(proxy_factory_address, self.ethereum_client) self.default_callback_handler = default_callback_handler self.funder_account = Account.from_key(safe_funder_private_key) - self.safe_fixed_creation_cost = safe_fixed_creation_cost + self.safe_fixed_creation_cost = 0 def _get_token_eth_value_or_raise(self, address: str) -> float: """ @@ -198,9 +198,9 @@ def create2_safe_tx( :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() + payment_token = NULL_ADDRESS + payment_token_eth_value = 0 + gas_price: int = 0 current_block_number = self.ethereum_client.current_block_number logger.debug("Building safe create2 tx with gas price %d", gas_price) safe_creation_tx = Safe.build_safe_create2_tx( @@ -212,7 +212,7 @@ def create2_safe_tx( threshold, gas_price, payment_token, - payment_receiver=self.funder_account.address, + payment_receiver=NULL_ADDRESS, fallback_handler=self.default_callback_handler, payment_token_eth_value=payment_token_eth_value, fixed_creation_cost=self.safe_fixed_creation_cost, @@ -249,10 +249,10 @@ def create2_safe_tx( if safe_creation_tx.payment_token == NULL_ADDRESS else safe_creation_tx.payment_token, payment=safe_creation_tx.payment, - payment_receiver=safe_creation_tx.payment_receiver, + payment_receiver=NULL_ADDRESS, setup_data=safe_creation_tx.safe_setup_data, gas_estimated=safe_creation_tx.gas, - gas_price_estimated=safe_creation_tx.gas_price, + gas_price_estimated=self._get_configured_gas_price(), ) def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: @@ -271,7 +271,7 @@ def deploy_create2_safe_tx(self, safe_address: str) -> SafeCreation2: ) return safe_creation2 - self._check_safe_balance(safe_creation2) + # self._check_safe_balance(safe_creation2) setup_data = HexBytes(safe_creation2.setup_data.tobytes()) proxy_factory = ProxyFactory(safe_creation2.proxy_factory, self.ethereum_client) @@ -326,7 +326,7 @@ def deploy_again_create2_safe_tx(self, safe_address: str) -> SafeCreation2: ethereum_tx: EthereumTx = EthereumTx.objects.get(tx_hash=safe_creation2.tx_hash) assert ethereum_tx, "Ethereum tx cannot be missing" - self._check_safe_balance(safe_creation2) + # self._check_safe_balance(safe_creation2) setup_data = HexBytes(safe_creation2.setup_data.tobytes()) proxy_factory = ProxyFactory(safe_creation2.proxy_factory, self.ethereum_client) @@ -364,9 +364,9 @@ def estimate_safe_creation2( :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() + payment_token = NULL_ADDRESS + payment_token_eth_value = 0 + gas_price = 0 fixed_creation_cost = self.safe_fixed_creation_cost return Safe.estimate_safe_creation_2( self.ethereum_client, @@ -375,7 +375,7 @@ def estimate_safe_creation2( number_owners, gas_price, payment_token, - payment_receiver=self.funder_account.address, + payment_receiver=NULL_ADDRESS, 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/services/transaction_service.py b/safe_relay_service/relay/services/transaction_service.py index f72a3416..d1325044 100644 --- a/safe_relay_service/relay/services/transaction_service.py +++ b/safe_relay_service/relay/services/transaction_service.py @@ -215,19 +215,19 @@ def _check_safe_gas_price( def _estimate_tx_gas_price( self, base_gas_price: int, gas_token: Optional[str] = None ) -> int: - if gas_token and gas_token != NULL_ADDRESS: - try: - gas_token_model = Token.objects.get(address=gas_token, gas=True) - estimated_gas_price = gas_token_model.calculate_gas_price( - base_gas_price - ) - except Token.DoesNotExist: - raise InvalidGasToken("Gas token %s not found" % gas_token) - else: - estimated_gas_price = base_gas_price + # if gas_token and gas_token != NULL_ADDRESS: + # try: + # gas_token_model = Token.objects.get(address=gas_token, gas=True) + # estimated_gas_price = gas_token_model.calculate_gas_price( + # base_gas_price + # ) + # except Token.DoesNotExist: + # raise InvalidGasToken("Gas token %s not found" % gas_token) + # else: + # estimated_gas_price = base_gas_price # FIXME Remove 2 / 3, workaround to prevent frontrunning - return int(estimated_gas_price * 2 / 3) + return 0 def _get_configured_gas_price(self) -> int: """ @@ -298,7 +298,7 @@ def estimate_tx( gas_price, gas_token or NULL_ADDRESS, last_used_nonce, - self.tx_sender_account.address, + NULL_ADDRESS, ) def estimate_tx_for_all_tokens( @@ -458,15 +458,15 @@ def _send_multisig_tx( safe = Safe(safe_address, self.ethereum_client) data = data or b"" - gas_token = gas_token or NULL_ADDRESS - refund_receiver = refund_receiver or NULL_ADDRESS + gas_token = NULL_ADDRESS + refund_receiver = NULL_ADDRESS to = to or NULL_ADDRESS # Make sure refund receiver is set to 0x0 so that the contract refunds the gas costs to tx.origin if not self._check_refund_receiver(refund_receiver): raise InvalidRefundReceiver(refund_receiver) - self._check_safe_gas_price(gas_token, gas_price) + # self._check_safe_gas_price(gas_token, gas_price) # Make sure proxy contract is ours if not self.proxy_factory.check_proxy_code(safe_address): @@ -478,29 +478,29 @@ def _send_multisig_tx( raise InvalidMasterCopyAddress(safe_master_copy_address) # Check enough funds to pay for the gas - if not safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, gas_token): - raise NotEnoughFundsForMultisigTx + # if not safe.check_funds_for_tx_gas(safe_tx_gas, base_gas, gas_price, gas_token): + # raise NotEnoughFundsForMultisigTx threshold = safe.retrieve_threshold() number_signatures = len(signatures) // 65 # One signature = 65 bytes if number_signatures < threshold: raise SignaturesNotFound("Need at least %d signatures" % threshold) - safe_tx_gas_estimation = safe.estimate_tx_gas(to, value, data, operation) - 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 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, - ) - ) + # safe_tx_gas_estimation = safe.estimate_tx_gas(to, value, data, operation) + # 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 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 tx_gas_price = self._get_configured_gas_price() diff --git a/safe_relay_service/relay/tasks.py b/safe_relay_service/relay/tasks.py index 2361caca..880c1a55 100644 --- a/safe_relay_service/relay/tasks.py +++ b/safe_relay_service/relay/tasks.py @@ -388,7 +388,7 @@ def check_create2_deployed_safes_task() -> None: timeout=LOCK_TIMEOUT, ): ethereum_client = EthereumClientProvider() - confirmations = 6 + confirmations = 2 current_block_number = ethereum_client.current_block_number for safe_creation2 in SafeCreation2.objects.pending_to_check(): safe_address = safe_creation2.safe_id diff --git a/safe_relay_service/relay/tests/test_resend_txs.py b/safe_relay_service/relay/tests/test_resend_txs.py index 0bc16a4f..9b904e3b 100644 --- a/safe_relay_service/relay/tests/test_resend_txs.py +++ b/safe_relay_service/relay/tests/test_resend_txs.py @@ -1,127 +1,118 @@ -from datetime import timedelta - from django.core.management import call_command from django.test import TestCase -from django.utils import timezone - -from eth_account import Account -from hexbytes import HexBytes - -from gnosis.eth.constants import NULL_ADDRESS -from gnosis.safe import Safe from ..management.commands import resend_txs -from ..models import SafeMultisigTx from .relay_test_case import RelayTestCaseMixin class TestResendTxsCommand(RelayTestCaseMixin, TestCase): def test_resend_txs(self): + # TODO Refactor resend txs for EIP-1559 # Nothing happens call_command(resend_txs.Command()) - w3 = self.w3 - # The balance we will send to the safe - safe_balance = w3.toWei(0.02, "ether") - - # Create Safe - accounts = [self.create_account(), self.create_account()] - - # Signatures must be sorted! - accounts.sort(key=lambda account: account.address.lower()) - - safe = self.deploy_test_safe( - owners=[x.address for x in accounts], - threshold=len(accounts), - initial_funding_wei=safe_balance, - ) - my_safe_address = safe.address - - to = Account().create().address - value = safe_balance // 4 - data = HexBytes("") - operation = 0 - safe_tx_gas = 100000 - data_gas = 300000 - gas_price = self.transaction_service._get_minimum_gas_price() - gas_token = NULL_ADDRESS - refund_receiver = NULL_ADDRESS - safe = Safe(my_safe_address, self.ethereum_client) - nonce = safe.retrieve_nonce() - safe_multisig_tx_hash = safe.build_multisig_tx( - to, - value, - data, - operation, - safe_tx_gas, - data_gas, - gas_price, - gas_token, - refund_receiver, - safe_nonce=nonce, - ).safe_tx_hash - - signatures = [account.signHash(safe_multisig_tx_hash) for account in accounts] - sender = self.transaction_service.tx_sender_account.address - - # Ganache snapshot - snapshot_id = w3.testing.snapshot() - safe_multisig_tx = self.transaction_service.create_multisig_tx( - my_safe_address, - to, - value, - data, - operation, - safe_tx_gas, - data_gas, - gas_price, - gas_token, - refund_receiver, - nonce, - signatures, - ) - - tx_receipt = w3.eth.wait_for_transaction_receipt( - safe_multisig_tx.ethereum_tx.tx_hash - ) - self.assertTrue(tx_receipt["status"]) - self.assertEqual(w3.toChecksumAddress(tx_receipt["from"]), sender) - self.assertEqual(w3.toChecksumAddress(tx_receipt["to"]), my_safe_address) - self.assertEqual(w3.eth.get_balance(to), value) - - w3.testing.revert(snapshot_id) # Revert to snapshot in ganache - snapshot_id = w3.testing.snapshot() - self.assertEqual(w3.eth.get_balance(to), 0) - - old_multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() - old_multisig_tx.created = timezone.now() - timedelta(days=1) - old_multisig_tx.save() - new_gas_price = old_multisig_tx.ethereum_tx.gas_price + 1 # Gas price increased - - call_command(resend_txs.Command(), gas_price=new_gas_price) - multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() - self.assertNotEqual(multisig_tx.ethereum_tx_id, old_multisig_tx.ethereum_tx_id) - self.assertEqual(multisig_tx.ethereum_tx.gas_price, new_gas_price) - self.assertEqual(w3.eth.get_balance(to), value) # Tx is executed again - self.assertEqual( - multisig_tx.get_safe_tx(self.ethereum_client).__dict__, - old_multisig_tx.get_safe_tx(self.ethereum_client).__dict__, - ) - - w3.testing.revert(snapshot_id) # Revert to snapshot in ganache - self.assertEqual(w3.eth.get_balance(to), 0) - - old_multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() - old_multisig_tx.created = timezone.now() - timedelta(days=1) - old_multisig_tx.save() - new_gas_price = old_multisig_tx.ethereum_tx.gas_price # Gas price is the same - - call_command(resend_txs.Command(), gas_price=new_gas_price) - multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() - self.assertEqual(multisig_tx.ethereum_tx_id, old_multisig_tx.ethereum_tx_id) - self.assertEqual(multisig_tx.ethereum_tx.gas_price, new_gas_price) - self.assertEqual(w3.eth.get_balance(to), value) # Tx is executed again - self.assertEqual( - multisig_tx.get_safe_tx(self.ethereum_client).__dict__, - old_multisig_tx.get_safe_tx(self.ethereum_client).__dict__, - ) + # w3 = self.w3 + # # The balance we will send to the safe + # safe_balance = w3.toWei(0.02, "ether") + # + # # Create Safe + # accounts = [self.create_account(), self.create_account()] + # + # # Signatures must be sorted! + # accounts.sort(key=lambda account: account.address.lower()) + # + # safe = self.deploy_test_safe( + # owners=[x.address for x in accounts], + # threshold=len(accounts), + # initial_funding_wei=safe_balance, + # ) + # my_safe_address = safe.address + # + # to = Account().create().address + # value = safe_balance // 4 + # data = HexBytes("") + # operation = 0 + # safe_tx_gas = 100000 + # data_gas = 300000 + # gas_price = self.transaction_service._get_minimum_gas_price() + # gas_token = NULL_ADDRESS + # refund_receiver = NULL_ADDRESS + # safe = Safe(my_safe_address, self.ethereum_client) + # nonce = safe.retrieve_nonce() + # safe_multisig_tx_hash = safe.build_multisig_tx( + # to, + # value, + # data, + # operation, + # safe_tx_gas, + # data_gas, + # gas_price, + # gas_token, + # refund_receiver, + # safe_nonce=nonce, + # ).safe_tx_hash + # + # signatures = [account.signHash(safe_multisig_tx_hash) for account in accounts] + # sender = self.transaction_service.tx_sender_account.address + # + # # Ganache snapshot + # snapshot_id = w3.testing.snapshot() + # safe_multisig_tx = self.transaction_service.create_multisig_tx( + # my_safe_address, + # to, + # value, + # data, + # operation, + # safe_tx_gas, + # data_gas, + # gas_price, + # gas_token, + # refund_receiver, + # nonce, + # signatures, + # ) + # + # tx_receipt = w3.eth.wait_for_transaction_receipt( + # safe_multisig_tx.ethereum_tx.tx_hash + # ) + # self.assertTrue(tx_receipt["status"]) + # self.assertEqual(w3.toChecksumAddress(tx_receipt["from"]), sender) + # self.assertEqual(w3.toChecksumAddress(tx_receipt["to"]), my_safe_address) + # self.assertEqual(w3.eth.get_balance(to), value) + # + # w3.testing.revert(snapshot_id) # Revert to snapshot in ganache + # snapshot_id = w3.testing.snapshot() + # self.assertEqual(w3.eth.get_balance(to), 0) + # + # old_multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() + # old_multisig_tx.created = timezone.now() - timedelta(days=1) + # old_multisig_tx.save() + # new_gas_price = old_multisig_tx.ethereum_tx.gas_price + 1 # Gas price increased + # + # call_command(resend_txs.Command(), gas_price=new_gas_price) + # multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() + # self.assertNotEqual(multisig_tx.ethereum_tx_id, old_multisig_tx.ethereum_tx_id) + # self.assertEqual(multisig_tx.ethereum_tx.gas_price, new_gas_price) + # self.assertEqual(w3.eth.get_balance(to), value) # Tx is executed again + # self.assertEqual( + # multisig_tx.get_safe_tx(self.ethereum_client).__dict__, + # old_multisig_tx.get_safe_tx(self.ethereum_client).__dict__, + # ) + # + # w3.testing.revert(snapshot_id) # Revert to snapshot in ganache + # self.assertEqual(w3.eth.get_balance(to), 0) + # + # old_multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() + # old_multisig_tx.created = timezone.now() - timedelta(days=1) + # old_multisig_tx.save() + # new_gas_price = old_multisig_tx.ethereum_tx.gas_price # Gas price is the same + # + # call_command(resend_txs.Command(), gas_price=new_gas_price) + # multisig_tx: SafeMultisigTx = SafeMultisigTx.objects.all().first() + # self.assertEqual(multisig_tx.ethereum_tx_id, old_multisig_tx.ethereum_tx_id) + # self.assertEqual(multisig_tx.ethereum_tx.gas_price, new_gas_price) + # self.assertEqual(w3.eth.get_balance(to), value) # Tx is executed again + # self.assertEqual( + # multisig_tx.get_safe_tx(self.ethereum_client).__dict__, + # old_multisig_tx.get_safe_tx(self.ethereum_client).__dict__, + # ) 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 b82b62d5..b9337d65 100644 --- a/safe_relay_service/relay/tests/test_safe_creation_service.py +++ b/safe_relay_service/relay/tests/test_safe_creation_service.py @@ -46,7 +46,7 @@ def test_deploy_create2_safe_tx(self): self.assertIsNone(safe_creation_2.tx_hash) self.assertEqual( safe_creation_2.payment_receiver, - self.safe_creation_service.funder_account.address, + NULL_ADDRESS ) with self.assertRaisesMessage( NotEnoughFundingForCreation, str(safe_creation_2.payment) diff --git a/safe_relay_service/relay/tests/test_transaction_service.py b/safe_relay_service/relay/tests/test_transaction_service.py index 4613f45f..1ff49cdd 100644 --- a/safe_relay_service/relay/tests/test_transaction_service.py +++ b/safe_relay_service/relay/tests/test_transaction_service.py @@ -47,7 +47,7 @@ def test_create_multisig_tx(self): safe = self.deploy_test_safe(owners=owners, threshold=threshold) my_safe_address = safe.address - my_safe_contract = safe.get_contract() + my_safe_contract = safe.contract SafeContractFactory(address=my_safe_address) to = funder @@ -309,9 +309,10 @@ def test_create_multisig_tx(self): self.assertTrue(tx_receipt["status"]) self.assertEqual(w3.toChecksumAddress(tx_receipt["from"]), sender) self.assertEqual(w3.toChecksumAddress(tx_receipt["to"]), my_safe_address) - self.assertGreater( - safe_multisig_tx.ethereum_tx.gas_price, gas_price - ) # We used minimum gas price + # Changed with EIP1559 + # self.assertGreater( + # safe_multisig_tx.ethereum_tx.gas_price, gas_price + # ) # We used minimum gas price sender_new_balance = w3.eth.get_balance(sender) gas_used = tx_receipt["gasUsed"] diff --git a/safe_relay_service/relay/views.py b/safe_relay_service/relay/views.py index 34277f87..8f4cfae4 100644 --- a/safe_relay_service/relay/views.py +++ b/safe_relay_service/relay/views.py @@ -7,6 +7,9 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from eth_account.account import Account +from gnosis.eth import EthereumClientProvider +from gnosis.eth.contracts import get_safe_V1_3_0_contract, get_safe_contract +from hexbytes import HexBytes from rest_framework import filters, status from rest_framework.authentication import TokenAuthentication from rest_framework.generics import CreateAPIView, ListAPIView @@ -53,6 +56,27 @@ logger = logging.getLogger(__name__) +def is_meta_tx_successful( + identity_address, meta_tx_hash, *, from_block=0, to_block="latest" +): + ethereum_client = EthereumClientProvider() + identity_contract = get_safe_contract(ethereum_client.w3, identity_address) + + success_filter = identity_contract.events.ExecutionSuccess.createFilter(fromBlock=from_block, + address=identity_address, + toBlock=to_block, + argument_filters={ + "txHash": HexBytes(meta_tx_hash)}) + meta_tx_execution_logs = success_filter.get_all_entries() + + assert len(meta_tx_execution_logs) <= 1 + + if len(meta_tx_execution_logs) == 1: + return True + + return False + + def custom_exception_handler(exc, context): # Call REST framework's default exception handler first, # to get the standard error response. @@ -432,6 +456,25 @@ def get_serializer_class(self): def get_queryset(self): return SafeMultisigTx.objects.filter(safe=self.kwargs["address"]) + @swagger_auto_schema( + responses={ + 201: SafeMultisigTxResponseSerializer(), + 400: "Data not valid", + 404: "Safe not found", + 422: "Safe address checksum not valid/Tx not valid", + } + ) + def get(self, request, address): + response = super().get(request, address) + + for x in range(len(response.data["results"])): + tx_hash = response.data["results"][x]["safe_tx_hash"] + response.data["results"][x]["meta_tx_successful"] = is_meta_tx_successful( + address, tx_hash + ) + + return response + @swagger_auto_schema( responses={ 201: SafeMultisigTxResponseSerializer(), diff --git a/safe_relay_service/tokens/models.py b/safe_relay_service/tokens/models.py index 9439867b..9a527bbd 100644 --- a/safe_relay_service/tokens/models.py +++ b/safe_relay_service/tokens/models.py @@ -92,7 +92,7 @@ def __str__(self): return "%s - %s" % (self.name, self.address) def get_eth_value(self) -> float: - multiplier = 1e18 / 10 ** self.decimals + multiplier = 1e18 / 10**self.decimals if self.fixed_eth_conversion: # `None` or `0` are ignored # Ether has 18 decimals, but maybe the token has a different number return round(multiplier * float(self.fixed_eth_conversion), 10) diff --git a/safe_relay_service/version.py b/safe_relay_service/version.py index f3d895ba..8a1e6a57 100644 --- a/safe_relay_service/version.py +++ b/safe_relay_service/version.py @@ -1,4 +1,4 @@ -__version__ = "4.0.2" +__version__ = "4.1.0" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/scripts/deploy_docker.sh b/scripts/deploy_docker.sh index cc15ec51..69f822ea 100644 --- a/scripts/deploy_docker.sh +++ b/scripts/deploy_docker.sh @@ -4,13 +4,13 @@ set -euo pipefail if [ "$1" = "develop" -o "$1" = "master" ]; then # If image does not exist, don't use cache - docker pull gnosispm/$DOCKERHUB_PROJECT:$1 && \ - docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . --cache-from gnosispm/$DOCKERHUB_PROJECT:$1 || \ + docker pull trustlines/$DOCKERHUB_PROJECT:$1 && \ + docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . --cache-from trustlines/$DOCKERHUB_PROJECT:$1 || \ docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . else - docker pull gnosispm/$DOCKERHUB_PROJECT:staging && \ - docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . --cache-from gnosispm/$DOCKERHUB_PROJECT:staging || \ + docker pull trustlines/$DOCKERHUB_PROJECT:staging && \ + docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . --cache-from trustlines/$DOCKERHUB_PROJECT:staging || \ docker build -t $DOCKERHUB_PROJECT -f docker/web/Dockerfile . fi -docker tag $DOCKERHUB_PROJECT gnosispm/$DOCKERHUB_PROJECT:$1 -docker push gnosispm/$DOCKERHUB_PROJECT:$1 \ No newline at end of file +docker tag $DOCKERHUB_PROJECT trustlines/$DOCKERHUB_PROJECT:$1 +docker push trustlines/$DOCKERHUB_PROJECT:$1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index e01dd6cb..b6ae2444 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [flake8] max-line-length = 88 select = C,E,F,W,B,B950 -extend-ignore = E203, E501, F841 +extend-ignore = E203,E501,F841,W503 exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv [pycodestyle] @@ -22,7 +22,7 @@ env = DJANGO_DOT_ENV_FILE=.env.test [mypy] -python_version = 3.9 +python_version = 3.10 check_untyped_defs = True ignore_missing_imports = True warn_unused_ignores = True