From 725a74314d9f9bbffe25e7da8cfbb75360d8474b Mon Sep 17 00:00:00 2001 From: "marek.galvanek" Date: Wed, 8 Apr 2026 16:26:38 +0200 Subject: [PATCH] feat: add Bitaps and LitecoinSpace APIs for Litecoin balance fetching and parsing, with comprehensive test coverage --- .../api/data/bitaps_ltc_balance_response.json | 26 +++++++++ .../data/litecoinspace_balance_response.json | 17 ++++++ blockapi/test/v2/api/test_bitaps_ltc.py | 39 +++++++++++++ blockapi/test/v2/api/test_litecoinspace.py | 46 ++++++++++++++++ blockapi/v2/api/__init__.py | 2 + blockapi/v2/api/bitaps.py | 53 ++++++++++++++++++ blockapi/v2/api/litecoinspace.py | 55 +++++++++++++++++++ 7 files changed, 238 insertions(+) create mode 100644 blockapi/test/v2/api/data/bitaps_ltc_balance_response.json create mode 100644 blockapi/test/v2/api/data/litecoinspace_balance_response.json create mode 100644 blockapi/test/v2/api/test_bitaps_ltc.py create mode 100644 blockapi/test/v2/api/test_litecoinspace.py create mode 100644 blockapi/v2/api/bitaps.py create mode 100644 blockapi/v2/api/litecoinspace.py diff --git a/blockapi/test/v2/api/data/bitaps_ltc_balance_response.json b/blockapi/test/v2/api/data/bitaps_ltc_balance_response.json new file mode 100644 index 0000000..4a42134 --- /dev/null +++ b/blockapi/test/v2/api/data/bitaps_ltc_balance_response.json @@ -0,0 +1,26 @@ +{ + "data": { + "balance": 75763, + "receivedAmount": 1137420376400653, + "receivedTxCount": 629, + "sentAmount": 1137420376324890, + "sentTxCount": 20, + "firstReceivedTxPointer": "1512464:19", + "firstSentTxPointer": "1512466:8", + "lastTxPointer": "3038651:188", + "largestSpentTxAmount": 150393732845214, + "largestSpentTxPointer": "2784397:708", + "largestReceivedTxAmount": 65763, + "largestReceivedTxPointer": "3038651:188", + "receivedOutsCount": 629, + "spentOutsCount": 627, + "pendingReceivedAmount": 0, + "pendingSentAmount": 0, + "pendingReceivedTxCount": 0, + "pendingSentTxCount": 0, + "pendingReceivedOutsCount": 0, + "pendingSpentOutsCount": 0, + "type": "P2SH" + }, + "time": 0.0047 +} \ No newline at end of file diff --git a/blockapi/test/v2/api/data/litecoinspace_balance_response.json b/blockapi/test/v2/api/data/litecoinspace_balance_response.json new file mode 100644 index 0000000..f422672 --- /dev/null +++ b/blockapi/test/v2/api/data/litecoinspace_balance_response.json @@ -0,0 +1,17 @@ +{ + "address": "M8T1B2Z97gVdvmfkQcAtYbEepune1tzGua", + "chain_stats": { + "funded_txo_count": 629, + "funded_txo_sum": 1137420376400653, + "spent_txo_count": 627, + "spent_txo_sum": 1137420376324890, + "tx_count": 649 + }, + "mempool_stats": { + "funded_txo_count": 0, + "funded_txo_sum": 0, + "spent_txo_count": 0, + "spent_txo_sum": 0, + "tx_count": 0 + } +} \ No newline at end of file diff --git a/blockapi/test/v2/api/test_bitaps_ltc.py b/blockapi/test/v2/api/test_bitaps_ltc.py new file mode 100644 index 0000000..57c6ca4 --- /dev/null +++ b/blockapi/test/v2/api/test_bitaps_ltc.py @@ -0,0 +1,39 @@ +from decimal import Decimal + +import pytest + +from blockapi.test.v2.api.conftest import read_file +from blockapi.test.v2.test_data import ltc_test_address +from blockapi.v2.api.bitaps import BitapsLitecoinApi +from blockapi.v2.models import FetchResult + + +def test_fetch_balances(requests_mock, bitaps_ltc_balance_response): + requests_mock.get( + f'https://api.bitaps.com/ltc/v1/blockchain/address/state/{ltc_test_address}', + text=bitaps_ltc_balance_response, + ) + + api = BitapsLitecoinApi() + balances = api.get_balance(ltc_test_address) + assert len(balances) == 1 + assert balances[0].balance == Decimal('0.00075763') + + +def test_parse_zero_balance(): + api = BitapsLitecoinApi() + fetch_result = FetchResult(data={'data': {'balance': 0}}) + result = api.parse_balances(fetch_result) + assert result.data is None + + +def test_parse_empty_response(): + api = BitapsLitecoinApi() + fetch_result = FetchResult(data=None) + result = api.parse_balances(fetch_result) + assert result.data is None + + +@pytest.fixture +def bitaps_ltc_balance_response(): + return read_file('data/bitaps_ltc_balance_response.json') diff --git a/blockapi/test/v2/api/test_litecoinspace.py b/blockapi/test/v2/api/test_litecoinspace.py new file mode 100644 index 0000000..74d5bdd --- /dev/null +++ b/blockapi/test/v2/api/test_litecoinspace.py @@ -0,0 +1,46 @@ +from decimal import Decimal + +import pytest + +from blockapi.test.v2.api.conftest import read_file +from blockapi.test.v2.test_data import ltc_test_address +from blockapi.v2.api.litecoinspace import LitecoinSpaceApi +from blockapi.v2.models import FetchResult + + +def test_fetch_balances(requests_mock, litecoinspace_balance_response): + requests_mock.get( + f'https://litecoinspace.org/api/address/{ltc_test_address}', + text=litecoinspace_balance_response, + ) + + api = LitecoinSpaceApi() + balances = api.get_balance(ltc_test_address) + assert len(balances) == 1 + assert balances[0].balance == Decimal('0.00075763') + + +def test_parse_zero_balance(): + api = LitecoinSpaceApi() + fetch_result = FetchResult( + data={ + 'chain_stats': { + 'funded_txo_sum': 100, + 'spent_txo_sum': 100, + } + } + ) + result = api.parse_balances(fetch_result) + assert result.data is None + + +def test_parse_empty_response(): + api = LitecoinSpaceApi() + fetch_result = FetchResult(data=None) + result = api.parse_balances(fetch_result) + assert result.data is None + + +@pytest.fixture +def litecoinspace_balance_response(): + return read_file('data/litecoinspace_balance_response.json') diff --git a/blockapi/v2/api/__init__.py b/blockapi/v2/api/__init__.py index f182d24..7147f27 100644 --- a/blockapi/v2/api/__init__.py +++ b/blockapi/v2/api/__init__.py @@ -1,3 +1,4 @@ +from blockapi.v2.api.bitaps import BitapsLitecoinApi from blockapi.v2.api.blockchain_info import BlockchainInfoApi from blockapi.v2.api.blockchainos import BlockchainosApi from blockapi.v2.api.blockchair import ( @@ -9,6 +10,7 @@ from blockapi.v2.api.debank import DebankApi, DebankApp, DebankPrediction from blockapi.v2.api.ethplorer import EthplorerApi from blockapi.v2.api.haskoin import HaskoinApi +from blockapi.v2.api.litecoinspace import LitecoinSpaceApi from blockapi.v2.api.optimistic_etherscan import OptimismEtherscanApi from blockapi.v2.api.perpetual import PerpetualApi, perp_contract_address from blockapi.v2.api.solana import SolanaApi, SolscanApi diff --git a/blockapi/v2/api/bitaps.py b/blockapi/v2/api/bitaps.py new file mode 100644 index 0000000..2a46ad2 --- /dev/null +++ b/blockapi/v2/api/bitaps.py @@ -0,0 +1,53 @@ +from blockapi.v2.base import BalanceMixin, BlockchainApi +from blockapi.v2.coins import COIN_LTC +from blockapi.v2.models import ( + ApiOptions, + AssetType, + BalanceItem, + Blockchain, + FetchResult, + ParseResult, +) + + +class BitapsLitecoinApi(BlockchainApi, BalanceMixin): + """ + Coin: Litecoin + API docs: https://developer.bitaps.com/ + Explorer: https://ltc.bitaps.com + """ + + coin = COIN_LTC + api_options = ApiOptions( + blockchain=Blockchain.LITECOIN, + base_url='https://api.bitaps.com/ltc/v1/', + rate_limit=0.1, + ) + + supported_requests = { + 'get_balance': 'blockchain/address/state/{address}', + } + + def fetch_balances(self, address: str) -> FetchResult: + return self.get_data('get_balance', address=address) + + def parse_balances(self, fetch_result: FetchResult) -> ParseResult: + if not fetch_result.data: + return ParseResult() + + data = fetch_result.data.get('data', {}) + balance_raw = data.get('balance') + + if not balance_raw: + return ParseResult() + + return ParseResult( + data=[ + BalanceItem.from_api( + balance_raw=balance_raw, + coin=self.coin, + asset_type=AssetType.AVAILABLE, + raw=fetch_result.data, + ) + ] + ) diff --git a/blockapi/v2/api/litecoinspace.py b/blockapi/v2/api/litecoinspace.py new file mode 100644 index 0000000..7da946f --- /dev/null +++ b/blockapi/v2/api/litecoinspace.py @@ -0,0 +1,55 @@ +from blockapi.v2.base import BalanceMixin, BlockchainApi +from blockapi.v2.coins import COIN_LTC +from blockapi.v2.models import ( + ApiOptions, + AssetType, + BalanceItem, + Blockchain, + FetchResult, + ParseResult, +) + + +class LitecoinSpaceApi(BlockchainApi, BalanceMixin): + """ + Coin: Litecoin + API docs: https://litecoinspace.org/docs/api/rest + Explorer: https://litecoinspace.org + """ + + coin = COIN_LTC + api_options = ApiOptions( + blockchain=Blockchain.LITECOIN, + base_url='https://litecoinspace.org', + rate_limit=0.2, + ) + + supported_requests = { + 'get_balance': '/api/address/{address}', + } + + def fetch_balances(self, address: str) -> FetchResult: + return self.get_data('get_balance', address=address) + + def parse_balances(self, fetch_result: FetchResult) -> ParseResult: + if not fetch_result.data: + return ParseResult() + + chain_stats = fetch_result.data.get('chain_stats', {}) + funded = chain_stats.get('funded_txo_sum', 0) + spent = chain_stats.get('spent_txo_sum', 0) + balance_raw = funded - spent + + if not balance_raw: + return ParseResult() + + return ParseResult( + data=[ + BalanceItem.from_api( + balance_raw=balance_raw, + coin=self.coin, + asset_type=AssetType.AVAILABLE, + raw=fetch_result.data, + ) + ] + )