diff --git a/.circleci/config.yml b/.circleci/config.yml index abd9b16..1a96484 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2 jobs: build: docker: - - image: circleci/python:3.7-node + - image: circleci/python:3.8-node - image: circleci/mysql:8.0.21 command: [--default-authentication-plugin=mysql_native_password] environment: @@ -20,12 +20,12 @@ jobs: command: | shasum requirement*.txt > /tmp/checksum_files_list sudo chown -R circleci:circleci /usr/local/bin - sudo chown circleci:circleci -R /usr/local/lib/python3.7 + sudo chown circleci:circleci -R /usr/local/lib/python3.8 - run: name: Revert the permissions command: | sudo chown root:root -R /usr/local/bin - sudo chown root:root -R /usr/local/lib/python3.7 + sudo chown root:root -R /usr/local/lib/python3.8 - run: # Our primary container isn't MYSQL so run a sleep command until it's ready. name: Waiting for MySQL to be ready @@ -68,4 +68,4 @@ jobs: - save_cache: key: dependency-cache-{{ checksum "/tmp/checksum_files_list" }} paths: - - /usr/local/lib/python3.7/site-packages + - /usr/local/lib/python3.8/site-packages diff --git a/.github/workflows/npm_audit.yml b/.github/workflows/npm_audit.yml new file mode 100644 index 0000000..58174f7 --- /dev/null +++ b/.github/workflows/npm_audit.yml @@ -0,0 +1,26 @@ +name: NPM Security Audit + +on: + push: + branches: [development, master] + pull_request: + branches: [development, master] + +jobs: + security-audit: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm install + + - name: Run npm audit + run: npm audit --audit-level=critical diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000..4548716 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,23 @@ +name: Sonar Analysis Workflow + +on: + push: + branches: + - master + - development + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - uses: SonarSource/sonarqube-scan-action@v4 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/alembic/versions/e6dfb71c97ce_added_trading_view.py b/alembic/versions/e6dfb71c97ce_added_trading_view.py new file mode 100644 index 0000000..c363f49 --- /dev/null +++ b/alembic/versions/e6dfb71c97ce_added_trading_view.py @@ -0,0 +1,41 @@ +"""added_trading_view + +Revision ID: e6dfb71c97ce +Revises: 19c20537fdbb +Create Date: 2025-01-16 20:47:52.983186 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e6dfb71c97ce' +down_revision = '19c20537fdbb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('trading_view', + sa.Column('row_id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('id', sa.VARCHAR(length=50), nullable=False), + sa.Column('symbol', sa.VARCHAR(length=250), nullable=True), + sa.Column('alt_text', sa.TEXT(), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('row_id'), + sa.UniqueConstraint('id') + ) + op.add_column('token', sa.Column('trading_view_id', sa.BIGINT(), nullable=True)) + op.create_foreign_key(None, 'token', 'trading_view', ['trading_view_id'], ['row_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'token', type_='foreignkey') + op.drop_column('token', 'trading_view_id') + op.drop_table('trading_view') + # ### end Alembic commands ### diff --git a/alembic/versions/f75df319b335_added_ada_thershold.py b/alembic/versions/f75df319b335_added_ada_thershold.py new file mode 100644 index 0000000..bc117c6 --- /dev/null +++ b/alembic/versions/f75df319b335_added_ada_thershold.py @@ -0,0 +1,28 @@ +"""added_ada_thershold + +Revision ID: f75df319b335 +Revises: e6dfb71c97ce +Create Date: 2025-01-28 14:44:37.728102 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f75df319b335' +down_revision = 'e6dfb71c97ce' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('token_pair', sa.Column('ada_threshold', sa.BIGINT(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('token_pair', 'ada_threshold') + # ### end Alembic commands ### diff --git a/application/handler/conversion_handlers.py b/application/handler/conversion_handlers.py index 3fd85f8..13c8d06 100644 --- a/application/handler/conversion_handlers.py +++ b/application/handler/conversion_handlers.py @@ -7,7 +7,7 @@ from web3 import Web3 from http import HTTPStatus -from constants.general import MAX_PAGE_SIZE +from constants.general import MAX_PAGE_SIZE, ConversionHistoryOrder from constants.api_parameters import ApiParameters from constants.lambdas import HttpRequestParamType, LambdaResponseStatus, PaginationDefaults from constants.error_details import ErrorCode, ErrorDetails @@ -98,7 +98,14 @@ def get_conversion_history(event, context): schema_key="GetConversionHistoryInput", input_json=event) query_param = get_valid_value(event, HttpRequestParamType.REQUEST_PARAM_QUERY_STRING.value) - address = query_param.get(ApiParameters.ADDRESS.value, None) + address = query_param.get(ApiParameters.ADDRESS.value) + blockchain_name = query_param.get(ApiParameters.BLOCKCHAIN_NAME.value) + token_symbol = query_param.get(ApiParameters.TOKEN_SYMBOL.value) + conversion_status = query_param.get(ApiParameters.CONVERSION_STATUS.value) + try: + history_order = ConversionHistoryOrder(query_param.get(ApiParameters.ORDER_BY.value).upper()) + except (AttributeError, ValueError): + history_order = ConversionHistoryOrder.DEFAULT if not address: raise BadRequestException(error_code=ErrorCode.PROPERTY_VALUES_EMPTY.value, @@ -115,11 +122,19 @@ def get_conversion_history(event, context): raise BadRequestException(error_code=ErrorCode.PAGE_SIZE_EXCEEDS_LIMIT.value, error_details=ErrorDetails[ErrorCode.PAGE_SIZE_EXCEEDS_LIMIT.value].value) - response = conversion_service.get_conversion_history(address=address, page_size=page_size, page_number=page_number) + response = conversion_service.get_conversion_history(address=address, + blockchain_name=blockchain_name, + token_symbol=token_symbol, + conversion_status=conversion_status, + order=history_order, + page_size=page_size, + page_number=page_number) return generate_lambda_response(HTTPStatus.OK.value, - make_response_body(status=LambdaResponseStatus.SUCCESS.value, data=response, - error=make_error_format()), cors_enabled=True) + make_response_body(status=LambdaResponseStatus.SUCCESS.value, + data=response, + error=make_error_format()), + cors_enabled=True) @exception_handler(EXCEPTIONS=EXCEPTIONS, SLACK_HOOK=SLACK_HOOK, logger=logger) diff --git a/application/service/consumer_service.py b/application/service/consumer_service.py index 9ed3462..023041a 100644 --- a/application/service/consumer_service.py +++ b/application/service/consumer_service.py @@ -25,7 +25,7 @@ generate_deposit_address_details_for_cardano_operation, \ validate_conversion_request_amount, validate_consumer_event_type, convert_str_to_decimal, \ get_current_block_confirmation, wait_until_transaction_hash_exists_in_blockchain, \ - validate_tx_hash_presence_in_blockchain + validate_tx_hash_presence_in_blockchain, validate_tx_token_received_ada_amount from utils.exception_handler import bridge_exception_handler from utils.exceptions import BadRequestException, InternalServerErrorException, BlockConfirmationNotEnoughException @@ -134,7 +134,8 @@ def process_event_consumer(self, event_type, tx_hash, network_id, blockchain_eve elif db_blockchain_name == BlockchainName.CARDANO.value.lower(): if event_type == CardanoEventType.TOKEN_RECEIVED.value: conversion = self.process_cardano_token_received_event(blockchain_event=blockchain_event, - transaction=transaction) + transaction=transaction, + network_id=network_id) elif transaction and event_type in CardanoServicesEventTypes: conversion = self.conversion_service.get_conversion_detail_by_tx_id( tx_id=transaction.get(TransactionEntities.ID.value)) @@ -273,7 +274,7 @@ def process_evm_event(self, event_type, tx_hash, tx_amount, conversion_id, trans return conversion - def process_cardano_token_received_event(self, blockchain_event, transaction): + def process_cardano_token_received_event(self, blockchain_event, transaction, network_id): created_by = CreatedBy.BACKEND.value fee_amount = Decimal(0) tx_hash = blockchain_event.get(CardanoEventConsumer.TX_HASH.value) @@ -307,6 +308,12 @@ def process_cardano_token_received_event(self, blockchain_event, transaction): min_value=token_pair.get(TokenPairEntities.MIN_VALUE.value), max_value=token_pair.get(TokenPairEntities.MAX_VALUE.value)) + target_amount = token_pair.get(TokenPairEntities.ADA_THRESHOLD.value) + validate_tx_token_received_ada_amount(target_amount=target_amount, + address=deposit_address, + tx_hash=tx_hash, + network_id=network_id) + token_address = token_pair.get(TokenPairEntities.FROM_TOKEN.value, {}) \ .get(TokenEntities.TOKEN_ADDRESS.value) token_symbol = token_pair.get(TokenPairEntities.FROM_TOKEN.value, {}).get(TokenEntities.SYMBOL.value) @@ -396,6 +403,12 @@ def process_cardano_token_received_event(self, blockchain_event, transaction): min_value=token_pair.get(TokenPairEntities.MIN_VALUE.value), max_value=token_pair.get(TokenPairEntities.MAX_VALUE.value)) + target_amount = token_pair.get(TokenPairEntities.ADA_THRESHOLD.value) + validate_tx_token_received_ada_amount(target_amount=target_amount, + address=deposit_address, + tx_hash=tx_hash, + network_id=network_id) + token_address = token_pair.get(TokenPairEntities.FROM_TOKEN.value, {}) \ .get(TokenEntities.TOKEN_ADDRESS.value) token_symbol = token_pair.get(TokenPairEntities.FROM_TOKEN.value, {}).get(TokenEntities.SYMBOL.value) diff --git a/application/service/conversion_fee_response.py b/application/service/conversion_fee_response.py new file mode 100644 index 0000000..60f5653 --- /dev/null +++ b/application/service/conversion_fee_response.py @@ -0,0 +1,33 @@ +from application.service.blockchain_response import get_blockchain_for_token_response +from constants.entity import ConversionFeeEntities, TokenEntities + + +def get_conversion_fee_response(conversion_fee): + if not len(conversion_fee): + return conversion_fee + + return { + ConversionFeeEntities.ID.value: conversion_fee[ConversionFeeEntities.ID.value], + ConversionFeeEntities.PERCENTAGE_FROM_SOURCE.value: + conversion_fee[ConversionFeeEntities.PERCENTAGE_FROM_SOURCE.value], + ConversionFeeEntities.TOKEN.value: + get_short_token_response(conversion_fee[ConversionFeeEntities.TOKEN.value]), + ConversionFeeEntities.UPDATED_AT.value: conversion_fee[ConversionFeeEntities.UPDATED_AT.value] + } + + +def get_short_token_response(token): + if not len(token): + return token + + return { + TokenEntities.ID.value: token[TokenEntities.ID.value], + TokenEntities.NAME.value: token[TokenEntities.NAME.value], + TokenEntities.SYMBOL.value: token[TokenEntities.SYMBOL.value], + TokenEntities.LOGO.value: token[TokenEntities.LOGO.value], + TokenEntities.ALLOWED_DECIMAL.value: token[TokenEntities.ALLOWED_DECIMAL.value], + TokenEntities.TOKEN_ADDRESS.value: token[TokenEntities.TOKEN_ADDRESS.value], + TokenEntities.CONTRACT_ADDRESS.value: token[TokenEntities.CONTRACT_ADDRESS.value], + TokenEntities.UPDATED_AT.value: token[TokenEntities.UPDATED_AT.value], + TokenEntities.BLOCKCHAIN.value: get_blockchain_for_token_response(token[TokenEntities.BLOCKCHAIN.value]) + } diff --git a/application/service/conversion_fee_respose.py b/application/service/conversion_fee_respose.py deleted file mode 100644 index 67e5ce4..0000000 --- a/application/service/conversion_fee_respose.py +++ /dev/null @@ -1,13 +0,0 @@ -from constants.entity import ConversionFeeEntities - - -def get_conversion_fee_response(conversion_fee): - if not len(conversion_fee): - return conversion_fee - - return { - ConversionFeeEntities.ID.value: conversion_fee[ConversionFeeEntities.ID.value], - ConversionFeeEntities.PERCENTAGE_FROM_SOURCE.value: conversion_fee[ - ConversionFeeEntities.PERCENTAGE_FROM_SOURCE.value], - ConversionFeeEntities.UPDATED_AT.value: conversion_fee[ConversionFeeEntities.UPDATED_AT.value] - } diff --git a/application/service/conversion_response.py b/application/service/conversion_response.py index 7942c9d..fbf95f3 100644 --- a/application/service/conversion_response.py +++ b/application/service/conversion_response.py @@ -1,5 +1,6 @@ from constants.entity import ConversionEntities, TokenPairEntities, WalletPairEntities, ConversionDetailEntities, TokenEntities, \ BlockchainEntities, TransactionEntities, TransactionConversionEntities, SignatureMetadataEntities +from application.service.token_response import get_trading_view_response def conversion_response(conversion): @@ -75,6 +76,7 @@ def get_blockchain_response(blockchain): return { BlockchainEntities.NAME.value: blockchain[BlockchainEntities.NAME.value], BlockchainEntities.SYMBOL.value: blockchain[BlockchainEntities.SYMBOL.value], + BlockchainEntities.LOGO.value: blockchain[BlockchainEntities.LOGO.value], BlockchainEntities.CHAIN_ID.value: blockchain[BlockchainEntities.CHAIN_ID.value] } @@ -83,8 +85,10 @@ def get_token_response(token): return { TokenEntities.NAME.value: token[TokenEntities.NAME.value], TokenEntities.SYMBOL.value: token[TokenEntities.SYMBOL.value], + TokenEntities.LOGO.value: token[TokenEntities.LOGO.value], TokenEntities.ALLOWED_DECIMAL.value: token[TokenEntities.ALLOWED_DECIMAL.value], - TokenEntities.BLOCKCHAIN.value: get_blockchain_response(token[TokenEntities.BLOCKCHAIN.value]) + TokenEntities.BLOCKCHAIN.value: get_blockchain_response(token[TokenEntities.BLOCKCHAIN.value]), + # TokenEntities.TRADING_VIEW.value: get_trading_view_response(token[TokenEntities.TRADING_VIEW.value]) } @@ -93,7 +97,8 @@ def get_token_internal_response(token): TokenEntities.ROW_ID.value: token[TokenEntities.ROW_ID.value], TokenEntities.NAME.value: token[TokenEntities.NAME.value], TokenEntities.SYMBOL.value: token[TokenEntities.SYMBOL.value], - TokenEntities.BLOCKCHAIN.value: get_blockchain_response(token[TokenEntities.BLOCKCHAIN.value]) + TokenEntities.BLOCKCHAIN.value: get_blockchain_response(token[TokenEntities.BLOCKCHAIN.value]), + # TokenEntities.TRADING_VIEW.value: get_trading_view_response(token[TokenEntities.TRADING_VIEW.value]) } diff --git a/application/service/conversion_service.py b/application/service/conversion_service.py index 62fc8ee..d6277fd 100644 --- a/application/service/conversion_service.py +++ b/application/service/conversion_service.py @@ -17,8 +17,9 @@ ConversionEntities, TokenEntities, BlockchainEntities, ConversionDetailEntities, TransactionConversionEntities, \ TransactionEntities, ConversionFeeEntities, ConverterBridgeEntities, EventConsumerEntity, TokenLiquidityEntities from constants.error_details import ErrorCode, ErrorDetails -from constants.general import BlockchainName, CreatedBy, SignatureTypeEntities, ConversionOn +from constants.general import BlockchainName, CreatedBy, SignatureTypeEntities, ConversionOn, ConversionHistoryOrder from constants.status import ConversionStatus, TransactionVisibility, TransactionStatus +from constants.lambdas import PaginationDefaults from infrastructure.repositories.conversion_repository import ConversionRepository from utils.blockchain import validate_address, validate_conversion_claim_request_signature, \ validate_conversion_request_amount, convert_str_to_decimal, get_next_activity_event_on_conversion, \ @@ -441,15 +442,29 @@ def get_token_contract_address_for_conversion_id(self, conversion_on, conversion error_code=ErrorCode.INVALID_CONVERSION_DIRECTION.value, error_details=ErrorDetails[ErrorCode.INVALID_CONVERSION_DIRECTION.value].value) - def get_conversion_history(self, address, page_size, page_number): - logger.info(f"Getting the conversion history for the given address={address}, page_size={page_size}, " - f"page_number={page_number}") - total_conversion_history = self.conversion_repo.get_conversion_history_count(address=address) + def get_conversion_history(self, address, blockchain_name, token_symbol, conversion_status, + order=ConversionHistoryOrder.DEFAULT, + page_size=PaginationDefaults.PAGE_SIZE.value, + page_number=PaginationDefaults.PAGE_NUMBER.value): + logger.info(f"Getting the conversion history for the given address={address}, " + f"blockchain={blockchain_name}, token={token_symbol}, status={conversion_status}, " + f"order={order.value}, page_size={page_size}, page_number={page_number}") + total_conversion_history = self.conversion_repo.get_conversion_history_count( + address=address, + blockchain_name=blockchain_name, + token_symbol=token_symbol, + conversion_status=conversion_status + ) offset = get_offset(page_number=page_number, page_size=page_size) if total_conversion_history and total_conversion_history > offset: - conversion_history_obj = self.conversion_repo.get_conversion_history(address=address, conversion_id=None, - offset=offset, limit=page_size) + conversion_history_obj = self.conversion_repo.get_conversion_history(address=address, + blockchain_name=blockchain_name, + token_symbol=token_symbol, + conversion_status=conversion_status, + order=order, + offset=offset, + limit=page_size) conversion_history = get_response_from_entities(conversion_history_obj) conversion_detail_history_response = get_conversion_history_response(conversion_history) else: @@ -457,7 +472,8 @@ def get_conversion_history(self, address, page_size, page_number): return paginate_items_response_format(items=conversion_detail_history_response, total_records=total_conversion_history, - page_number=page_number, page_size=page_size) + page_number=page_number, + page_size=page_size) @staticmethod def get_conversion_row_ids(conversion_details): diff --git a/application/service/token_reponse.py b/application/service/token_response.py similarity index 83% rename from application/service/token_reponse.py rename to application/service/token_response.py index 521615b..bfcb7be 100644 --- a/application/service/token_reponse.py +++ b/application/service/token_response.py @@ -1,18 +1,27 @@ from application.service.blockchain_response import get_blockchain_for_token_response -from application.service.conversion_fee_respose import get_conversion_fee_response -from constants.entity import TokenPairEntities, TokenEntities +from application.service.conversion_fee_response import get_conversion_fee_response +from constants.entity import TokenPairEntities, TokenEntities, TradingViewEntities + + +def get_trading_view_response(trading_view): + return { + TradingViewEntities.SYMBOL.value: trading_view[TradingViewEntities.SYMBOL.value], + TradingViewEntities.ALT_TEXT.value: trading_view[TradingViewEntities.ALT_TEXT.value] + } def get_token_response(token): return { TokenEntities.ID.value: token[TokenEntities.ID.value], + TokenEntities.NAME.value: token[TokenEntities.NAME.value], TokenEntities.SYMBOL.value: token[TokenEntities.SYMBOL.value], TokenEntities.LOGO.value: token[TokenEntities.LOGO.value], TokenEntities.ALLOWED_DECIMAL.value: token[TokenEntities.ALLOWED_DECIMAL.value], TokenEntities.TOKEN_ADDRESS.value: token[TokenEntities.TOKEN_ADDRESS.value], TokenEntities.CONTRACT_ADDRESS.value: token[TokenEntities.CONTRACT_ADDRESS.value], TokenEntities.UPDATED_AT.value: token[TokenEntities.UPDATED_AT.value], - TokenEntities.BLOCKCHAIN.value: get_blockchain_for_token_response(token[TokenEntities.BLOCKCHAIN.value]) + TokenEntities.BLOCKCHAIN.value: get_blockchain_for_token_response(token[TokenEntities.BLOCKCHAIN.value]), + TokenEntities.TRADING_VIEW.value: get_trading_view_response(token[TokenEntities.TRADING_VIEW.value]) } @@ -56,6 +65,7 @@ def get_token_pair_internal_response(token_pair): TokenPairEntities.FROM_TOKEN.value: get_token_response(token_pair[TokenPairEntities.FROM_TOKEN.value]), TokenPairEntities.TO_TOKEN.value: get_token_response(token_pair[TokenPairEntities.TO_TOKEN.value]), TokenPairEntities.IS_LIQUID.value: token_pair[TokenPairEntities.IS_LIQUID.value], + TokenPairEntities.ADA_THRESHOLD.value: token_pair[TokenPairEntities.ADA_THRESHOLD.value], TokenPairEntities.CONVERSION_FEE.value: get_conversion_fee_response( token_pair[TokenPairEntities.CONVERSION_FEE.value]), TokenPairEntities.UPDATED_AT.value: token_pair[TokenPairEntities.UPDATED_AT.value] diff --git a/application/service/token_service.py b/application/service/token_service.py index 5cf6f95..eb85ac7 100644 --- a/application/service/token_service.py +++ b/application/service/token_service.py @@ -1,5 +1,8 @@ -from application.service.token_reponse import get_all_token_pair_response, get_token_pair_response, \ - get_token_pair_internal_response +from application.service.token_response import ( + get_all_token_pair_response, + get_token_pair_internal_response, + get_token_pair_response +) from common.logger import get_logger from infrastructure.repositories.token_repository import TokenRepository from utils.general import get_response_from_entities diff --git a/constants/api_parameters.py b/constants/api_parameters.py index c9f8bd6..c31c887 100644 --- a/constants/api_parameters.py +++ b/constants/api_parameters.py @@ -2,6 +2,8 @@ class ApiParameters(Enum): + BLOCKCHAIN_NAME = "blockchain_name" + TOKEN_SYMBOL = "token_symbol" TOKEN_PAIR_ID = "token_pair_id" AMOUNT = "amount" FROM_ADDRESS = "from_address" @@ -10,9 +12,11 @@ class ApiParameters(Enum): SIGNATURE = "signature" KEY = "key" CONVERSION_ID = "conversion_id" + CONVERSION_STATUS = "conversion_status" TRANSACTION_HASH = "transaction_hash" PAGE_SIZE = "page_size" PAGE_NUMBER = "page_number" + ORDER_BY = "order_by" ADDRESS = "address" ETHEREUM_ADDRESS = "ethereum_address" diff --git a/constants/blockchain.py b/constants/blockchain.py index 952869e..e7ea83a 100644 --- a/constants/blockchain.py +++ b/constants/blockchain.py @@ -65,3 +65,6 @@ class BinanceBlockchainEntities(Enum): class CardanoBlockEntities(Enum): CONFIRMATIONS = "confirmations" + + +DEFAULT_ADA_THRESHOLD = 0 diff --git a/constants/entity.py b/constants/entity.py index d0b78a8..5e63d4b 100644 --- a/constants/entity.py +++ b/constants/entity.py @@ -26,11 +26,21 @@ class TokenEntities(Enum): TOKEN_ADDRESS = "token_address" CONTRACT_ADDRESS = "contract_address" BLOCKCHAIN = "blockchain" + TRADING_VIEW = "trading_view" CREATED_BY = "created_by" CREATED_AT = "created_at" UPDATED_AT = "updated_at" +class TradingViewEntities(Enum): + ROW_ID = "row_id" + ID = "id" + SYMBOL = "symbol" + ALT_TEXT = "alt_text" + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + + class TokenPairEntities(Enum): ROW_ID = "row_id" ID = "id" @@ -41,6 +51,7 @@ class TokenPairEntities(Enum): CONVERSION_FEE = "conversion_fee" CONVERSION_RATIO = "conversion_ratio" IS_LIQUID = "check_liquidity" + ADA_THRESHOLD = "ada_threshold" CREATED_BY = "created_by" CREATED_AT = "created_at" UPDATED_AT = "updated_at" @@ -56,6 +67,7 @@ class TokenLiquidityEntities(Enum): class ConversionFeeEntities(Enum): ID = "id" PERCENTAGE_FROM_SOURCE = "percentage_from_source" + TOKEN = "token" CREATED_BY = "created_by" CREATED_AT = "created_at" UPDATED_AT = "updated_at" diff --git a/constants/error_details.py b/constants/error_details.py index 7651acb..eccd2e7 100644 --- a/constants/error_details.py +++ b/constants/error_details.py @@ -44,8 +44,8 @@ class ErrorCode(Enum): UNABLE_TO_PARSE_THE_INPUT_EVENT = "E0040" QUEUE_DETAILS_NOT_FOUND = "E0041" INVALID_TRANSACTION_OPERATION = "E0042" - LAMBDA_ARN_MINT_NOT_FOUND = "E0043" # TODO currently unused, candidate for removal - LAMBDA_ARN_BURN_NOT_FOUND = "E0044" # TODO currently unused, candidate for removal + INVALID_TX_ADA_AMOUNT = "E0043" + UNEXPECTED_ERROR_ON_ADA_AMOUNT_VALIDATION = "E0044" SECRET_KEY_NOT_FOUND = "E0045" SECRET_DETAILS_FOR_CONTRACT_NOT_AVAILABLE = "E0046" INVALID_SIGNATURE_TYPE_PROVIDED = "E0047" @@ -130,8 +130,8 @@ class ErrorDetails(Enum): E0040 = "Unable to parse the input event provided" E0041 = "Queue details not found" E0042 = "Invalid Transaction Operation provided" - E0043 = "Config of lambda arn for minting is empty" # TODO currently unused, candidate for removal - E0044 = "Config of lambda arn for burn is empty" # TODO currently unused, candidate for removal + E0043 = "Amount of ADA in given transaction is below threshold" + E0044 = "Unexpected error on transaction ADA amount validation" E0045 = "Secret key not found for signing" E0046 = "Secret details for this contract not available" E0047 = "Invalid signature type provided" diff --git a/constants/general.py b/constants/general.py index 85f3931..dd45d5b 100644 --- a/constants/general.py +++ b/constants/general.py @@ -47,6 +47,12 @@ class SleepTimeEntities(Enum): TRANSACTION_HASH_PRESENCE = "TRANSACTION_HASH_PRESENCE" +class ConversionHistoryOrder(Enum): + STATUS = "STATUS" + DATE = "DATE" + DEFAULT = STATUS + + SIGNATURE_TYPES = [SignatureTypeEntities.CONVERSION_IN.value, SignatureTypeEntities.CONVERSION_OUT.value] ENV_CONVERTER_SIGNER_PRIVATE_KEY_PATH = { diff --git a/constants/status.py b/constants/status.py index a665ad1..9ffb284 100644 --- a/constants/status.py +++ b/constants/status.py @@ -8,12 +8,14 @@ class ConversionStatus(Enum): SUCCESS = "SUCCESS" CLAIM_INITIATED = "CLAIM_INITIATED" EXPIRED = "EXPIRED" + CANCELED = "CANCELED" class ConversionTransactionStatus(Enum): FAILED = "FAILED" SUCCESS = "SUCCESS" PROCESSING = "PROCESSING" + CANCELED = "CANCELED" class TransactionVisibility(Enum): @@ -26,6 +28,7 @@ class TransactionOperation(Enum): TOKEN_BURNT = "TOKEN_BURNT" TOKEN_MINTED = "TOKEN_MINTED" TOKEN_TRANSFERRED = "TOKEN_TRANSFERRED" + TOKEN_REFUNDED = "TOKEN_REFUNDED" EthereumToCardanoEvent = {"ethereum": [TransactionOperation.TOKEN_BURNT.value], diff --git a/domain/entities/conversion_fee.py b/domain/entities/conversion_fee.py index dd1f1c4..0f83dd8 100644 --- a/domain/entities/conversion_fee.py +++ b/domain/entities/conversion_fee.py @@ -2,22 +2,26 @@ from decimal import Decimal from constants.entity import ConversionFeeEntities +from domain.entities.token import Token from utils.general import datetime_to_str class ConversionFee: - def __init__(self, id: str, percentage_from_source: Decimal, created_by: str, created_at: date, updated_at: date): + def __init__(self, id: str, percentage_from_source: Decimal, token_obj: Token, created_by: str, created_at: date, updated_at: date): self.id = id self.percentage_from_source = str(percentage_from_source.normalize()) + self.token_obj = token_obj self.created_by = created_by self.created_at = datetime_to_str(created_at) self.updated_at = datetime_to_str(updated_at) def to_dict(self): + token_obj = {} if self.token_obj is None else self.token_obj.to_dict() return { ConversionFeeEntities.ID.value: self.id, ConversionFeeEntities.PERCENTAGE_FROM_SOURCE.value: self.percentage_from_source, + ConversionFeeEntities.TOKEN.value: token_obj, ConversionFeeEntities.CREATED_BY.value: self.created_by, ConversionFeeEntities.CREATED_AT.value: self.created_at, ConversionFeeEntities.UPDATED_AT.value: self.updated_at diff --git a/domain/entities/token.py b/domain/entities/token.py index d8a6077..218a8ed 100644 --- a/domain/entities/token.py +++ b/domain/entities/token.py @@ -2,13 +2,14 @@ from constants.entity import TokenEntities from domain.entities.blockchain import Blockchain +from domain.entities.trading_view import TradingView from utils.general import datetime_to_str class Token: def __init__(self, row_id: int, id_: str, name: str, description: str, symbol: str, logo: str, allowed_decimal: int, token_address: str, contract_address: str, created_by: str, created_at: date, updated_at: date, - blockchain_obj: Blockchain): + blockchain_obj: Blockchain, trading_view_obj: TradingView): self.row_id = row_id self.id = id_ self.name = name @@ -19,12 +20,14 @@ def __init__(self, row_id: int, id_: str, name: str, description: str, symbol: s self.token_address = token_address self.contract_address = contract_address self.blockchain_obj = blockchain_obj + self.trading_view_obj = trading_view_obj self.created_by = created_by self.created_at = datetime_to_str(created_at) self.updated_at = datetime_to_str(updated_at) def to_dict(self): blockchain = {} if self.blockchain_obj is None else self.blockchain_obj.to_dict() + trading_view = {} if self.trading_view_obj is None else self.trading_view_obj.to_dict() return { TokenEntities.ROW_ID.value: self.row_id, TokenEntities.ID.value: self.id, @@ -38,5 +41,6 @@ def to_dict(self): TokenEntities.CREATED_BY.value: self.created_by, TokenEntities.CREATED_AT.value: self.created_at, TokenEntities.UPDATED_AT.value: self.updated_at, - TokenEntities.BLOCKCHAIN.value: blockchain + TokenEntities.BLOCKCHAIN.value: blockchain, + TokenEntities.TRADING_VIEW.value: trading_view } diff --git a/domain/entities/token_pair.py b/domain/entities/token_pair.py index 9c7f924..08fb52e 100644 --- a/domain/entities/token_pair.py +++ b/domain/entities/token_pair.py @@ -1,6 +1,7 @@ from datetime import date from constants.entity import TokenPairEntities +from constants.blockchain import DEFAULT_ADA_THRESHOLD from domain.entities.conversion_fee import ConversionFee from domain.entities.token import Token from utils.general import datetime_to_str @@ -11,7 +12,7 @@ class TokenPair: def __init__(self, row_id: int, id_: str, min_value: Decimal, max_value: Decimal, created_by: str, created_at: date, updated_at: date, from_token_obj: Token, to_token_obj: Token, conversion_fee_obj: ConversionFee, - conversion_ratio: Decimal, is_liquid: bool): + conversion_ratio: Decimal, is_liquid: bool, ada_threshold: int): self.row_id = row_id self.id = id_ self.min_value = str(min_value.normalize()) @@ -22,6 +23,7 @@ def __init__(self, row_id: int, id_: str, min_value: Decimal, max_value: Decimal self.conversion_fee_obj = conversion_fee_obj self.conversion_ratio = str(conversion_ratio) if conversion_ratio else None self.is_liquid = is_liquid + self.ada_threshold = ada_threshold if ada_threshold is not None else DEFAULT_ADA_THRESHOLD self.created_at = datetime_to_str(created_at) self.updated_at = datetime_to_str(updated_at) @@ -40,6 +42,7 @@ def to_dict(self): TokenPairEntities.CONVERSION_FEE.value: conversion_fee, TokenPairEntities.CONVERSION_RATIO.value: self.conversion_ratio, TokenPairEntities.IS_LIQUID.value: self.is_liquid, + TokenPairEntities.ADA_THRESHOLD.value: self.ada_threshold, TokenPairEntities.CREATED_BY.value: self.created_by, TokenPairEntities.CREATED_AT.value: self.created_at, TokenPairEntities.UPDATED_AT.value: self.updated_at diff --git a/domain/entities/trading_view.py b/domain/entities/trading_view.py new file mode 100644 index 0000000..1681294 --- /dev/null +++ b/domain/entities/trading_view.py @@ -0,0 +1,25 @@ +from datetime import date + +from constants.entity import TradingViewEntities +from utils.general import datetime_to_str + + +class TradingView: + + def __init__(self, row_id: int, id_: str, symbol: str, alt_text: str, created_at: date, updated_at: date): + self.row_id = row_id + self.id = id_ + self.symbol = symbol + self.alt_text = alt_text + self.created_at = datetime_to_str(created_at) + self.updated_at = datetime_to_str(updated_at) + + def to_dict(self): + return { + TradingViewEntities.ROW_ID.value: self.row_id, + TradingViewEntities.ID.value: self.id, + TradingViewEntities.SYMBOL.value: self.symbol, + TradingViewEntities.ALT_TEXT.value: self.alt_text, + TradingViewEntities.CREATED_AT.value: self.created_at, + TradingViewEntities.UPDATED_AT.value: self.updated_at + } diff --git a/domain/factory/conversion_factory.py b/domain/factory/conversion_factory.py index a4d402c..7ec7163 100644 --- a/domain/factory/conversion_factory.py +++ b/domain/factory/conversion_factory.py @@ -68,7 +68,8 @@ def conversion_detail(conversion): token_address=from_token.token_address, contract_address=from_token.contract_address, created_by=from_token.created_by, created_at=from_token.created_at, - updated_at=from_token.updated_at, blockchain_detail=from_blockchain) + updated_at=from_token.updated_at, blockchain_detail=from_blockchain, + trading_view=from_token.trading_view) to_token = token_pair.to_token to_blockchain = to_token.blockchain_detail @@ -79,7 +80,8 @@ def conversion_detail(conversion): token_address=from_token.token_address, contract_address=to_token.contract_address, created_by=to_token.created_by, created_at=to_token.created_at, - updated_at=to_token.updated_at, blockchain_detail=to_blockchain) + updated_at=to_token.updated_at, blockchain_detail=to_blockchain, + trading_view=to_token.trading_view) token_pair_obj = TokenFactory.token_pair(row_id=token_pair.row_id, id_=token_pair.id, min_value=token_pair.min_value, max_value=token_pair.max_value, @@ -87,7 +89,8 @@ def conversion_detail(conversion): updated_at=token_pair.updated_at, from_token=token_pair.from_token, to_token=token_pair.to_token, conversion_fee=token_pair.conversion_fee, conversion_ratio=token_pair.conversion_ratio, - is_liquid=token_pair.is_liquid) + is_liquid=token_pair.is_liquid, + ada_threshold=token_pair.ada_threshold) return ConversionDetail(conversion_obj=conversion_obj, wallet_pair_obj=wallet_pair_obj, from_token_obj=from_token_obj, to_token_obj=to_token_obj, @@ -113,7 +116,8 @@ def transaction_detail(transaction): contract_address=token_db_obj.contract_address, created_by=token_db_obj.created_by, created_at=token_db_obj.created_at, updated_at=token_db_obj.updated_at, - blockchain_detail=token_db_obj.blockchain_detail) + blockchain_detail=token_db_obj.blockchain_detail, + trading_view=token_db_obj.trading_view) return ConversionFactory.transaction(row_id=transaction.row_id, id=transaction.id, conversion_transaction_id=transaction.conversion_transaction_id, diff --git a/domain/factory/conversion_fee_factory.py b/domain/factory/conversion_fee_factory.py index 6353feb..d0bdeb5 100644 --- a/domain/factory/conversion_fee_factory.py +++ b/domain/factory/conversion_fee_factory.py @@ -4,17 +4,18 @@ class ConversionFeeFactory: @staticmethod - def conversion_fee(id, percentage_from_source, created_by, created_at, updated_at): - return ConversionFee(id=id, percentage_from_source=percentage_from_source, created_by=created_by, - created_at=created_at, updated_at=updated_at) + def conversion_fee(id, percentage_from_source, token_obj, created_by, created_at, updated_at): + return ConversionFee(id=id, percentage_from_source=percentage_from_source, token_obj=token_obj, + created_by=created_by, created_at=created_at, updated_at=updated_at) @staticmethod - def convert_conversion_fee_db_object_to_object(conversion_fee): + def convert_conversion_fee_db_object_to_object(conversion_fee, token_obj): if conversion_fee is None: return None return ConversionFeeFactory.conversion_fee(id=conversion_fee.id, percentage_from_source=conversion_fee.percentage_from_source, + token_obj=token_obj, created_by=conversion_fee.created_by, created_at=conversion_fee.created_at, updated_at=conversion_fee.updated_at diff --git a/domain/factory/PoolFactory.py b/domain/factory/pool_factory.py similarity index 100% rename from domain/factory/PoolFactory.py rename to domain/factory/pool_factory.py diff --git a/domain/factory/token_factory.py b/domain/factory/token_factory.py index e5dbb99..c9d2173 100644 --- a/domain/factory/token_factory.py +++ b/domain/factory/token_factory.py @@ -1,14 +1,24 @@ from domain.entities.token import Token from domain.entities.token_pair import TokenPair +from domain.entities.trading_view import TradingView from domain.factory.blockchain_factory import BlockchainFactory from domain.factory.conversion_fee_factory import ConversionFeeFactory class TokenFactory: + @staticmethod + def trading_view(row_id, id_, symbol, alt_text, created_at, updated_at): + return TradingView(row_id=row_id, + id_=id_, + symbol=symbol, + alt_text=alt_text, + created_at=created_at, + updated_at=updated_at) + @staticmethod def token(row_id, id_, name, description, symbol, logo, allowed_decimal, token_address, contract_address, - created_by, created_at, updated_at, blockchain_detail): + created_by, created_at, updated_at, blockchain_detail, trading_view): blockchain_obj = BlockchainFactory.blockchain(id=blockchain_detail.id, name=blockchain_detail.name, description=blockchain_detail.description, symbol=blockchain_detail.symbol, @@ -19,18 +29,25 @@ def token(row_id, id_, name, description, symbol, logo, allowed_decimal, token_a created_by=blockchain_detail.created_by, created_at=blockchain_detail.created_at, updated_at=blockchain_detail.updated_at) + trading_view_obj = TokenFactory.trading_view(row_id=trading_view.row_id, + id_=trading_view.id, + symbol=trading_view.symbol, + alt_text=trading_view.alt_text, + created_at=trading_view.created_at, + updated_at=trading_view.updated_at) return Token(row_id=row_id, id_=id_, name=name, description=description, symbol=symbol, logo=logo, allowed_decimal=allowed_decimal, token_address=token_address, contract_address=contract_address, - created_by=created_by, created_at=created_at, updated_at=updated_at, blockchain_obj=blockchain_obj) + created_by=created_by, created_at=created_at, updated_at=updated_at, + blockchain_obj=blockchain_obj, trading_view_obj=trading_view_obj) @staticmethod def token_pair_detail(row_id, id_, min_value, max_value, created_by, created_at, updated_at, from_token_obj, - to_token_obj, conversion_fee_obj, conversion_ratio, is_liquid): + to_token_obj, conversion_fee_obj, conversion_ratio, is_liquid, ada_threshold): return TokenPair(row_id=row_id, id_=id_, min_value=min_value, max_value=max_value, created_by=created_by, created_at=created_at, updated_at=updated_at, from_token_obj=from_token_obj, to_token_obj=to_token_obj, conversion_fee_obj=conversion_fee_obj, - conversion_ratio=conversion_ratio, is_liquid=is_liquid) + conversion_ratio=conversion_ratio, is_liquid=is_liquid, ada_threshold=ada_threshold) @staticmethod def convert_token_db_object_to_object(token): @@ -38,17 +55,19 @@ def convert_token_db_object_to_object(token): symbol=token.symbol, logo=token.logo, allowed_decimal=token.allowed_decimal, token_address=token.token_address, contract_address=token.contract_address, created_by=token.created_by, created_at=token.created_at, updated_at=token.updated_at, - blockchain_detail=token.blockchain_detail) + blockchain_detail=token.blockchain_detail, trading_view=token.trading_view) @staticmethod def token_pair(row_id, id_, min_value, max_value, created_by, created_at, updated_at, from_token, - to_token, conversion_fee, conversion_ratio, is_liquid): + to_token, conversion_fee, conversion_ratio, is_liquid, ada_threshold): from_token_obj = TokenFactory.convert_token_db_object_to_object(from_token) to_token_obj = TokenFactory.convert_token_db_object_to_object(to_token) - conversion_fee_obj = ConversionFeeFactory.convert_conversion_fee_db_object_to_object(conversion_fee) + token_obj = (TokenFactory.convert_token_db_object_to_object(conversion_fee.token) + if conversion_fee and conversion_fee.token else None) + conversion_fee_obj = ConversionFeeFactory.convert_conversion_fee_db_object_to_object(conversion_fee, token_obj) return TokenFactory.token_pair_detail(row_id=row_id, id_=id_, min_value=min_value, max_value=max_value, created_by=created_by, created_at=created_at, updated_at=updated_at, from_token_obj=from_token_obj, to_token_obj=to_token_obj, conversion_fee_obj=conversion_fee_obj, conversion_ratio=conversion_ratio, - is_liquid=is_liquid) + is_liquid=is_liquid, ada_threshold=ada_threshold) diff --git a/infrastructure/models.py b/infrastructure/models.py index b674010..c511603 100644 --- a/infrastructure/models.py +++ b/infrastructure/models.py @@ -26,6 +26,19 @@ class BlockChainDBModel(Base): __table_args__ = (UniqueConstraint(name, symbol), {}) +class TradingViewDBModel(Base): + __tablename__ = "trading_view" + row_id = Column("row_id", BIGINT, primary_key=True, autoincrement=True) + id = Column("id", VARCHAR(50), unique=True, nullable=False) + symbol = Column("symbol", VARCHAR(250), nullable=True) + alt_text = Column("alt_text", TEXT, nullable=True) + created_at = Column("created_at", TIMESTAMP, + server_default=func.current_timestamp(), nullable=False) + updated_at = Column("updated_at", TIMESTAMP, + server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"), + nullable=False) + + class TokenDBModel(Base): __tablename__ = "token" row_id = Column("row_id", BIGINT, primary_key=True, autoincrement=True) @@ -35,6 +48,7 @@ class TokenDBModel(Base): symbol = Column("symbol", VARCHAR(30), nullable=False) logo = Column("logo", VARCHAR(250)) blockchain_id = Column("blockchain_id", BIGINT, ForeignKey(BlockChainDBModel.row_id), nullable=False) + trading_view_id = Column("trading_view_id", BIGINT, ForeignKey(TradingViewDBModel.row_id)) allowed_decimal = Column("allowed_decimal", INTEGER) token_address = Column("token_address", VARCHAR(100), nullable=False) contract_address = Column("contract_address", VARCHAR(250), nullable=True) @@ -45,6 +59,7 @@ class TokenDBModel(Base): server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"), nullable=False) blockchain_detail = relationship(BlockChainDBModel, uselist=False, lazy="joined") + trading_view = relationship(TradingViewDBModel, foreign_keys=[trading_view_id], uselist=False, lazy="joined") __table_args__ = (UniqueConstraint(name, symbol, blockchain_id), {}) @@ -74,6 +89,7 @@ class TokenPairDBModel(Base): conversion_ratio = Column("conversion_ratio", DECIMAL(32, 18)) is_enabled = Column("is_enabled", BOOLEAN, default=True) is_liquid = Column("is_liquid", BOOLEAN, nullable=False, server_default="0") + ada_threshold = Column("ada_threshold", BIGINT, nullable=True) min_value = Column("min_value", DECIMAL(64, 0)) max_value = Column("max_value", DECIMAL(64, 0)) created_by = Column("created_by", VARCHAR(50), nullable=False) diff --git a/infrastructure/repositories/conversion_repository.py b/infrastructure/repositories/conversion_repository.py index 9137abc..9e951ab 100644 --- a/infrastructure/repositories/conversion_repository.py +++ b/infrastructure/repositories/conversion_repository.py @@ -1,8 +1,9 @@ from sqlalchemy import or_, case, func, and_ from sqlalchemy.orm import joinedload, aliased -from constants.general import CreatedBy, BlockchainName, ConversionOn +from constants.general import CreatedBy, BlockchainName, ConversionOn, ConversionHistoryOrder from constants.status import ConversionStatus, ConversionTransactionStatus +from constants.lambdas import PaginationDefaults from domain.factory.conversion_factory import ConversionFactory from infrastructure.models import ConversionDBModel, WalletPairDBModel, TokenPairDBModel, TokenDBModel, \ ConversionTransactionDBModel, TransactionDBModel, BlockChainDBModel @@ -250,58 +251,108 @@ def get_token_contract_address_for_conversion_id(self, conversion_on, conversion return contract_address[0] @read_from_db() - def get_conversion_history_count(self, address): - count = self.session.query(func.count(ConversionDBModel.id)) \ - .join(WalletPairDBModel, WalletPairDBModel.row_id == ConversionDBModel.wallet_pair_id) \ - .filter( - or_(WalletPairDBModel.from_address == address, WalletPairDBModel.to_address == address)) \ - .first() + def get_conversion_history_count(self, address, blockchain_name, token_symbol, conversion_status): + + from_token = aliased(TokenDBModel) + to_token = aliased(TokenDBModel) + from_blockchain = aliased(BlockChainDBModel) + to_blockchain = aliased(BlockChainDBModel) + + query = self.session.query(func.count(ConversionDBModel.id)) \ + .join(ConversionDBModel.wallet_pair) \ + .join(WalletPairDBModel.token_pair) \ + .join(from_token, TokenPairDBModel.from_token) \ + .join(to_token, TokenPairDBModel.to_token) \ + .join(from_blockchain, from_token.blockchain_detail) \ + .join(to_blockchain, to_token.blockchain_detail) + + # Filtering + if address: + query = query.filter( + or_(WalletPairDBModel.from_address == address, + WalletPairDBModel.to_address == address) + ) + if blockchain_name: + query = query.filter( + or_(from_blockchain.name == blockchain_name, + to_blockchain.name == blockchain_name) + ) + if token_symbol: + query = query.filter( + or_(from_token.symbol == token_symbol, + to_token.symbol == token_symbol) + ) + if conversion_status: + query = query.filter(ConversionDBModel.status == conversion_status) + + count = query.first() return count[0] @read_from_db() - def get_conversion_history(self, address, conversion_id, offset=0, limit=15): + def get_conversion_history(self, address, blockchain_name, token_symbol, conversion_status, + order=ConversionHistoryOrder.DEFAULT, + offset=0, limit=PaginationDefaults.PAGE_SIZE.value): - conversions_detail_query = self.session.query(ConversionDBModel) \ - .join(WalletPairDBModel, WalletPairDBModel.row_id == ConversionDBModel.wallet_pair_id) \ - .join(TokenPairDBModel, TokenPairDBModel.row_id == WalletPairDBModel.token_pair_id) \ - .order_by(case( - [ - ( - ConversionDBModel.status == ConversionStatus.WAITING_FOR_CLAIM.value, 1 - ), - ( - ConversionDBModel.status == ConversionStatus.USER_INITIATED.value, 2 - ), - ( - ConversionDBModel.status == ConversionStatus.CLAIM_INITIATED.value, 3 - ), - ( - ConversionDBModel.status == ConversionStatus.PROCESSING.value, 4 - ), - ( - ConversionDBModel.status == ConversionStatus.SUCCESS.value, 5 - ), - ( - ConversionDBModel.status == ConversionStatus.EXPIRED.value, 6 - ) - ], - else_=7 - ).asc(), ConversionDBModel.created_at.desc()) - - if address: - conversions_detail_query = conversions_detail_query.filter( - or_(WalletPairDBModel.from_address == address, WalletPairDBModel.to_address == address)) + from_token = aliased(TokenDBModel) + to_token = aliased(TokenDBModel) + from_blockchain = aliased(BlockChainDBModel) + to_blockchain = aliased(BlockChainDBModel) - if conversion_id: - conversions_detail_query = conversions_detail_query.filter(ConversionDBModel.id == conversion_id) + query = self.session.query(ConversionDBModel) \ + .join(ConversionDBModel.wallet_pair) \ + .join(WalletPairDBModel.token_pair) \ + .join(from_token, TokenPairDBModel.from_token) \ + .join(to_token, TokenPairDBModel.to_token) \ + .join(from_blockchain, from_token.blockchain_detail) \ + .join(to_blockchain, to_token.blockchain_detail) - conversions_detail = conversions_detail_query.options(joinedload(ConversionDBModel.wallet_pair)).options( - joinedload(ConversionDBModel.wallet_pair).joinedload(WalletPairDBModel.token_pair)).offset(offset).limit( - limit).all() + # Filtering + if address: + query = query.filter( + or_(WalletPairDBModel.from_address == address, + WalletPairDBModel.to_address == address) + ) + if blockchain_name: + query = query.filter( + or_(from_blockchain.name == blockchain_name, + to_blockchain.name == blockchain_name) + ) + if token_symbol: + query = query.filter( + or_(from_token.symbol == token_symbol, + to_token.symbol == token_symbol) + ) + if conversion_status: + query = query.filter(ConversionDBModel.status == conversion_status) + + # Ordering + if order == ConversionHistoryOrder.STATUS: + query = query.order_by( + case( + [ + (ConversionDBModel.status == ConversionStatus.WAITING_FOR_CLAIM.value, 1), + (ConversionDBModel.status == ConversionStatus.USER_INITIATED.value, 2), + (ConversionDBModel.status == ConversionStatus.CLAIM_INITIATED.value, 3), + (ConversionDBModel.status == ConversionStatus.PROCESSING.value, 4), + (ConversionDBModel.status == ConversionStatus.SUCCESS.value, 5), + (ConversionDBModel.status == ConversionStatus.EXPIRED.value, 6), + (ConversionDBModel.status == ConversionStatus.CANCELED.value, 7) + ], + else_=8 + ).asc(), + ConversionDBModel.created_at.desc() + ) + elif order == ConversionHistoryOrder.DATE: + query = query.order_by(ConversionDBModel.created_at.desc()) + + conversions_details = query.options(joinedload(ConversionDBModel.wallet_pair)) \ + .options(joinedload(ConversionDBModel.wallet_pair).joinedload(WalletPairDBModel.token_pair)) \ + .offset(offset) \ + .limit(limit) \ + .all() - return [ConversionFactory.conversion_detail(conversion=conversion_detail) for conversion_detail in - conversions_detail] + return [ConversionFactory.conversion_detail(conversion_detail) for conversion_detail in conversions_details] @read_from_db() def get_transactions_for_conversion_row_ids(self, conversion_row_ids): diff --git a/infrastructure/repositories/pooling_repository.py b/infrastructure/repositories/pooling_repository.py index 28333ce..cdb4e57 100644 --- a/infrastructure/repositories/pooling_repository.py +++ b/infrastructure/repositories/pooling_repository.py @@ -1,6 +1,6 @@ from datetime import datetime -from domain.factory.PoolFactory import PoolFactory +from domain.factory.pool_factory import PoolFactory from infrastructure.models import MessageGroupPoolDBModel from infrastructure.repositories.base_repository import BaseRepository from utils.database import read_from_db, update_in_db diff --git a/infrastructure/repositories/token_repository.py b/infrastructure/repositories/token_repository.py index 141d85c..52b213d 100644 --- a/infrastructure/repositories/token_repository.py +++ b/infrastructure/repositories/token_repository.py @@ -2,7 +2,7 @@ from constants.error_details import ErrorCode, ErrorDetails from domain.factory.token_factory import TokenFactory -from infrastructure.models import TokenPairDBModel +from infrastructure.models import ConversionFeeDBModel, TokenPairDBModel from infrastructure.repositories.base_repository import BaseRepository from utils.database import read_from_db from utils.exceptions import TokenPairIdNotExitsException @@ -14,14 +14,15 @@ class TokenRepository(BaseRepository): def get_all_token_pair(self): token_pairs = self.session.query(TokenPairDBModel).filter(TokenPairDBModel.is_enabled.is_(True)) \ .options(joinedload(TokenPairDBModel.from_token)).options(joinedload(TokenPairDBModel.to_token)) \ - .options(joinedload(TokenPairDBModel.conversion_fee)).all() + .options(joinedload(TokenPairDBModel.conversion_fee).joinedload(ConversionFeeDBModel.token)).all() return [TokenFactory.token_pair(row_id=token_pair.row_id, id_=token_pair.id, min_value=token_pair.min_value, max_value=token_pair.max_value, created_by=token_pair.created_by, created_at=token_pair.created_at, updated_at=token_pair.updated_at, from_token=token_pair.from_token, to_token=token_pair.to_token, conversion_fee=token_pair.conversion_fee, conversion_ratio=token_pair.conversion_ratio, - is_liquid=token_pair.is_liquid) for + is_liquid=token_pair.is_liquid, + ada_threshold=token_pair.ada_threshold) for token_pair in token_pairs] @@ -48,4 +49,5 @@ def get_token_pair(self, token_pair_id, token_pair_row_id=None): from_token=token_pair.from_token, to_token=token_pair.to_token, conversion_fee=token_pair.conversion_fee, conversion_ratio=token_pair.conversion_ratio, - is_liquid=token_pair.is_liquid) + is_liquid=token_pair.is_liquid, + ada_threshold=token_pair.ada_threshold) diff --git a/sonar-project.properties b/sonar-project.properties index b27bae9..a86898c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,8 +1,6 @@ -sonar.projectKey=converter-services -sonar.projectName=converter-services +sonar.projectKey=singnet_snet-converter-services_3e19ab1a-6965-4921-9bfc-1eb9c704ec98 +sonar.projectName=snet-converter-services +sonar.projectVersion=0.0.1 sonar.sources=. -sonar.tests=. -sonar.test.inclusions=**/testcases/*_testcases/test_*.py -sonar.coverage.exclusions=**/config.py,**/constant.py,**/error.py,**/errors.py,**/testcases/** -sonar.exclusions=*.sh,*.md,.circleci/config.yml,LICENSE,**/requirements.txt,node_modules/**,**/serverless.yml,**/package.json,**/alembic.ini,**/alembic/** -sonar.python.coverage.reportPaths=coverage.xml \ No newline at end of file + + diff --git a/utils/blockchain.py b/utils/blockchain.py index 1ac291d..b57b089 100644 --- a/utils/blockchain.py +++ b/utils/blockchain.py @@ -4,9 +4,12 @@ from http import HTTPStatus import re +from web3.logs import DISCARD from web3.exceptions import TransactionNotFound, ABIFunctionNotFound + from pycardano import Address from pycardano.exception import DecodingException +from blockfrost.utils import ApiError as BlockfrostApiError from application.service.cardano_service import CardanoService from common.blockchain_util import BlockChainUtil @@ -125,9 +128,9 @@ def get_evm_transaction_details(web3_object, transaction_hash): def get_event_logs(contract_instance, receipt, conversion_on): if conversion_on == ConversionOn.FROM.value: - logs = contract_instance.events.ConversionOut().processReceipt(receipt) + logs = contract_instance.events.ConversionOut().processReceipt(receipt, errors=DISCARD) else: - logs = contract_instance.events.ConversionIn().processReceipt(receipt) + logs = contract_instance.events.ConversionIn().processReceipt(receipt, errors=DISCARD) return logs @@ -379,6 +382,36 @@ def validate_tx_hash_presence_in_blockchain(blockchain_name, tx_hash, network_id raise InternalServerErrorException(error_code=ErrorCode.UNEXPECTED_ERROR_ON_TX_HASH_PRESENCE) +def validate_tx_token_received_ada_amount(target_amount, address, tx_hash, network_id): + """ + Validates that amount of ADA received on the given address in given transaction (tx_hash) not less then + target_amount + Only for Cardano transactions, mostly for TOKEN_RECEIVED operation transactions + """ + logger.info(f"Validating that amount of ADA received on the address {address} in transaction {tx_hash} not less" + f"then {target_amount}") + try: + url, project_id = get_cardano_network_url_and_project_id(chain_id=network_id) + cardano_blockchain = CardanoBlockchainUtil(project_id=project_id, base_url=url) + transaction_utxos = cardano_blockchain.get_transaction_utxos(tx_hash) + transaction_amount = 0 + for output in transaction_utxos.outputs: + if output.address == address: + for amount in output.amount: + if amount.unit == 'lovelace': + transaction_amount += int(amount.quantity) + logger.info(f"Amount of ADA received on address {address} = {transaction_amount}") + if transaction_amount < target_amount: + raise BadRequestException(ErrorCode.INVALID_TX_ADA_AMOUNT) + except BlockfrostApiError: + raise BadRequestException(error_code=ErrorCode.TRANSACTION_HASH_NOT_FOUND) + except BadRequestException as e: + raise e + except Exception: + logger.exception(f"Unexpected error on transaction ADA amount validation", exc_info=True) + raise InternalServerErrorException(error_code=ErrorCode.UNEXPECTED_ERROR_ON_ADA_AMOUNT_VALIDATION) + + def validate_consumer_event_type(blockchain_name, event_type): logger.info(f"Validating the consumer event type for blockchain_name={blockchain_name}, event_type={event_type}") if blockchain_name.lower() == BlockchainName.ETHEREUM.value.lower() and event_type not in EthereumAllowedEventType: