From 5fe43b0a3fe05c48e09cc074ee3b2ea7c88df3fc Mon Sep 17 00:00:00 2001 From: tkernell Date: Tue, 23 Jan 2024 19:14:01 -0600 Subject: [PATCH 1/6] btc historic balance logic --- src/telliot_feeds/queries/btc_balance.py | 30 ++++++++ src/telliot_feeds/sources/btc_balance.py | 92 ++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/telliot_feeds/queries/btc_balance.py create mode 100644 src/telliot_feeds/sources/btc_balance.py diff --git a/src/telliot_feeds/queries/btc_balance.py b/src/telliot_feeds/queries/btc_balance.py new file mode 100644 index 00000000..de22329f --- /dev/null +++ b/src/telliot_feeds/queries/btc_balance.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass + +from telliot_feeds.queries.abi_query import AbiQuery +from telliot_feeds.dtypes.value_type import ValueType + +@dataclass +class BTCBalance(AbiQuery): + """Returns the BTC balance of a given address at a specific timestamp. + + Attributes: + btcAddress: the address of the bitcoin hodler + timestamp: timestamp which will be rounded down to the closest Bitcoin block + """ + + btcAddress: str + timestamp: int + + #: ABI used for encoding/decoding parameters + abi = [{"name": "btcAddress", "type": "string"}, {"name": "timestamp", "type": "uint256"}] + + @property + def value_type(self) -> ValueType: + """Data type returned for a BTCBalance query. + + - 'uint256': balance in satoshis + - 'packed': false + """ + + return ValueType(abi_type="uint256", packed=False) + diff --git a/src/telliot_feeds/sources/btc_balance.py b/src/telliot_feeds/sources/btc_balance.py new file mode 100644 index 00000000..2d52308f --- /dev/null +++ b/src/telliot_feeds/sources/btc_balance.py @@ -0,0 +1,92 @@ +from dataclasses import dataclass +from typing import Any, Optional +import asyncio + +import requests +from requests import JSONDecodeError +from requests.adapters import HTTPAdapter + +from telliot_feeds.datasource import DataSource +from telliot_feeds.dtypes.datapoint import OptionalDataPoint +from telliot_feeds.utils.log import get_logger +from telliot_feeds.dtypes.datapoint import datetime_now_utc + +logger = get_logger(__name__) + +@dataclass +class BTCBalanceSource(DataSource[Any]): + """DataSource for returning the balance of a BTC address at a given timestamp.""" + + address: Optional[str] = None + timestamp: Optional[int] = None + + async def get_response(self) -> Optional[Any]: + """gets balance of address from https://blockchain.info/multiaddr?active=$address|$address""" + if not self.address: + raise ValueError("BTC address not provided") + if not self.timestamp: + raise ValueError("Timestamp not provided") + + with requests.Session() as s: + url = f"https://blockchain.info/multiaddr?active={self.address}|{self.address}" + try: + rsp = s.get(url) + except requests.exceptions.ConnectTimeout: + logger.error("Connection timeout getting BTC balance") + return None + except requests.exceptions.RequestException as e: + logger.error(f"Blockchain.info API error: {e}") + return None + + try: + data = rsp.json() + except JSONDecodeError: + logger.error("Blockchain.info API returned invalid JSON") + return None + + if 'txs' not in data: + logger.warning("Blockchain.info response doesn't contain needed data") + return None + + if 'addresses' not in data: + logger.warning("Blockchain.info response doesn't contain needed data") + return None + + if int(data['addresses'][0]['n_tx']) == 0: + # No transactions for this address + return 0 + + # Sort transactions by time in ascending order + sorted_txs = sorted(data['txs'], key=lambda tx: tx['time']) + + # Find the most recent transaction before the query's timestamp + last_tx = None + for tx in sorted_txs: + if tx['time'] > self.timestamp: + break + last_tx = tx + if last_tx is None: + # No transactions before the query's timestamp + return 0 + + # Use the balance from the last transaction as the BTC balance + btc_balance = last_tx['balance'] + + return btc_balance + + async def fetch_new_datapoint(self) -> OptionalDataPoint[Any]: + """Fetches the balance of a BTC address.""" + datapoint = (self.get_response(), datetime_now_utc()) + self.store_datapoint(datapoint) + return self.get_response() + + +if __name__ == "__main__": + btc_balance_source = BTCBalanceSource(address='', timestamp=1706033856) + + async def main(): + bal = await btc_balance_source.get_response() + print("query timestamp: ", btc_balance_source.timestamp) + print("BTC Balance: ", bal) + + asyncio.run(main()) From 16d5be06d612c2b0febe3f66e17f7d9f71ae420b Mon Sep 17 00:00:00 2001 From: tkernell Date: Wed, 24 Jan 2024 16:36:38 -0600 Subject: [PATCH 2/6] use block timestamps, get height, sort txs by height --- src/telliot_feeds/feeds/btc_balance.py | 19 +++++ src/telliot_feeds/queries/btc_balance.py | 7 +- src/telliot_feeds/queries/query_catalog.py | 7 ++ src/telliot_feeds/sources/btc_balance.py | 86 +++++++++++++++++++--- tests/queries/test_query_btc_balance.py | 33 +++++++++ tests/sources/test_btc_balance_source.py | 30 ++++++++ 6 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 src/telliot_feeds/feeds/btc_balance.py create mode 100644 tests/queries/test_query_btc_balance.py create mode 100644 tests/sources/test_btc_balance_source.py diff --git a/src/telliot_feeds/feeds/btc_balance.py b/src/telliot_feeds/feeds/btc_balance.py new file mode 100644 index 00000000..5d3efb86 --- /dev/null +++ b/src/telliot_feeds/feeds/btc_balance.py @@ -0,0 +1,19 @@ +"""Datafeed for BTC balance of an address at a specific timestamp.""" +from typing import Optional + +from telliot_feeds.datafeed import DataFeed +from telliot_feeds.queries.btc_balance import BTCBalance +from telliot_feeds.sources.btc_balance import BTCBalanceSource + +bitcoin_balance_feed = DataFeed( + source=BTCBalanceSource(), + query=BTCBalance(), +) + + +btcAddress='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03' +timestamp=1706051389 +btc_balance_feed_example = DataFeed( + source=BTCBalanceSource(address=btcAddress, timestamp=timestamp), + query=BTCBalance(btcAddress==btcAddress, timestamp=timestamp), +) \ No newline at end of file diff --git a/src/telliot_feeds/queries/btc_balance.py b/src/telliot_feeds/queries/btc_balance.py index de22329f..39c716ad 100644 --- a/src/telliot_feeds/queries/btc_balance.py +++ b/src/telliot_feeds/queries/btc_balance.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional from telliot_feeds.queries.abi_query import AbiQuery from telliot_feeds.dtypes.value_type import ValueType @@ -7,13 +8,15 @@ class BTCBalance(AbiQuery): """Returns the BTC balance of a given address at a specific timestamp. + More info: https://github.com/tellor-io/dataSpecs/blob/main/types/BTCBalance.md + Attributes: btcAddress: the address of the bitcoin hodler timestamp: timestamp which will be rounded down to the closest Bitcoin block """ - btcAddress: str - timestamp: int + btcAddress: Optional[str] = None + timestamp: Optional[int] = None #: ABI used for encoding/decoding parameters abi = [{"name": "btcAddress", "type": "string"}, {"name": "timestamp", "type": "uint256"}] diff --git a/src/telliot_feeds/queries/query_catalog.py b/src/telliot_feeds/queries/query_catalog.py index 902acd05..f7dfac6b 100644 --- a/src/telliot_feeds/queries/query_catalog.py +++ b/src/telliot_feeds/queries/query_catalog.py @@ -5,6 +5,7 @@ from telliot_feeds.integrations.diva_protocol import DIVA_DIAMOND_ADDRESS from telliot_feeds.queries.ampleforth.ampl_usd_vwap import AmpleforthCustomSpotPrice from telliot_feeds.queries.ampleforth.uspce import AmpleforthUSPCE +from telliot_feeds.queries.btc_balance import BTCBalance from telliot_feeds.queries.catalog import Catalog from telliot_feeds.queries.custom_price import CustomPrice from telliot_feeds.queries.daily_volatility import DailyVolatility @@ -513,3 +514,9 @@ title="WMNT/USD spot price", q=SpotPrice(asset="wmnt", currency="usd"), ) + +query_catalog.add_entry( + tag="btc-bal-example", + title="BTC balance example", + q=BTCBalance(address="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389), +) diff --git a/src/telliot_feeds/sources/btc_balance.py b/src/telliot_feeds/sources/btc_balance.py index 2d52308f..9d15651a 100644 --- a/src/telliot_feeds/sources/btc_balance.py +++ b/src/telliot_feeds/sources/btc_balance.py @@ -5,6 +5,7 @@ import requests from requests import JSONDecodeError from requests.adapters import HTTPAdapter +from urllib3.util import Retry from telliot_feeds.datasource import DataSource from telliot_feeds.dtypes.datapoint import OptionalDataPoint @@ -13,6 +14,14 @@ logger = get_logger(__name__) +retry_strategy = Retry( + total=3, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET"], +) +adapter = HTTPAdapter(max_retries=retry_strategy) + @dataclass class BTCBalanceSource(DataSource[Any]): """DataSource for returning the balance of a BTC address at a given timestamp.""" @@ -26,6 +35,10 @@ async def get_response(self) -> Optional[Any]: raise ValueError("BTC address not provided") if not self.timestamp: raise ValueError("Timestamp not provided") + + block_num = await self.block_num_from_timestamp(self.timestamp) + if block_num is None: + return None with requests.Session() as s: url = f"https://blockchain.info/multiaddr?active={self.address}|{self.address}" @@ -43,7 +56,7 @@ async def get_response(self) -> Optional[Any]: except JSONDecodeError: logger.error("Blockchain.info API returned invalid JSON") return None - + if 'txs' not in data: logger.warning("Blockchain.info response doesn't contain needed data") return None @@ -54,39 +67,92 @@ async def get_response(self) -> Optional[Any]: if int(data['addresses'][0]['n_tx']) == 0: # No transactions for this address + logger.info("No transactions for this address") return 0 - + # Sort transactions by time in ascending order - sorted_txs = sorted(data['txs'], key=lambda tx: tx['time']) + sorted_txs = sorted(data['txs'], key=lambda tx: (tx['block_height'], tx['tx_index'])) # Find the most recent transaction before the query's timestamp last_tx = None for tx in sorted_txs: - if tx['time'] > self.timestamp: + if tx['block_height'] > block_num: break last_tx = tx if last_tx is None: # No transactions before the query's timestamp + logger.info("No transactions before the query's timestamp") return 0 # Use the balance from the last transaction as the BTC balance btc_balance = last_tx['balance'] - return btc_balance + + async def block_num_from_timestamp(self, timestamp: int) -> Optional[int]: + """Fetches next Bitcoin blockhash after timestamp from API.""" + with requests.Session() as s: + s.mount("https://", adapter) + ts = timestamp + 480 * 60 + + try: + rsp = s.get(f"https://blockchain.info/blocks/{ts * 1000}?format=json") + except requests.exceptions.ConnectTimeout: + logger.error("Connection timeout getting BTC block num from timestamp") + return None + except requests.exceptions.RequestException as e: + logger.error(f"Blockchain.info API error: {e}") + return None + + try: + blocks = rsp.json() + except JSONDecodeError: + logger.error("Blockchain.info API returned invalid JSON") + return None + + if not isinstance(blocks, list): + logger.warning("Blockchain.info API response is not a list") + return None + + if len(blocks) == 0: + logger.warning("Blockchain.info API returned no blocks") + return None + + if "time" not in blocks[0]: + logger.warning("Blockchain.info response doesn't contain needed data") + return None + + sorted_blocks = sorted(blocks, key=lambda block: block['time']) + + block = sorted_blocks[0] + next_block = sorted_blocks[1] + for b in sorted_blocks: + if b["time"] > timestamp: + next_block = b + break + block = b + + if block["time"] > timestamp: + logger.warning("Blockchain.info API returned no blocks before or equal to timestamp") + return None + logger.info(f"Using BTC block number {block['height']}") + return int(block["height"]) async def fetch_new_datapoint(self) -> OptionalDataPoint[Any]: """Fetches the balance of a BTC address.""" - datapoint = (self.get_response(), datetime_now_utc()) + balance = await self.get_response() + if balance is None: + return None, None + datapoint = (balance, datetime_now_utc()) self.store_datapoint(datapoint) - return self.get_response() + return datapoint if __name__ == "__main__": - btc_balance_source = BTCBalanceSource(address='', timestamp=1706033856) + btc_balance_source = BTCBalanceSource(address='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03', timestamp=1706051389) async def main(): - bal = await btc_balance_source.get_response() + bal = await btc_balance_source.fetch_new_datapoint() print("query timestamp: ", btc_balance_source.timestamp) - print("BTC Balance: ", bal) + print("BTC Balance: ", bal[0]) asyncio.run(main()) diff --git a/tests/queries/test_query_btc_balance.py b/tests/queries/test_query_btc_balance.py new file mode 100644 index 00000000..001cd4dc --- /dev/null +++ b/tests/queries/test_query_btc_balance.py @@ -0,0 +1,33 @@ +""" Unit tests for BTCBalance Query + +Copyright (c) 2024-, Tellor Development Community +Distributed under the terms of the MIT License. +""" +from eth_abi import decode_abi +from eth_abi import decode_single + +from telliot_feeds.queries.btc_balance import BTCBalance + +def test_btc_balance_query(): + """Validate btc balance query""" + q = BTCBalance( + btcAddress="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", + timestamp=1706051389, + ) + assert q.value_type.abi_type == "uint256" + assert q.value_type.packed is False + + exp_abi = bytes.fromhex("00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000a42544342616c616e63650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000065b0473d000000000000000000000000000000000000000000000000000000000000002a6263317130367977736565643673633378326661667070636865667171387639637164306c367678303300000000000000000000000000000000000000000000") + + assert q.query_data == exp_abi + + query_type, encoded_param_vals = decode_abi(["string", "bytes"], q.query_data) + assert query_type == "BTCBalance" + + (btcAddress, timestamp) = decode_abi(["string", "uint256"], encoded_param_vals) + + assert btcAddress == "bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03" + assert timestamp == 1706051389 + assert isinstance(btcAddress, str) + assert isinstance(timestamp, int) + assert q.query_id.hex() == "8e0130642e4beec47a3c2e59daf498fb2ee4069ec58da4ddb34ebc0ed1c62626" \ No newline at end of file diff --git a/tests/sources/test_btc_balance_source.py b/tests/sources/test_btc_balance_source.py new file mode 100644 index 00000000..f1ca8d2e --- /dev/null +++ b/tests/sources/test_btc_balance_source.py @@ -0,0 +1,30 @@ +from datetime import datetime +from unittest import mock + +import pytest +import requests +import asyncio + +from telliot_feeds.sources.btc_balance import BTCBalanceSource + +@pytest.mark.asyncio +async def test_btc_balance(): + """Retrieve random number.""" + # "1652075943" # BCT block num: 731547 + with mock.patch("telliot_feeds.utils.input_timeout.InputTimeout.__call__", side_effect=["1652075943", ""]): + btc_bal_source = BTCBalanceSource(address='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03', timestamp=1706051389) + v, t = await btc_bal_source.fetch_new_datapoint() + + assert v == 151914 + + assert isinstance(v, int) + assert isinstance(t, datetime) + +@pytest.mark.asyncio +async def test_no_value_from_api(): + """Test that no value is returned if the API returns no value.""" + with mock.patch("requests.Session.get", return_value=mock.Mock(json=lambda: {"status": "0", "result": None})): + btc_bal_source = BTCBalanceSource(address='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03', timestamp=1706051389) + v, t = await btc_bal_source.fetch_new_datapoint() + assert v is None + assert t is None From 58075560dde2abdcae11bdc8c981b4d2a8b22b70 Mon Sep 17 00:00:00 2001 From: tkernell Date: Wed, 24 Jan 2024 16:53:27 -0600 Subject: [PATCH 3/6] style and typing --- src/telliot_feeds/feeds/btc_balance.py | 10 +- src/telliot_feeds/queries/btc_balance.py | 4 +- src/telliot_feeds/sources/btc_balance.py | 120 ++++++++++------------- tests/queries/test_query_btc_balance.py | 16 ++- tests/sources/test_btc_balance_source.py | 8 +- 5 files changed, 77 insertions(+), 81 deletions(-) diff --git a/src/telliot_feeds/feeds/btc_balance.py b/src/telliot_feeds/feeds/btc_balance.py index 5d3efb86..48c59ead 100644 --- a/src/telliot_feeds/feeds/btc_balance.py +++ b/src/telliot_feeds/feeds/btc_balance.py @@ -1,6 +1,4 @@ """Datafeed for BTC balance of an address at a specific timestamp.""" -from typing import Optional - from telliot_feeds.datafeed import DataFeed from telliot_feeds.queries.btc_balance import BTCBalance from telliot_feeds.sources.btc_balance import BTCBalanceSource @@ -11,9 +9,9 @@ ) -btcAddress='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03' -timestamp=1706051389 +btcAddress = "bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03" +timestamp = 1706051389 btc_balance_feed_example = DataFeed( source=BTCBalanceSource(address=btcAddress, timestamp=timestamp), - query=BTCBalance(btcAddress==btcAddress, timestamp=timestamp), -) \ No newline at end of file + query=BTCBalance(btcAddress == btcAddress, timestamp=timestamp), +) diff --git a/src/telliot_feeds/queries/btc_balance.py b/src/telliot_feeds/queries/btc_balance.py index 39c716ad..8caebe30 100644 --- a/src/telliot_feeds/queries/btc_balance.py +++ b/src/telliot_feeds/queries/btc_balance.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from typing import Optional -from telliot_feeds.queries.abi_query import AbiQuery from telliot_feeds.dtypes.value_type import ValueType +from telliot_feeds.queries.abi_query import AbiQuery + @dataclass class BTCBalance(AbiQuery): @@ -30,4 +31,3 @@ def value_type(self) -> ValueType: """ return ValueType(abi_type="uint256", packed=False) - diff --git a/src/telliot_feeds/sources/btc_balance.py b/src/telliot_feeds/sources/btc_balance.py index 9d15651a..de19009a 100644 --- a/src/telliot_feeds/sources/btc_balance.py +++ b/src/telliot_feeds/sources/btc_balance.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from typing import Any, Optional -import asyncio +from typing import Any +from typing import Optional import requests from requests import JSONDecodeError @@ -8,9 +8,9 @@ from urllib3.util import Retry from telliot_feeds.datasource import DataSource +from telliot_feeds.dtypes.datapoint import datetime_now_utc from telliot_feeds.dtypes.datapoint import OptionalDataPoint from telliot_feeds.utils.log import get_logger -from telliot_feeds.dtypes.datapoint import datetime_now_utc logger = get_logger(__name__) @@ -22,6 +22,7 @@ ) adapter = HTTPAdapter(max_retries=retry_strategy) + @dataclass class BTCBalanceSource(DataSource[Any]): """DataSource for returning the balance of a BTC address at a given timestamp.""" @@ -35,7 +36,7 @@ async def get_response(self) -> Optional[Any]: raise ValueError("BTC address not provided") if not self.timestamp: raise ValueError("Timestamp not provided") - + block_num = await self.block_num_from_timestamp(self.timestamp) if block_num is None: return None @@ -57,26 +58,26 @@ async def get_response(self) -> Optional[Any]: logger.error("Blockchain.info API returned invalid JSON") return None - if 'txs' not in data: + if "txs" not in data: logger.warning("Blockchain.info response doesn't contain needed data") return None - - if 'addresses' not in data: + + if "addresses" not in data: logger.warning("Blockchain.info response doesn't contain needed data") return None - - if int(data['addresses'][0]['n_tx']) == 0: + + if int(data["addresses"][0]["n_tx"]) == 0: # No transactions for this address logger.info("No transactions for this address") return 0 # Sort transactions by time in ascending order - sorted_txs = sorted(data['txs'], key=lambda tx: (tx['block_height'], tx['tx_index'])) + sorted_txs = sorted(data["txs"], key=lambda tx: (tx["block_height"], tx["tx_index"])) # Find the most recent transaction before the query's timestamp last_tx = None for tx in sorted_txs: - if tx['block_height'] > block_num: + if tx["block_height"] > block_num: break last_tx = tx if last_tx is None: @@ -85,57 +86,55 @@ async def get_response(self) -> Optional[Any]: return 0 # Use the balance from the last transaction as the BTC balance - btc_balance = last_tx['balance'] + btc_balance = last_tx["balance"] return btc_balance - + async def block_num_from_timestamp(self, timestamp: int) -> Optional[int]: - """Fetches next Bitcoin blockhash after timestamp from API.""" - with requests.Session() as s: - s.mount("https://", adapter) - ts = timestamp + 480 * 60 + """Fetches next Bitcoin blockhash after timestamp from API.""" + with requests.Session() as s: + s.mount("https://", adapter) + ts = timestamp + 480 * 60 - try: - rsp = s.get(f"https://blockchain.info/blocks/{ts * 1000}?format=json") - except requests.exceptions.ConnectTimeout: - logger.error("Connection timeout getting BTC block num from timestamp") - return None - except requests.exceptions.RequestException as e: - logger.error(f"Blockchain.info API error: {e}") - return None + try: + rsp = s.get(f"https://blockchain.info/blocks/{ts * 1000}?format=json") + except requests.exceptions.ConnectTimeout: + logger.error("Connection timeout getting BTC block num from timestamp") + return None + except requests.exceptions.RequestException as e: + logger.error(f"Blockchain.info API error: {e}") + return None - try: - blocks = rsp.json() - except JSONDecodeError: - logger.error("Blockchain.info API returned invalid JSON") - return None - - if not isinstance(blocks, list): - logger.warning("Blockchain.info API response is not a list") - return None + try: + blocks = rsp.json() + except JSONDecodeError: + logger.error("Blockchain.info API returned invalid JSON") + return None - if len(blocks) == 0: - logger.warning("Blockchain.info API returned no blocks") - return None + if not isinstance(blocks, list): + logger.warning("Blockchain.info API response is not a list") + return None - if "time" not in blocks[0]: - logger.warning("Blockchain.info response doesn't contain needed data") - return None - - sorted_blocks = sorted(blocks, key=lambda block: block['time']) - - block = sorted_blocks[0] - next_block = sorted_blocks[1] - for b in sorted_blocks: - if b["time"] > timestamp: - next_block = b - break - block = b + if len(blocks) == 0: + logger.warning("Blockchain.info API returned no blocks") + return None - if block["time"] > timestamp: - logger.warning("Blockchain.info API returned no blocks before or equal to timestamp") - return None - logger.info(f"Using BTC block number {block['height']}") - return int(block["height"]) + if "time" not in blocks[0]: + logger.warning("Blockchain.info response doesn't contain needed data") + return None + + sorted_blocks = sorted(blocks, key=lambda block: block["time"]) + + block = sorted_blocks[0] + for b in sorted_blocks: + if b["time"] > timestamp: + break + block = b + + if block["time"] > timestamp: + logger.warning("Blockchain.info API returned no blocks before or equal to timestamp") + return None + logger.info(f"Using BTC block number {block['height']}") + return int(block["height"]) async def fetch_new_datapoint(self) -> OptionalDataPoint[Any]: """Fetches the balance of a BTC address.""" @@ -145,14 +144,3 @@ async def fetch_new_datapoint(self) -> OptionalDataPoint[Any]: datapoint = (balance, datetime_now_utc()) self.store_datapoint(datapoint) return datapoint - - -if __name__ == "__main__": - btc_balance_source = BTCBalanceSource(address='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03', timestamp=1706051389) - - async def main(): - bal = await btc_balance_source.fetch_new_datapoint() - print("query timestamp: ", btc_balance_source.timestamp) - print("BTC Balance: ", bal[0]) - - asyncio.run(main()) diff --git a/tests/queries/test_query_btc_balance.py b/tests/queries/test_query_btc_balance.py index 001cd4dc..4250305c 100644 --- a/tests/queries/test_query_btc_balance.py +++ b/tests/queries/test_query_btc_balance.py @@ -4,10 +4,10 @@ Distributed under the terms of the MIT License. """ from eth_abi import decode_abi -from eth_abi import decode_single from telliot_feeds.queries.btc_balance import BTCBalance + def test_btc_balance_query(): """Validate btc balance query""" q = BTCBalance( @@ -17,7 +17,17 @@ def test_btc_balance_query(): assert q.value_type.abi_type == "uint256" assert q.value_type.packed is False - exp_abi = bytes.fromhex("00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000a42544342616c616e63650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000065b0473d000000000000000000000000000000000000000000000000000000000000002a6263317130367977736565643673633378326661667070636865667171387639637164306c367678303300000000000000000000000000000000000000000000") + exp_abi = bytes.fromhex( + "0000000000000000000000000000000000000000000000000000000000000040000000" + + "0000000000000000000000000000000000000000000000000000000080000000000000" + + "000000000000000000000000000000000000000000000000000a42544342616c616e636" + + "50000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000a0000000000000000000000000000000000" + + "00000000000000000000000000000400000000000000000000000000000000000000000" + + "000000000000000065b0473d00000000000000000000000000000000000000000000000" + + "0000000000000002a626331713036797773656564367363337832666166707063686566" + + "7171387639637164306c367678303300000000000000000000000000000000000000000000" + ) assert q.query_data == exp_abi @@ -30,4 +40,4 @@ def test_btc_balance_query(): assert timestamp == 1706051389 assert isinstance(btcAddress, str) assert isinstance(timestamp, int) - assert q.query_id.hex() == "8e0130642e4beec47a3c2e59daf498fb2ee4069ec58da4ddb34ebc0ed1c62626" \ No newline at end of file + assert q.query_id.hex() == "8e0130642e4beec47a3c2e59daf498fb2ee4069ec58da4ddb34ebc0ed1c62626" diff --git a/tests/sources/test_btc_balance_source.py b/tests/sources/test_btc_balance_source.py index f1ca8d2e..6e13c2d1 100644 --- a/tests/sources/test_btc_balance_source.py +++ b/tests/sources/test_btc_balance_source.py @@ -2,17 +2,16 @@ from unittest import mock import pytest -import requests -import asyncio from telliot_feeds.sources.btc_balance import BTCBalanceSource + @pytest.mark.asyncio async def test_btc_balance(): """Retrieve random number.""" # "1652075943" # BCT block num: 731547 with mock.patch("telliot_feeds.utils.input_timeout.InputTimeout.__call__", side_effect=["1652075943", ""]): - btc_bal_source = BTCBalanceSource(address='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03', timestamp=1706051389) + btc_bal_source = BTCBalanceSource(address="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389) v, t = await btc_bal_source.fetch_new_datapoint() assert v == 151914 @@ -20,11 +19,12 @@ async def test_btc_balance(): assert isinstance(v, int) assert isinstance(t, datetime) + @pytest.mark.asyncio async def test_no_value_from_api(): """Test that no value is returned if the API returns no value.""" with mock.patch("requests.Session.get", return_value=mock.Mock(json=lambda: {"status": "0", "result": None})): - btc_bal_source = BTCBalanceSource(address='bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03', timestamp=1706051389) + btc_bal_source = BTCBalanceSource(address="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389) v, t = await btc_bal_source.fetch_new_datapoint() assert v is None assert t is None From f7c307012dc0f1ac887f038a1eea83ad9e97c638 Mon Sep 17 00:00:00 2001 From: 0xSpuddy Date: Thu, 25 Jan 2024 10:03:15 -0500 Subject: [PATCH 4/6] lil fix --- src/telliot_feeds/queries/query_catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telliot_feeds/queries/query_catalog.py b/src/telliot_feeds/queries/query_catalog.py index f7dfac6b..e7d930a4 100644 --- a/src/telliot_feeds/queries/query_catalog.py +++ b/src/telliot_feeds/queries/query_catalog.py @@ -518,5 +518,5 @@ query_catalog.add_entry( tag="btc-bal-example", title="BTC balance example", - q=BTCBalance(address="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389), + q=BTCBalance(btcAddress="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389), ) From e89f999a5e7fee4f7bc74f321d693ddf11b48827 Mon Sep 17 00:00:00 2001 From: tkernell Date: Thu, 25 Jan 2024 09:48:49 -0600 Subject: [PATCH 5/6] fix query parameter name in cli example --- src/telliot_feeds/queries/query_catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telliot_feeds/queries/query_catalog.py b/src/telliot_feeds/queries/query_catalog.py index f7dfac6b..e7d930a4 100644 --- a/src/telliot_feeds/queries/query_catalog.py +++ b/src/telliot_feeds/queries/query_catalog.py @@ -518,5 +518,5 @@ query_catalog.add_entry( tag="btc-bal-example", title="BTC balance example", - q=BTCBalance(address="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389), + q=BTCBalance(btcAddress="bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03", timestamp=1706051389), ) From 8389e0cd2f4bf69a98b60d5b456b5ad193ccb053 Mon Sep 17 00:00:00 2001 From: 0xSpuddy Date: Thu, 25 Jan 2024 10:59:42 -0500 Subject: [PATCH 6/6] btcAddress issues --- src/telliot_feeds/feeds/__init__.py | 4 ++++ .../feeds/{btc_balance.py => btc_balance_feed.py} | 2 +- src/telliot_feeds/sources/btc_balance.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) rename src/telliot_feeds/feeds/{btc_balance.py => btc_balance_feed.py} (87%) diff --git a/src/telliot_feeds/feeds/__init__.py b/src/telliot_feeds/feeds/__init__.py index 86aef509..647d1048 100644 --- a/src/telliot_feeds/feeds/__init__.py +++ b/src/telliot_feeds/feeds/__init__.py @@ -97,6 +97,8 @@ from telliot_feeds.feeds.wsteth_feed import wsteth_usd_median_feed from telliot_feeds.feeds.xdai_usd_feed import xdai_usd_median_feed from telliot_feeds.feeds.yfi_usd_feed import yfi_usd_median_feed +from telliot_feeds.feeds.btc_balance_feed import bitcoin_balance_feed +from telliot_feeds.feeds.btc_balance_feed import btc_balance_feed_example CATALOG_FEEDS: Dict[str, DataFeed[Any]] = { @@ -182,6 +184,7 @@ "mnt-usd-spot": mnt_usd_median_feed, "usdy-usd-spot": usdy_usd_median_feed, "wmnt-usd-spot": wmnt_usd_median_feed, + "btc-bal-example": btc_balance_feed_example } DATAFEED_BUILDER_MAPPING: Dict[str, DataFeed[Any]] = { @@ -203,4 +206,5 @@ "MimicryMacroMarketMashup": mimicry_mashup_feed, "EVMCall": evm_call_feed, "CustomPrice": custom_price_manual_feed, + "BTCBalance": bitcoin_balance_feed, } diff --git a/src/telliot_feeds/feeds/btc_balance.py b/src/telliot_feeds/feeds/btc_balance_feed.py similarity index 87% rename from src/telliot_feeds/feeds/btc_balance.py rename to src/telliot_feeds/feeds/btc_balance_feed.py index 48c59ead..96b6c614 100644 --- a/src/telliot_feeds/feeds/btc_balance.py +++ b/src/telliot_feeds/feeds/btc_balance_feed.py @@ -12,6 +12,6 @@ btcAddress = "bc1q06ywseed6sc3x2fafppchefqq8v9cqd0l6vx03" timestamp = 1706051389 btc_balance_feed_example = DataFeed( - source=BTCBalanceSource(address=btcAddress, timestamp=timestamp), + source=BTCBalanceSource(btcAddress=btcAddress, timestamp=timestamp), query=BTCBalance(btcAddress == btcAddress, timestamp=timestamp), ) diff --git a/src/telliot_feeds/sources/btc_balance.py b/src/telliot_feeds/sources/btc_balance.py index de19009a..870afc1e 100644 --- a/src/telliot_feeds/sources/btc_balance.py +++ b/src/telliot_feeds/sources/btc_balance.py @@ -27,12 +27,12 @@ class BTCBalanceSource(DataSource[Any]): """DataSource for returning the balance of a BTC address at a given timestamp.""" - address: Optional[str] = None + btcAddress: Optional[str] = None timestamp: Optional[int] = None async def get_response(self) -> Optional[Any]: """gets balance of address from https://blockchain.info/multiaddr?active=$address|$address""" - if not self.address: + if not self.btcAddress: raise ValueError("BTC address not provided") if not self.timestamp: raise ValueError("Timestamp not provided") @@ -42,7 +42,7 @@ async def get_response(self) -> Optional[Any]: return None with requests.Session() as s: - url = f"https://blockchain.info/multiaddr?active={self.address}|{self.address}" + url = f"https://blockchain.info/multiaddr?active={self.btcAddress}|{self.btcAddress}" try: rsp = s.get(url) except requests.exceptions.ConnectTimeout: