From 0ace271b88dba449d9039fa4ef6581a9cf2a6cc3 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:36:30 +0000 Subject: [PATCH 1/9] feat(ci): adding ci jobs with simple test suite --- .github/workflows/docker-publish.yaml | 72 ++++++++++++++ .github/workflows/python-app.yaml | 32 ++++++ Dockerfile | 2 +- src/main.py | 135 +++++++++++++++++++------- tests/__init__.py | 0 tests/test_main.py | 124 +++++++++++++++++++++++ 6 files changed, 328 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/docker-publish.yaml create mode 100644 .github/workflows/python-app.yaml create mode 100644 tests/__init__.py create mode 100644 tests/test_main.py diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml new file mode 100644 index 0000000..c82591c --- /dev/null +++ b/.github/workflows/docker-publish.yaml @@ -0,0 +1,72 @@ +name: Docker + +on: + push: + branches: [ main, github-actions ] + tags: [ 'v*.*.*' ] + pull_request: + branches: [ main ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@faadad0 #v4.0.0 + with: + cosign-release: 'v4.0.0' + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171 # v3.11.1 + + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@5e57cd1 # v3.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@c299e40 # v5.10.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@2634353 # v6.18.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml new file mode 100644 index 0000000..88c3228 --- /dev/null +++ b/.github/workflows/python-app.yaml @@ -0,0 +1,32 @@ +name: Python application + +on: + push: + branches: [ main, github-actions ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12.12 + uses: actions/setup-python@v3 + with: + python-version: "3.12.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with pylint + run: | + pylint src/ tests/ --fail-under=8.0 + - name: Test with pytest + run: | + pytest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 87a0c88..a2ae11e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.25-alpine3.22 +FROM python:3.12.12-alpine3.23 ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 diff --git a/src/main.py b/src/main.py index cf71199..e18620f 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,18 @@ -import argparse, os, asyncio, signal, json, logging, threading +"""Token Transaction Service - Sends EOA transactions at regular intervals.""" +import argparse +import asyncio +import json +import logging +import os +import signal +import threading from datetime import datetime, timezone -from web3.providers.rpc.async_rpc import AsyncHTTPProvider -from web3 import AsyncWeb3, Web3, AsyncHTTPProvider -from web3.middleware import SignAndSendRawMiddlewareBuilder, ExtraDataToPOAMiddleware from typing import Optional + from aiohttp import web +from web3 import AsyncWeb3, Web3 +from web3.middleware import ExtraDataToPOAMiddleware, SignAndSendRawMiddlewareBuilder +from web3.providers.rpc.async_rpc import AsyncHTTPProvider # Shutdown flag for graceful termination (thread-safe for signal handlers) shutdown_event = threading.Event() @@ -45,19 +53,26 @@ def format(self, record): def init_logger(): """Initialize logger with JSON formatter""" - logger = logging.getLogger(SERVICE_NAME) - logger.setLevel(logging.INFO) + app_logger = logging.getLogger(SERVICE_NAME) + app_logger.setLevel(logging.INFO) # Create console handler with JSON formatter handler = logging.StreamHandler() handler.setFormatter(JSONFormatter()) - logger.addHandler(handler) + app_logger.addHandler(handler) - return logger + return app_logger logger = init_logger() -async def send_funds(w3: AsyncWeb3, amount: int, pk: str, to_address: str, interval: int, use_legacy_gas: bool = False): +async def send_funds( # pylint: disable=too-many-arguments,too-many-positional-arguments + w3: AsyncWeb3, + amount: int, + pk: str, + to_address: str, + interval: int, + use_legacy_gas: bool = False +): """ Send funds to an account on a regular interval. @@ -74,7 +89,8 @@ async def send_funds(w3: AsyncWeb3, amount: int, pk: str, to_address: str, inter w3.eth.default_account = account.address # SignAndSendRawMiddlewareBuilder handles signing automatically # https://web3py.readthedocs.io/en/stable/middleware.html#web3.middleware.SignAndSendRawMiddlewareBuilder - w3.middleware_onion.inject(SignAndSendRawMiddlewareBuilder.build(account), layer=0) + middleware = SignAndSendRawMiddlewareBuilder.build(account) # pylint: disable=no-value-for-parameter + w3.middleware_onion.inject(middleware, layer=0) # Use legacy gas if flag is set if use_legacy_gas: @@ -83,7 +99,13 @@ async def send_funds(w3: AsyncWeb3, amount: int, pk: str, to_address: str, inter logger.info("Using EIP-1559 dynamic fee transactions") logger.info("Starting transaction service") - logger.info(f"from: {account.address}, to: {to_address}, amount: {amount} wei, interval: {interval} seconds") + logger.info( + "from: %s, to: %s, amount: %s wei, interval: %s seconds", + account.address, + to_address, + amount, + interval + ) # Considered in ready state service_state['ready'] = True @@ -112,7 +134,12 @@ async def send_funds(w3: AsyncWeb3, amount: int, pk: str, to_address: str, inter tx_hash = await w3.eth.send_transaction(tx_dict) - logger.info(f"Sent {amount} wei to {to_address}, TX: {tx_hash.hex()}") + logger.info( + "Sent %s wei to %s, TX: %s", + amount, + to_address, + tx_hash.hex() + ) # Update state service_state['last_tx_time'] = datetime.now().isoformat() @@ -124,20 +151,23 @@ async def send_funds(w3: AsyncWeb3, amount: int, pk: str, to_address: str, inter except asyncio.CancelledError: logger.info("Transaction service cancelled, shutting down") break - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught service_state['error_count'] += 1 - logger.error(f"Error sending transaction: {e}") + logger.error("Error sending transaction: %s", e) # Considered not ready if too many consecutive errors (threshold: 5) if service_state['error_count'] >= 5: service_state['ready'] = False - logger.warning(f"Service marked as not ready due to {service_state['error_count']} consecutive errors") + logger.warning( + "Service marked as not ready due to %s consecutive errors", + service_state['error_count'] + ) if shutdown_event.is_set(): logger.info("Shutdown signal received during error handling, stopping") break - logger.info(f"Retrying in {interval} seconds") + logger.info("Retrying in %s seconds", interval) await asyncio.sleep(interval) logger.info("Transaction service stopped") @@ -164,12 +194,23 @@ async def connect_with_retry(rpc_url: str, max_attempts: int = 3) -> Optional[As return w3 attempt += 1 wait_time = 2 ** attempt - logger.warning(f"Failed to connect to RPC node (attempt {attempt}/{max_attempts}), retrying in {wait_time} seconds") + logger.warning( + "Failed to connect to RPC node (attempt %s/%s), retrying in %s seconds", + attempt, + max_attempts, + wait_time + ) await asyncio.sleep(wait_time) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught attempt += 1 wait_time = 2 ** attempt - logger.error(f"Error connecting to RPC node: {e} (attempt {attempt}/{max_attempts}), retrying in {wait_time} seconds") + logger.error( + "Error connecting to RPC node: %s (attempt %s/%s), retrying in %s seconds", + e, + attempt, + max_attempts, + wait_time + ) await asyncio.sleep(wait_time) logger.error("Unable to connect to RPC node after maximum retry attempts") @@ -196,14 +237,13 @@ async def readiness_handler(_request): 'last_tx_hash': service_state['last_tx_hash'], 'error_count': service_state['error_count'] }, status=200) - else: - return web.json_response({ - 'status': 'not_ready', - 'ready': service_state['ready'], - 'connected': service_state['connected'], - 'error_count': service_state['error_count'], - 'reason': 'too_many_errors' if service_state['error_count'] >= 5 else 'not_initialized' - }, status=503) + return web.json_response({ + 'status': 'not_ready', + 'ready': service_state['ready'], + 'connected': service_state['connected'], + 'error_count': service_state['error_count'], + 'reason': 'too_many_errors' if service_state['error_count'] >= 5 else 'not_initialized' + }, status=503) async def health_handler(_request): """Combined health check endpoint.""" @@ -223,7 +263,7 @@ def signal_handler(signum, _frame): signum: Signal number _frame: Current stack frame """ - logger.info(f"Received signal {signum}, initiating graceful shutdown") + logger.info("Received signal %s, initiating graceful shutdown", signum) shutdown_event.set() # https://docs.python.org/3/library/signal.html#signal.signal @@ -245,14 +285,24 @@ async def start_health_server(port: int = 8080): await runner.setup() site = web.TCPSite(runner, '0.0.0.0', port) await site.start() - logger.info(f"Health check server started on http://0.0.0.0:{port}") - logger.info(f"Health endpoints - Liveness: http://localhost:{port}/health/live, Readiness: http://localhost:{port}/health/ready, Health: http://localhost:{port}/health") + logger.info("Health check server started on http://0.0.0.0:%s", port) + logger.info( + "Health endpoints - Liveness: http://localhost:%s/health/live, " + "Readiness: http://localhost:%s/health/ready, " + "Health: http://localhost:%s/health", + port, + port, + port + ) return runner async def main(): """Parses arguments and runs the tx service.""" parser = argparse.ArgumentParser( - description="Token Transaction Service - Sends ETH transactions to a specified address at regular intervals.", + description=( + "Token Transaction Service - Sends ETH transactions to a " + "specified address at regular intervals." + ), epilog=""" Examples: # Basic usage with private key (EIP-1559 dynamic fees) @@ -288,7 +338,10 @@ async def main(): type=str, required=True, metavar='URL', - help="RPC URL for the blockchain node (e.g., https://linea-mainnet.infura.io/v3/YOUR_KEY)" + help=( + "RPC URL for the blockchain node " + "(e.g., https://linea-mainnet.infura.io/v3/YOUR_KEY)" + ) ) parser.add_argument( '--amount', @@ -347,7 +400,10 @@ async def main(): pk = args.pk or os.environ.get('SENDER_PK') if not pk: - parser.error("--pk must be provided or SENDER_PK must be set in your environment variables.") + parser.error( + "--pk must be provided or SENDER_PK must be set in your " + "environment variables." + ) if not args.to_address: parser.error("--to-address must be provided.") @@ -376,11 +432,18 @@ async def main(): return # Run transaction service (this will run until shutdown_event is set) - await send_funds(w3, args.amount, pk, to_address, args.interval, use_legacy_gas=args.legacy_gas) + await send_funds( + w3, + args.amount, + pk, + to_address, + args.interval, + use_legacy_gas=args.legacy_gas + ) except asyncio.CancelledError: logger.info("Service cancelled") - except Exception as e: - logger.error(f"Unexpected error: {e}") + except Exception as e: # pylint: disable=broad-exception-caught + logger.error("Unexpected error: %s", e) finally: # Cleanup logger.info("Cleaning up") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..bc91efd --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,124 @@ +"""Tests for the token transaction service.""" +import json +import logging +import re +import sys + +import pytest +from aiohttp.test_utils import make_mocked_request + +sys.path.insert(0, 'src') +from main import ( # pylint: disable=wrong-import-position + JSONFormatter, + health_handler, + init_logger, + liveness_handler, + readiness_handler, + service_state +) + + +def test_json_formatter(): + """Test that JSONFormatter creates valid JSON log entries""" + formatter = JSONFormatter() + record = logging.LogRecord( + name='test', + level=logging.INFO, + pathname='test.py', + lineno=1, + msg='Test message', + args=(), + exc_info=None + ) + + result = formatter.format(record) + log_data = json.loads(result) + + assert 'timestamp' in log_data + # Verify timestamp format: YYYY-MM-DDTHH:MM:SS.sssZ + timestamp_pattern = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$' + timestamp = log_data['timestamp'] + assert re.match(timestamp_pattern, timestamp), ( + f"Timestamp {timestamp} does not match expected format " + "YYYY-MM-DDTHH:MM:SS.sssZ" + ) + assert 'level' in log_data + assert 'service' in log_data + assert 'message' in log_data + assert log_data['level'] == 'info' + assert log_data['message'] == 'Test message' + assert log_data['service'] == 'token-tx' + + +def test_init_logger(): + """Test that logger is initialized correctly""" + # Clear existing handlers to test fresh initialization + logger = logging.getLogger('token-tx') + logger.handlers.clear() + + logger = init_logger() + + assert logger is not None + assert logger.name == 'token-tx' + assert logger.level == logging.INFO + assert len(logger.handlers) >= 1 + + +@pytest.mark.asyncio +async def test_liveness_handler(): + """Test that liveness handler returns correct response""" + request = make_mocked_request('GET', '/health/live') + response = await liveness_handler(request) + + assert response.status == 200 + data = json.loads(response.text) + assert data['status'] == 'alive' + + +@pytest.mark.asyncio +async def test_readiness_handler_ready(): + """Test readiness handler when service is ready""" + # Set service state to ready + service_state['ready'] = True + service_state['connected'] = True + service_state['error_count'] = 0 + + request = make_mocked_request('GET', '/health/ready') + response = await readiness_handler(request) + + assert response.status == 200 + data = json.loads(response.text) + assert data['status'] == 'ready' + assert data['connected'] is True + + +@pytest.mark.asyncio +async def test_readiness_handler_not_ready(): + """Test readiness handler when service is not ready""" + # Set service state to not ready + service_state['ready'] = False + service_state['connected'] = False + service_state['error_count'] = 0 + + request = make_mocked_request('GET', '/health/ready') + response = await readiness_handler(request) + + assert response.status == 503 + data = json.loads(response.text) + assert data['status'] == 'not_ready' + + +@pytest.mark.asyncio +async def test_health_handler(): + """Test health handler returns service state""" + service_state['ready'] = True + service_state['connected'] = True + + request = make_mocked_request('GET', '/health') + response = await health_handler(request) + + assert response.status == 200 + data = json.loads(response.text) + assert 'status' in data + assert 'ready' in data + assert 'connected' in data From 5cd76598287d4e3f63b208505ef6e0a3ce38eb68 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:46:47 +0000 Subject: [PATCH 2/9] fix(ci): resolve correct job hashes --- .github/workflows/docker-publish.yaml | 10 +++++----- .github/workflows/python-app.yaml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index c82591c..109f723 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -28,18 +28,18 @@ jobs: # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@faadad0 #v4.0.0 + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad #v4.0.0 with: cosign-release: 'v4.0.0' # https://github.com/docker/setup-buildx-action - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171 # v3.11.1 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@5e57cd1 # v3.6.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -48,14 +48,14 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@c299e40 # v5.10.0 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@2634353 # v6.18.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 88c3228..ca7e71e 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -14,9 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python 3.12.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.12.12" - name: Install dependencies From 7619a76b84685d73ca14eaa7d687a60ea080948c Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 12:53:42 +0000 Subject: [PATCH 3/9] fix(tests): resolve pylint warning and ci job --- .github/workflows/docker-publish.yaml | 2 -- .github/workflows/python-app.yaml | 3 +-- src/main.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index 109f723..e4037ab 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -29,8 +29,6 @@ jobs: - name: Install cosign if: github.event_name != 'pull_request' uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad #v4.0.0 - with: - cosign-release: 'v4.0.0' # https://github.com/docker/setup-buildx-action - name: Set up Docker Buildx diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index ca7e71e..3c5ebdb 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -22,8 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - name: Lint with pylint run: | pylint src/ tests/ --fail-under=8.0 diff --git a/src/main.py b/src/main.py index e18620f..a9438d7 100644 --- a/src/main.py +++ b/src/main.py @@ -65,7 +65,7 @@ def init_logger(): logger = init_logger() -async def send_funds( # pylint: disable=too-many-arguments,too-many-positional-arguments +async def send_funds( # pylint: disable=too-many-arguments,too-many-positional w3: AsyncWeb3, amount: int, pk: str, From 92a87b3949be8516909cbb080b2d7d952d974a6c Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:00:44 +0000 Subject: [PATCH 4/9] fix(tests): resolve pylint issues --- .github/workflows/python-app.yaml | 2 +- requirements-dev.txt | 4 ++++ src/main.py | 35 +++++++++++++++---------------- 3 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 requirements-dev.txt diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 3c5ebdb..1b4ef70 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -25,7 +25,7 @@ jobs: if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - name: Lint with pylint run: | - pylint src/ tests/ --fail-under=8.0 + pylint src/ tests/ --fail-under=9.0 - name: Test with pytest run: | pytest \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5457703 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +pytest==9.0.1 +pytest-asyncio==1.3.0 +pylint==4.0.4 diff --git a/src/main.py b/src/main.py index a9438d7..c7887b7 100644 --- a/src/main.py +++ b/src/main.py @@ -65,25 +65,21 @@ def init_logger(): logger = init_logger() -async def send_funds( # pylint: disable=too-many-arguments,too-many-positional - w3: AsyncWeb3, - amount: int, - pk: str, - to_address: str, - interval: int, - use_legacy_gas: bool = False -): +async def send_funds(w3: AsyncWeb3, tx_data: dict): """ Send funds to an account on a regular interval. Args: w3: AsyncWeb3 object - amount: Amount in wei to send to the account - pk: Private key of the account that will send the funds - to_address: Address of the account that will receive the funds - interval: Interval in seconds to send the funds - use_legacy_gas: If True, use legacy gas pricing - useful for local networks (fixed) + tx_data: Dictionary containing the transaction data """ + + amount = tx_data['value'] + pk = tx_data['pk'] + to_address = tx_data['to'] + interval = tx_data['interval'] + use_legacy_gas = tx_data['use_legacy_gas'] + # Derive account address from private key account = w3.eth.account.from_key(pk) w3.eth.default_account = account.address @@ -92,6 +88,7 @@ async def send_funds( # pylint: disable=too-many-arguments,too-many-positional middleware = SignAndSendRawMiddlewareBuilder.build(account) # pylint: disable=no-value-for-parameter w3.middleware_onion.inject(middleware, layer=0) + # Use legacy gas if flag is set if use_legacy_gas: logger.info("Using legacy gas pricing (--legacy-gas flag set)") @@ -434,11 +431,13 @@ async def main(): # Run transaction service (this will run until shutdown_event is set) await send_funds( w3, - args.amount, - pk, - to_address, - args.interval, - use_legacy_gas=args.legacy_gas + { + 'value': args.amount, + 'pk': pk, + 'to': to_address, + 'interval': args.interval, + 'use_legacy_gas': args.legacy_gas + } ) except asyncio.CancelledError: logger.info("Service cancelled") From feca4aecd4e76942fe89b12d62ffe54d62d75919 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:07:37 +0000 Subject: [PATCH 5/9] feat(ci): add checkov job for dockerfile and resolve issues --- .github/workflows/checkov.yaml | 38 +++++++++++++++++++++++++++ .github/workflows/docker-publish.yaml | 2 +- Dockerfile | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/checkov.yaml diff --git a/.github/workflows/checkov.yaml b/.github/workflows/checkov.yaml new file mode 100644 index 0000000..a09f71a --- /dev/null +++ b/.github/workflows/checkov.yaml @@ -0,0 +1,38 @@ +name: 'Checkov' + +on: + push: + pull_request: + + workflow_dispatch: + +permissions: + contents: read + +jobs: + scan: + name: 'Checkov' + environment: production + + permissions: + contents: read + security-events: write + actions: read + + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Checkov Dockerfile + uses: bridgecrewio/checkov-action@99bb2caf247dfd9f03cf984373bc6043d4e32ebf # v12.1347.0 + with: + file: Dockerfile + quiet: true + output_format: cli,sarif + output_file_path: console,results.sarif \ No newline at end of file diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index e4037ab..def4a22 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # https://github.com/sigstore/cosign-installer - name: Install cosign diff --git a/Dockerfile b/Dockerfile index a2ae11e..b1411c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 WORKDIR /app -RUN apk add --no-cache curl=8.14.1-r2 jq=1.8.1-r0 +RUN apk add --no-cache jq=1.8.1-r0 COPY requirements.txt requirements.txt From c4dd086f76520fb83dc1978f9be2d421c2916379 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:20:17 +0000 Subject: [PATCH 6/9] fix(ci): moving checkov job into docker publish workflow --- .github/workflows/checkov.yaml | 38 --------------------------- .github/workflows/docker-publish.yaml | 11 +++++++- .github/workflows/python-app.yaml | 2 +- 3 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 .github/workflows/checkov.yaml diff --git a/.github/workflows/checkov.yaml b/.github/workflows/checkov.yaml deleted file mode 100644 index a09f71a..0000000 --- a/.github/workflows/checkov.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: 'Checkov' - -on: - push: - pull_request: - - workflow_dispatch: - -permissions: - contents: read - -jobs: - scan: - name: 'Checkov' - environment: production - - permissions: - contents: read - security-events: write - actions: read - - runs-on: ubuntu-latest - - defaults: - run: - shell: bash - - steps: - - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - - name: Checkov Dockerfile - uses: bridgecrewio/checkov-action@99bb2caf247dfd9f03cf984373bc6043d4e32ebf # v12.1347.0 - with: - file: Dockerfile - quiet: true - output_format: cli,sarif - output_file_path: console,results.sarif \ No newline at end of file diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index def4a22..d784ea3 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -67,4 +67,13 @@ jobs: env: TAGS: ${{ steps.meta.outputs.tags }} DIGEST: ${{ steps.build-and-push.outputs.digest }} - run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} + + - name: Checkov Dockerfile + uses: bridgecrewio/checkov-action@99bb2caf247dfd9f03cf984373bc6043d4e32ebf # v12.1347.0 + with: + quiet: true + soft_fail: true + docker_image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + dockerfile_path: ./Dockerfile + container_user: 10001 diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 1b4ef70..088da8e 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -10,7 +10,7 @@ permissions: contents: read jobs: - build: + lint-and-test: runs-on: ubuntu-latest steps: From 92c0daba9dda9b8aceb17f4fb49ed15d504818a3 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:23:55 +0000 Subject: [PATCH 7/9] fix(dockerfile): resolve checkov issues with docker image --- .github/workflows/docker-publish.yaml | 1 - Dockerfile | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index d784ea3..6529791 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -73,7 +73,6 @@ jobs: uses: bridgecrewio/checkov-action@99bb2caf247dfd9f03cf984373bc6043d4e32ebf # v12.1347.0 with: quiet: true - soft_fail: true docker_image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} dockerfile_path: ./Dockerfile container_user: 10001 diff --git a/Dockerfile b/Dockerfile index b1411c0..b2feb46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,9 @@ USER tokentx COPY src/ /app/src/ +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health/ready || exit 1 + ENTRYPOINT ["python", "src/main.py"] From 9ebafee9ef9fce16ae48ffe5e54094e105f6f9c1 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:43:16 +0000 Subject: [PATCH 8/9] fix(ci): format docker build versions --- .github/workflows/docker-publish.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index 6529791..3c8e0fc 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -49,6 +49,11 @@ jobs: uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} # https://github.com/docker/build-push-action - name: Build and push Docker image From 92f29efc5c5f7dafa7d0570f6fb7e893de6cbc21 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:46:36 +0000 Subject: [PATCH 9/9] chore(deps): fixing dockerfile image to digest --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b2feb46..9ffe961 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.12-alpine3.23 +FROM python:3.12.12-alpine3.23@sha256:f47255bb0de452ac59afc49eaabe992720fe282126bb6a4f62de9dd3db1742dc ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1