mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-02 07:23:25 +00:00
* implement user and change is guest * replace User methods * move remaining functions to user * fix bug * portfolio test * fix tests * fix tests * uncomment skip * ruff * start credentials model implementation * change deault structure * refacto quandl * change default struct and file names * bug * start using global user * add comments * move update flair * change cfg references in keys model * reddit & fmp * refactore additional keys * remaing config keys * remove unsude imports and fix hub keys * ruff * fix line too long * unblock unittests * fix keys model tests * remove os patch * fix account controller tests * fix account controller tests * fix account model * account fixed * fix user tests * User Model * User Model * User Model * Move session folder * User Model : grouping the models * User Model : fix bugs * Config Plot + bug fix * User Model : Refactoring plots * Feature flags * Remove dup * clean comments a fix some vars * fix feature flags controller * refactor obbff from settings controller * refactor a bunch of obbff variables * fix bug blocking terminal * fix integration tests * refactor controllers with obbff * refactor remaining ffs * remove comments * log not started * mispelled variable * set current user on local model * User Model : moving files + updating * User Preferences * User : Refactor is_guest function + bug fix * User : refactor is_guest function * Fix conftest.py * remove lost obbffs * typo on fileoverwrite * Plot config: move variables and start plot dpi * User : tests * refactor plot dpi references * remove plot dpi imports * refactor some config terminal leftover * /account/sync : fix bug * remove configs reload * start path refactor * installation paths vs user paths * remove cfg_plot * Tests : User Refactoring * user prefix only for preferences * user prefix only for preferences * move user paths * remove comment * update apply remote configs * fix tz not updating * save some copys * small fix * create interface for credentials and preferences * wrong docstrings fixed * ruff core * fix some pref setting * login is better * ruff * ruff * remove Literal * add small comment * Tests : User Refactoring * Credentials Model : add DataBento * Fix helper_funcs * Fixing code * Tests : fixing * User Model : freezing the models * Fixed Income + Databento * Tests : fixing keys * black * ruff * Tests : user model * Tests : user model * Fix linting * Fix linting * Linting * Linting * Fixing code and tests * Fix tests * mypy * SDK * Clean ruff cache * Fix syntax issue * Update fred_view * Black * Linting * Update code * Update tests * Linting * Linting * Tests * Black * Linting * Linting * Mock os.listdir to avoid looking for file that doesnt exist * Mock options paths * lint * Tests : fix * Code : update * Tests + Linting * Tests * Ruff * Code : update * Tests * Code : update * Fix workflow * Code : update --------- Co-authored-by: Chavithra PARANA <chavithra@gmail.com> Co-authored-by: luqazino@gmail.com <luqazino@gmail.com> Co-authored-by: James Maslek <jmaslek11@gmail.com>
587 lines
16 KiB
Python
587 lines
16 KiB
Python
"""Ethplorer model"""
|
|
__docformat__ = "numpy"
|
|
|
|
import logging
|
|
import textwrap
|
|
from datetime import datetime
|
|
from time import sleep
|
|
from typing import Any, Optional
|
|
|
|
import pandas as pd
|
|
|
|
from openbb_terminal.core.session.current_user import get_current_user
|
|
from openbb_terminal.cryptocurrency.dataframe_helpers import create_df_index
|
|
from openbb_terminal.decorators import log_start_end
|
|
from openbb_terminal.helper_funcs import request
|
|
from openbb_terminal.rich_config import console
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
PRICES_FILTERS = [
|
|
"date",
|
|
"cap",
|
|
"volumeConverted",
|
|
"open",
|
|
"high",
|
|
"close",
|
|
"low",
|
|
]
|
|
|
|
TOP_FILTERS = [
|
|
"rank",
|
|
"name",
|
|
"symbol",
|
|
"price",
|
|
"txsCount",
|
|
"transfersCount",
|
|
"holdersCount",
|
|
]
|
|
|
|
TH_FILTERS = [
|
|
"value",
|
|
]
|
|
|
|
BALANCE_FILTERS = [
|
|
"index",
|
|
"balance",
|
|
"tokenName",
|
|
"tokenSymbol",
|
|
]
|
|
|
|
HIST_FILTERS = ["timestamp", "transactionHash", "token", "value"]
|
|
|
|
HOLDERS_FILTERS = [
|
|
"balance",
|
|
"share",
|
|
]
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def split_cols_with_dot(column: str) -> str:
|
|
"""Split column name in data frame columns whenever there is a dot between 2 words.
|
|
E.g. price.availableSupply -> priceAvailableSupply.
|
|
|
|
Parameters
|
|
----------
|
|
column: str
|
|
Pandas dataframe column value
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Value of column with replaced format.
|
|
"""
|
|
|
|
@log_start_end(log=logger)
|
|
def replace(string: str, char: str, index: int) -> str:
|
|
"""Helper method which replaces values with dot as a separator and converts it to camelCase format
|
|
|
|
Parameters
|
|
----------
|
|
string: str
|
|
String in which we remove dots and convert it to camelcase format.
|
|
char: str
|
|
First letter of given word.
|
|
index:
|
|
Index of string element.
|
|
|
|
Returns
|
|
----------
|
|
str
|
|
Camel case string with no dots. E.g. price.availableSupply -> priceAvailableSupply.
|
|
"""
|
|
|
|
return string[:index] + char + string[index + 1 :]
|
|
|
|
if "." in column:
|
|
part1, part2 = column.split(".")
|
|
part2 = replace(part2, part2[0].upper(), 0)
|
|
return part1 + part2
|
|
return column
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def enrich_social_media(dct: dict) -> None:
|
|
"""Searching inside dictionary if there are any information about twitter, reddit or coingecko.
|
|
If yes it updates dictionary with url to given social media site.
|
|
|
|
Parameters
|
|
----------
|
|
dct: dict
|
|
Dictionary in which we search for coingecko, twitter or reddit url.
|
|
"""
|
|
|
|
social_media = {
|
|
"twitter": "https://www.twitter.com/",
|
|
"reddit": "https://www.reddit.com/r/",
|
|
"coingecko": "https://www.coingecko.com/en/coins/",
|
|
}
|
|
|
|
for k, v in social_media.items():
|
|
if k in dct:
|
|
dct[k] = v + dct[k]
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def make_request(
|
|
endpoint: str, address: Optional[str] = None, **kwargs: Any
|
|
) -> Optional[dict]:
|
|
"""Helper method that handles request for Ethplorer API [Source: https://ethplorer.io/]
|
|
|
|
Parameters
|
|
----------
|
|
endpoint: str
|
|
endpoint we want to query e.g. https://api.ethplorer.io/<endpoint><arg>?=apiKey=freekey
|
|
address: str
|
|
balance argument for given endpoint. In most cases it's tx hash, or eth balance.
|
|
kwargs: Any
|
|
Additional keywords arguments e.g. limit of transactions
|
|
|
|
Returns
|
|
-------
|
|
Optional[dict]
|
|
dictionary with response data
|
|
"""
|
|
|
|
base_url = "https://api.ethplorer.io/"
|
|
url = f"{base_url}{endpoint}"
|
|
|
|
if address:
|
|
url = url + "/" + address
|
|
|
|
url += f"?apiKey={get_current_user().credentials.API_ETHPLORER_KEY}"
|
|
|
|
if "limit" in kwargs:
|
|
url += f"&limit={kwargs['limit']}"
|
|
|
|
sleep(0.5) # Limit is 2 API calls per 1 sec.
|
|
response = request(url)
|
|
result = {}
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
|
|
if not result:
|
|
console.print("No data found")
|
|
|
|
elif response.status_code == 401:
|
|
console.print("[red]Invalid API Key[/red]\n")
|
|
else:
|
|
console.print(response.json()["error"]["message"])
|
|
|
|
return result
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_token_decimals(address: str) -> Optional[int]:
|
|
"""Helper methods that gets token decimals number. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Blockchain balance e.g. 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984
|
|
|
|
Returns
|
|
-------
|
|
Optional[int]
|
|
Number of decimals for given token.
|
|
"""
|
|
response = make_request("getTokenInfo", address)
|
|
if response and "decimals" in response:
|
|
return 10 ** int(response["decimals"])
|
|
return None
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_address_info(
|
|
address: str, sortby: str = "index", ascend: bool = False
|
|
) -> pd.DataFrame:
|
|
"""Get info about tokens on you ethereum blockchain balance. Eth balance, balance of all tokens which
|
|
have name and symbol. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Blockchain balance e.g. 0x3cD751E6b0078Be393132286c442345e5DC49699
|
|
sortby: str
|
|
Key to sort by.
|
|
ascend: str
|
|
Sort in descending order.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with list of tokens and their balances.
|
|
"""
|
|
|
|
response = make_request("getAddressInfo", address)
|
|
tokens = []
|
|
if "tokens" in response:
|
|
tokens = response.pop("tokens")
|
|
for token in tokens:
|
|
token_info = token.pop("tokenInfo")
|
|
token.update(
|
|
{
|
|
"tokenName": token_info.get("name"),
|
|
"tokenSymbol": token_info.get("symbol"),
|
|
"tokenAddress": token_info.get("balance"),
|
|
"balance": token.get("balance")
|
|
/ (10 ** int(token_info.get("decimals"))),
|
|
}
|
|
)
|
|
elif "token_info" in response:
|
|
token_info = response.get("tokenInfo") or {}
|
|
tokens = [
|
|
{
|
|
"tokenName": token_info.get("name"),
|
|
"tokenSymbol": token_info.get("symbol"),
|
|
"tokenAddress": token_info.get("balance"),
|
|
"balance": token_info.get("balance")
|
|
/ (10 ** int(token_info.get("decimals"))),
|
|
}
|
|
]
|
|
|
|
eth = response.get("ETH") or {}
|
|
eth_balance = eth.get("balance")
|
|
eth_row = [
|
|
"Ethereum",
|
|
"ETH",
|
|
"0x0000000000000000000000000000000000000000",
|
|
eth_balance,
|
|
]
|
|
cols = [
|
|
"tokenName",
|
|
"tokenSymbol",
|
|
"tokenAddress",
|
|
"balance",
|
|
]
|
|
df = pd.DataFrame(tokens)
|
|
eth_row_df = pd.DataFrame([eth_row], columns=cols)
|
|
df = pd.concat([eth_row_df, df], ignore_index=True)
|
|
df = df[df["tokenName"].notna()][cols]
|
|
create_df_index(df, "index")
|
|
df = df.sort_values(by=sortby, ascending=ascend)
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_top_tokens(sortby: str = "rank", ascend: bool = False) -> pd.DataFrame:
|
|
"""Get top 50 tokens. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
sortby: str
|
|
Key to sort by.
|
|
ascend: str
|
|
Sort in descending order.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with list of top 50 tokens.
|
|
"""
|
|
|
|
response = make_request("getTopTokens")
|
|
tokens = response["tokens"]
|
|
df = pd.DataFrame(tokens)[
|
|
[
|
|
"name",
|
|
"symbol",
|
|
"price",
|
|
"txsCount",
|
|
"transfersCount",
|
|
"holdersCount",
|
|
"twitter",
|
|
"coingecko",
|
|
]
|
|
]
|
|
df["price"] = df["price"].apply(lambda x: x["rate"] if x and "rate" in x else None)
|
|
create_df_index(df, "rank")
|
|
df = df.sort_values(by=sortby, ascending=ascend)
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_top_token_holders(
|
|
address: str, sortby: str = "balance", ascend: bool = True
|
|
) -> pd.DataFrame:
|
|
"""Get info about top token holders. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Token balance e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
|
|
sortby: str
|
|
Key to sort by.
|
|
ascend: str
|
|
Sort in descending order.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with list of top token holders.
|
|
"""
|
|
|
|
response = make_request("getTopTokenHolders", address, limit=100)
|
|
df = pd.DataFrame(response["holders"])
|
|
sleep(0.5)
|
|
token_decimals_divider = get_token_decimals(address)
|
|
if token_decimals_divider:
|
|
df["balance"] = df["balance"] / token_decimals_divider
|
|
df = df.sort_values(by=sortby, ascending=ascend)
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_address_history(
|
|
address: str, sortby: str = "timestamp", ascend: bool = True
|
|
) -> pd.DataFrame:
|
|
"""Get information about balance historical transactions. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Blockchain balance e.g. 0x3cD751E6b0078Be393132286c442345e5DC49699
|
|
sortby: str
|
|
Key to sort by.
|
|
ascend: str
|
|
Sort in ascending order.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with balance historical transactions (last 100)
|
|
"""
|
|
response = make_request("getAddressHistory", address, limit=100)
|
|
operations = response.pop("operations")
|
|
if operations:
|
|
for operation in operations:
|
|
token = operation.pop("tokenInfo")
|
|
if token:
|
|
operation["token"] = token["name"]
|
|
operation["tokenAddress"] = token["address"]
|
|
operation["decimals"] = int(token["decimals"])
|
|
operation["timestamp"] = datetime.fromtimestamp(operation["timestamp"])
|
|
|
|
df = pd.DataFrame(operations)
|
|
cols = ["timestamp", "transactionHash", "token", "value"]
|
|
df["value"] = df["value"].astype(float) / (10 ** df["decimals"])
|
|
|
|
if df.empty:
|
|
console.print(f"No historical transaction found for {address}")
|
|
return pd.DataFrame(columns=cols)
|
|
|
|
df = df[cols]
|
|
df = df.sort_values(by=sortby, ascending=ascend)
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_token_info(address) -> pd.DataFrame:
|
|
"""Get info about ERC20 token. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Token balance e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with information about provided ERC20 token.
|
|
"""
|
|
|
|
response = make_request("getTokenInfo", address)
|
|
decimals = response.pop("decimals") if "decimals" in response else None
|
|
|
|
for name in [
|
|
"issuancesCount",
|
|
"lastUpdated",
|
|
"image",
|
|
"transfersCount",
|
|
"ethTransfersCount",
|
|
]:
|
|
try:
|
|
response.pop(name)
|
|
except KeyError as e:
|
|
logger.exception(str(e))
|
|
continue
|
|
|
|
enrich_social_media(response)
|
|
df = pd.json_normalize(response)
|
|
df.columns = [split_cols_with_dot(x) for x in df.columns.tolist()]
|
|
if "priceTs" in df:
|
|
df.drop("priceTs", axis=1, inplace=True)
|
|
|
|
for col in [
|
|
"owner",
|
|
"slot",
|
|
"facebook",
|
|
"priceDiff",
|
|
"priceDiff7d",
|
|
"priceDiff30d",
|
|
"priceVolDiff1",
|
|
"priceVolDiff7",
|
|
"priceVolDiff30",
|
|
"priceCurrency",
|
|
]:
|
|
if col in df.columns:
|
|
df.drop(col, axis=1, inplace=True)
|
|
|
|
df["totalSupply"] = df["totalSupply"].astype(float) / (10 ** int(decimals))
|
|
|
|
df = df.T.reset_index()
|
|
df.columns = ["Metric", "Value"]
|
|
# pylint: disable=unsupported-assignment-operation
|
|
df["Value"] = df["Value"].apply(
|
|
lambda x: "\n".join(textwrap.wrap(x, width=70)) if isinstance(x, str) else x
|
|
)
|
|
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_tx_info(tx_hash: str) -> pd.DataFrame:
|
|
"""Get info about transaction. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
tx_hash: str
|
|
Transaction hash e.g. 0x9dc7b43ad4288c624fdd236b2ecb9f2b81c93e706b2ffd1d19b112c1df7849e6
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with information about ERC20 token transaction.
|
|
"""
|
|
decimals = None
|
|
response = make_request("getTxInfo", tx_hash)
|
|
|
|
try:
|
|
response.pop("logs")
|
|
operations = response.pop("operations")[0]
|
|
if operations:
|
|
operations.pop("addresses")
|
|
token = operations.pop("tokenInfo")
|
|
decimals = token.get("decimals")
|
|
if token:
|
|
operations["token"] = token["name"]
|
|
operations["tokenAddress"] = token["address"]
|
|
operations["timestamp"] = datetime.fromtimestamp(operations["timestamp"])
|
|
response.update(operations)
|
|
response.pop("input")
|
|
|
|
df = pd.Series(response)
|
|
|
|
if decimals:
|
|
for col in ["intValue", "value"]:
|
|
df[col] = float(df[col]) // (10 ** int(decimals))
|
|
|
|
df = df.to_frame().reset_index()
|
|
df.columns = ["Metric", "Value"]
|
|
except KeyError as e:
|
|
logger.exception(str(e))
|
|
return pd.DataFrame()
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_token_history(
|
|
address: str, sortby: str = "timestamp", ascend: bool = False
|
|
) -> pd.DataFrame:
|
|
"""Get info about token historical transactions. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Token e.g. 0xf3db5fa2c66b7af3eb0c0b782510816cbe4813b8
|
|
sortby: str
|
|
Key to sort by.
|
|
ascend: str
|
|
Sort in descending order.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with token historical transactions.
|
|
"""
|
|
|
|
response = make_request("getTokenHistory", address, limit=1000)
|
|
all_operations = []
|
|
operations = response["operations"]
|
|
try:
|
|
first_row = operations[0]["tokenInfo"]
|
|
name, symbol, _ = (
|
|
first_row.get("name"),
|
|
first_row.get("symbol"),
|
|
first_row.get("balance"),
|
|
)
|
|
decimals = first_row.get("decimals")
|
|
except Exception as e:
|
|
logger.exception(str(e))
|
|
name, symbol = "", ""
|
|
decimals = None
|
|
|
|
for operation in operations:
|
|
operation.pop("type")
|
|
operation.pop("tokenInfo")
|
|
operation["timestamp"] = datetime.fromtimestamp(operation["timestamp"])
|
|
all_operations.append(operation)
|
|
|
|
df = pd.DataFrame(all_operations)
|
|
|
|
if df.empty:
|
|
console.print(f"No historical transaction found for {address}")
|
|
return df
|
|
|
|
df[["name", "symbol"]] = name, symbol
|
|
df["value"] = df["value"].astype(float) / (10 ** int(decimals))
|
|
df = df[["timestamp", "name", "symbol", "value", "from", "to", "transactionHash"]]
|
|
df = df.sort_values(by=sortby, ascending=ascend)
|
|
return df
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_token_historical_price(
|
|
address: str,
|
|
sortby: str = "date",
|
|
ascend: bool = False,
|
|
) -> pd.DataFrame:
|
|
"""Get token historical prices with volume and market cap, and average price. [Source: Ethplorer]
|
|
|
|
Parameters
|
|
----------
|
|
address: str
|
|
Token e.g. 0xf3db5fa2c66b7af3eb0c0b782510816cbe4813b8
|
|
sortby: str
|
|
Key to sort by.
|
|
ascend: str
|
|
Sort in descending order.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
DataFrame with token historical prices.
|
|
"""
|
|
|
|
response = make_request("getTokenPriceHistoryGrouped", address)
|
|
data = response["history"]
|
|
data.pop("current")
|
|
prices = data.get("prices")
|
|
|
|
if not prices:
|
|
console.print(f"No historical price found for {address}")
|
|
|
|
return pd.DataFrame()
|
|
|
|
prices_df = pd.DataFrame(prices)
|
|
prices_df["ts"] = prices_df["ts"].apply(lambda x: datetime.fromtimestamp(x))
|
|
if "tmp" in prices_df.columns:
|
|
prices_df.drop("tmp", axis=1, inplace=True)
|
|
|
|
df = prices_df[
|
|
["date", "open", "close", "high", "low", "volumeConverted", "cap", "average"]
|
|
]
|
|
df = df.sort_values(by=sortby, ascending=ascend)
|
|
return df
|