mm-balance 0.1.13__tar.gz → 0.1.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mm_balance-0.1.13 → mm_balance-0.1.15}/PKG-INFO +2 -1
- {mm_balance-0.1.13 → mm_balance-0.1.15}/justfile +1 -1
- {mm_balance-0.1.13 → mm_balance-0.1.15}/pyproject.toml +5 -4
- mm_balance-0.1.15/src/mm_balance/cli.py +84 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/src/mm_balance/config/example.yml +11 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/src/mm_balance/config.py +16 -6
- mm_balance-0.1.15/src/mm_balance/constants.py +69 -0
- mm_balance-0.1.15/src/mm_balance/output/formats/json_format.py +27 -0
- mm_balance-0.1.15/src/mm_balance/output/formats/table_format.py +115 -0
- mm_balance-0.1.15/src/mm_balance/output/utils.py +20 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/src/mm_balance/price.py +5 -23
- mm_balance-0.1.15/src/mm_balance/result.py +131 -0
- mm_balance-0.1.15/src/mm_balance/rpc/aptos.py +23 -0
- mm_balance-0.1.15/src/mm_balance/rpc/evm.py +27 -0
- mm_balance-0.1.15/src/mm_balance/rpc/solana.py +26 -0
- mm_balance-0.1.15/src/mm_balance/token_decimals.py +55 -0
- mm_balance-0.1.15/src/mm_balance/utils.py +10 -0
- mm_balance-0.1.15/src/mm_balance/workers.py +84 -0
- mm_balance-0.1.15/tests/conftest.py +0 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/uv.lock +111 -97
- mm_balance-0.1.13/src/mm_balance/balances.py +0 -90
- mm_balance-0.1.13/src/mm_balance/cli.py +0 -58
- mm_balance-0.1.13/src/mm_balance/constants.py +0 -56
- mm_balance-0.1.13/src/mm_balance/output.py +0 -82
- mm_balance-0.1.13/src/mm_balance/rpc/eth.py +0 -31
- mm_balance-0.1.13/src/mm_balance/rpc/solana.py +0 -26
- mm_balance-0.1.13/src/mm_balance/token_decimals.py +0 -37
- mm_balance-0.1.13/src/mm_balance/total.py +0 -112
- {mm_balance-0.1.13 → mm_balance-0.1.15}/.gitignore +0 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/README.md +0 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/src/mm_balance/__init__.py +0 -0
- {mm_balance-0.1.13/src/mm_balance/rpc → mm_balance-0.1.15/src/mm_balance/output}/__init__.py +0 -0
- {mm_balance-0.1.13/tests → mm_balance-0.1.15/src/mm_balance/rpc}/__init__.py +0 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/src/mm_balance/rpc/btc.py +0 -0
- /mm_balance-0.1.13/tests/conftest.py → /mm_balance-0.1.15/tests/__init__.py +0 -0
- {mm_balance-0.1.13 → mm_balance-0.1.15}/tests/test_dummy.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-balance"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.15"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"mm-btc==0.1.0",
|
|
8
8
|
"mm-eth==0.1.3",
|
|
9
9
|
"mm-solana==0.1.5",
|
|
10
|
+
"mm-aptos==0.1.2",
|
|
10
11
|
"typer>=0.12.5",
|
|
11
12
|
]
|
|
12
13
|
[project.scripts]
|
|
@@ -21,11 +22,11 @@ dev-dependencies = [
|
|
|
21
22
|
"pytest~=8.3.3",
|
|
22
23
|
"pytest-xdist~=3.6.1",
|
|
23
24
|
"pytest-httpserver~=1.1.0",
|
|
24
|
-
"coverage~=7.6.
|
|
25
|
-
"ruff~=0.
|
|
25
|
+
"coverage~=7.6.4",
|
|
26
|
+
"ruff~=0.7.1",
|
|
26
27
|
"pip-audit~=2.7.0",
|
|
27
28
|
"bandit~=1.7.10",
|
|
28
|
-
"mypy~=1.
|
|
29
|
+
"mypy~=1.13.0",
|
|
29
30
|
"types-python-dateutil~=2.9.0.20241003",
|
|
30
31
|
"types-PyYAML~=6.0.12.20240917",
|
|
31
32
|
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import pathlib
|
|
3
|
+
import pkgutil
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from mm_std import PrintFormat, fatal
|
|
8
|
+
|
|
9
|
+
from mm_balance.config import Config
|
|
10
|
+
from mm_balance.constants import NETWORKS
|
|
11
|
+
from mm_balance.output.formats import json_format, table_format
|
|
12
|
+
from mm_balance.price import Prices, get_prices
|
|
13
|
+
from mm_balance.result import create_balances_result
|
|
14
|
+
from mm_balance.token_decimals import get_token_decimals
|
|
15
|
+
from mm_balance.workers import Workers
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def example_callback(value: bool) -> None:
|
|
21
|
+
if value:
|
|
22
|
+
data = pkgutil.get_data(__name__, "config/example.yml")
|
|
23
|
+
typer.echo(data)
|
|
24
|
+
raise typer.Exit
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def networks_callback(value: bool) -> None:
|
|
28
|
+
if value:
|
|
29
|
+
for network in NETWORKS:
|
|
30
|
+
typer.echo(network)
|
|
31
|
+
raise typer.Exit
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def cli(
|
|
36
|
+
config_path: Annotated[pathlib.Path, typer.Argument()],
|
|
37
|
+
print_format: Annotated[PrintFormat | None, typer.Option("--format", "-f", help="Print format.")] = None,
|
|
38
|
+
skip_empty: Annotated[bool | None, typer.Option("--skip-empty", "-s", help="Skip empty balances.")] = None,
|
|
39
|
+
debug: Annotated[bool | None, typer.Option("--debug", "-d", help="Print debug info.")] = None,
|
|
40
|
+
price: Annotated[bool | None, typer.Option("--price/--no-price", help="Print prices.")] = None,
|
|
41
|
+
_example: Annotated[bool | None, typer.Option("--example", callback=example_callback, help="Print a config example.")] = None,
|
|
42
|
+
_networks: Annotated[
|
|
43
|
+
bool | None, typer.Option("--networks", callback=networks_callback, help="Print supported networks.")
|
|
44
|
+
] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
zip_password = "" # nosec
|
|
47
|
+
if config_path.name.endswith(".zip"):
|
|
48
|
+
zip_password = getpass.getpass("zip password")
|
|
49
|
+
config = Config.read_config(config_path, zip_password=zip_password)
|
|
50
|
+
|
|
51
|
+
if print_format is not None:
|
|
52
|
+
config.print_format = print_format
|
|
53
|
+
if debug is not None:
|
|
54
|
+
config.print_debug = debug
|
|
55
|
+
if skip_empty is not None:
|
|
56
|
+
config.skip_empty = skip_empty
|
|
57
|
+
if price is not None:
|
|
58
|
+
config.price = price
|
|
59
|
+
|
|
60
|
+
if config.print_debug and config.print_format is PrintFormat.TABLE:
|
|
61
|
+
table_format.print_nodes(config)
|
|
62
|
+
|
|
63
|
+
token_decimals = get_token_decimals(config)
|
|
64
|
+
if config.print_debug and config.print_format is PrintFormat.TABLE:
|
|
65
|
+
table_format.print_token_decimals(token_decimals)
|
|
66
|
+
|
|
67
|
+
prices = get_prices(config) if config.price else Prices()
|
|
68
|
+
if config.print_format is PrintFormat.TABLE:
|
|
69
|
+
table_format.print_prices(config, prices)
|
|
70
|
+
|
|
71
|
+
workers = Workers(config, token_decimals)
|
|
72
|
+
workers.process()
|
|
73
|
+
|
|
74
|
+
result = create_balances_result(config, prices, workers)
|
|
75
|
+
if config.print_format is PrintFormat.TABLE:
|
|
76
|
+
table_format.print_result(config, result, workers)
|
|
77
|
+
elif config.print_format is PrintFormat.JSON:
|
|
78
|
+
json_format.print_result(config, token_decimals, prices, workers, result)
|
|
79
|
+
else:
|
|
80
|
+
fatal("Unsupported print format")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
app()
|
|
@@ -38,6 +38,14 @@ coins:
|
|
|
38
38
|
comment: binance
|
|
39
39
|
addresses: binance_eth
|
|
40
40
|
|
|
41
|
+
- ticker: USDC
|
|
42
|
+
comment: okx aptos
|
|
43
|
+
network: aptos
|
|
44
|
+
token_address: 0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDC
|
|
45
|
+
token_decimals: 6
|
|
46
|
+
addresses: "0x834d639b10d20dcb894728aa4b9b572b2ea2d97073b10eacb111f338b20ea5d7" # for a single line it's necessary to use quotes
|
|
47
|
+
|
|
48
|
+
|
|
41
49
|
addresses:
|
|
42
50
|
- name: okx_eth
|
|
43
51
|
addresses: |
|
|
@@ -61,3 +69,6 @@ addresses:
|
|
|
61
69
|
#round_ndigits: 4
|
|
62
70
|
#price: yes
|
|
63
71
|
#skip_empty: no
|
|
72
|
+
#print_debug: no
|
|
73
|
+
#print_format: table # table, json
|
|
74
|
+
#format_number_separator: ","
|
|
@@ -15,7 +15,8 @@ class Group(BaseConfig):
|
|
|
15
15
|
comment: str = ""
|
|
16
16
|
ticker: str
|
|
17
17
|
network: Network
|
|
18
|
-
token_address: str | None = None
|
|
18
|
+
token_address: str | None = None # If None, it's a native token, for example ETH
|
|
19
|
+
token_decimals: int | None = None
|
|
19
20
|
coingecko_id: str | None = None
|
|
20
21
|
addresses: list[str] = Field(default_factory=list)
|
|
21
22
|
share: Decimal = Decimal(1)
|
|
@@ -25,7 +26,7 @@ class Group(BaseConfig):
|
|
|
25
26
|
result = self.ticker
|
|
26
27
|
if self.comment:
|
|
27
28
|
result += " / " + self.comment
|
|
28
|
-
result += " / " + self.network
|
|
29
|
+
result += " / " + self.network
|
|
29
30
|
return result
|
|
30
31
|
|
|
31
32
|
@field_validator("ticker", mode="after")
|
|
@@ -46,7 +47,7 @@ class Group(BaseConfig):
|
|
|
46
47
|
def final_validator(self) -> Self:
|
|
47
48
|
if self.token_address is None:
|
|
48
49
|
self.token_address = detect_token_address(self.ticker, self.network)
|
|
49
|
-
if self.token_address is not None and self.network
|
|
50
|
+
if self.token_address is not None and self.network.is_evm_network():
|
|
50
51
|
self.token_address = self.token_address.lower()
|
|
51
52
|
return self
|
|
52
53
|
|
|
@@ -81,12 +82,16 @@ class Config(BaseConfig):
|
|
|
81
82
|
print_format: PrintFormat = PrintFormat.TABLE
|
|
82
83
|
price: bool = True
|
|
83
84
|
skip_empty: bool = False # don't print the address with an empty balance
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
print_debug: bool = False # print debug info: nodes, token_decimals
|
|
86
|
+
format_number_separator: str = "," # as thousands separators
|
|
87
|
+
workers: dict[Network, int] = Field(default_factory=dict)
|
|
86
88
|
|
|
87
89
|
def has_share(self) -> bool:
|
|
88
90
|
return any(g.share != Decimal(1) for g in self.groups)
|
|
89
91
|
|
|
92
|
+
def networks(self) -> list[Network]:
|
|
93
|
+
return pydash.uniq([group.network for group in self.groups])
|
|
94
|
+
|
|
90
95
|
@model_validator(mode="after")
|
|
91
96
|
def final_validator(self) -> Self:
|
|
92
97
|
# load from proxies_url
|
|
@@ -98,10 +103,15 @@ class Config(BaseConfig):
|
|
|
98
103
|
group.process_addresses(self.addresses)
|
|
99
104
|
|
|
100
105
|
# load default rpc nodes
|
|
101
|
-
for network in
|
|
106
|
+
for network in self.networks():
|
|
102
107
|
if network not in self.nodes:
|
|
103
108
|
self.nodes[network] = DEFAULT_NODES[network]
|
|
104
109
|
|
|
110
|
+
# load default workers
|
|
111
|
+
for network in self.networks():
|
|
112
|
+
if network not in self.workers:
|
|
113
|
+
self.workers[network] = 5
|
|
114
|
+
|
|
105
115
|
return self
|
|
106
116
|
|
|
107
117
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
|
4
|
+
from pydantic_core import CoreSchema, core_schema
|
|
5
|
+
|
|
6
|
+
RETRIES_BALANCE = 5
|
|
7
|
+
RETRIES_DECIMALS = 5
|
|
8
|
+
RETRIES_COINGECKO_PRICES = 5
|
|
9
|
+
TIMEOUT_BALANCE = 5
|
|
10
|
+
TIMEOUT_DECIMALS = 5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Network(str):
|
|
14
|
+
def is_evm_network(self) -> bool:
|
|
15
|
+
return self in [NETWORK_ETHEREUM, NETWORK_ARBITRUM_ONE, NETWORK_OP_MAINNET] or self.startswith("evm-")
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def __get_pydantic_core_schema__(cls, _source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
19
|
+
return core_schema.no_info_after_validator_function(cls, handler(str))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
NETWORK_APTOS = Network("aptos")
|
|
23
|
+
NETWORK_ARBITRUM_ONE = Network("arbitrum-one")
|
|
24
|
+
NETWORK_BITCOIN = Network("bitcoin")
|
|
25
|
+
NETWORK_ETHEREUM = Network("ethereum")
|
|
26
|
+
NETWORK_SOLANA = Network("solana")
|
|
27
|
+
NETWORK_OP_MAINNET = Network("op-mainnet")
|
|
28
|
+
NETWORKS = [NETWORK_APTOS, NETWORK_ARBITRUM_ONE, NETWORK_BITCOIN, NETWORK_ETHEREUM, NETWORK_SOLANA, NETWORK_OP_MAINNET]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TOKEN_ADDRESS: dict[Network, dict[str, str]] = {
|
|
32
|
+
NETWORK_ETHEREUM: {
|
|
33
|
+
"USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
|
34
|
+
"USDC": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
|
35
|
+
},
|
|
36
|
+
NETWORK_SOLANA: {
|
|
37
|
+
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
|
38
|
+
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
39
|
+
},
|
|
40
|
+
NETWORK_ARBITRUM_ONE: {
|
|
41
|
+
"USDT": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
|
|
42
|
+
"USDC": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
|
|
43
|
+
},
|
|
44
|
+
NETWORK_OP_MAINNET: {
|
|
45
|
+
"USDT": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
|
|
46
|
+
"USDC": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
TICKER_TO_COINGECKO_ID = {
|
|
51
|
+
"BTC": "bitcoin",
|
|
52
|
+
"ETH": "ethereum",
|
|
53
|
+
"USDT": "tether",
|
|
54
|
+
"USDC": "usd-coin",
|
|
55
|
+
"SOL": "solana",
|
|
56
|
+
"APT": "aptos",
|
|
57
|
+
"POL": "matic-network",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
USD_STABLECOINS = ["USDT", "USDC"]
|
|
61
|
+
|
|
62
|
+
DEFAULT_NODES: dict[Network, list[str]] = {
|
|
63
|
+
NETWORK_ARBITRUM_ONE: ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com"],
|
|
64
|
+
NETWORK_BITCOIN: [],
|
|
65
|
+
NETWORK_ETHEREUM: ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"],
|
|
66
|
+
NETWORK_SOLANA: ["https://api.mainnet-beta.solana.com"],
|
|
67
|
+
NETWORK_OP_MAINNET: ["https://mainnet.optimism.io", "https://optimism.llamarpc.com"],
|
|
68
|
+
NETWORK_APTOS: ["https://fullnode.mainnet.aptoslabs.com/v1"],
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from mm_std import print_json
|
|
2
|
+
|
|
3
|
+
from mm_balance.config import Config
|
|
4
|
+
from mm_balance.price import Prices
|
|
5
|
+
from mm_balance.result import BalancesResult
|
|
6
|
+
from mm_balance.token_decimals import TokenDecimals
|
|
7
|
+
from mm_balance.workers import Workers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_result(config: Config, token_decimals: TokenDecimals, prices: Prices, workers: Workers, result: BalancesResult) -> None:
|
|
11
|
+
data: dict[str, object] = {}
|
|
12
|
+
if config.print_debug:
|
|
13
|
+
data["nodes"] = config.nodes
|
|
14
|
+
data["token_decimals"] = token_decimals
|
|
15
|
+
if config.price:
|
|
16
|
+
data["prices"] = prices
|
|
17
|
+
|
|
18
|
+
data["groups"] = result.groups
|
|
19
|
+
data["total"] = result.total
|
|
20
|
+
if config.has_share():
|
|
21
|
+
data["total_share"] = result.total_share
|
|
22
|
+
|
|
23
|
+
errors = workers.get_errors()
|
|
24
|
+
if errors:
|
|
25
|
+
data["errors"] = errors
|
|
26
|
+
|
|
27
|
+
print_json(data)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_std import print_table
|
|
4
|
+
|
|
5
|
+
from mm_balance.config import Config
|
|
6
|
+
from mm_balance.output.utils import format_number
|
|
7
|
+
from mm_balance.price import Prices
|
|
8
|
+
from mm_balance.result import BalancesResult, GroupResult, Total
|
|
9
|
+
from mm_balance.token_decimals import TokenDecimals
|
|
10
|
+
from mm_balance.workers import Workers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_nodes(config: Config) -> None:
|
|
14
|
+
rows = []
|
|
15
|
+
for network, nodes in config.nodes.items():
|
|
16
|
+
rows.append([network, "\n".join(nodes)])
|
|
17
|
+
print_table("Nodes", ["network", "nodes"], rows)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_token_decimals(token_decimals: TokenDecimals) -> None:
|
|
21
|
+
rows = []
|
|
22
|
+
for network, decimals in token_decimals.items():
|
|
23
|
+
rows.append([network, decimals])
|
|
24
|
+
print_table("Token Decimals", ["network", "decimals"], rows)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_prices(config: Config, prices: Prices) -> None:
|
|
28
|
+
if config.price:
|
|
29
|
+
rows = []
|
|
30
|
+
for ticker, price in prices.items():
|
|
31
|
+
rows.append([ticker, format_number(round(price, config.round_ndigits), config.format_number_separator, "$")])
|
|
32
|
+
print_table("Prices", ["coin", "usd"], rows)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_result(config: Config, result: BalancesResult, workers: Workers) -> None:
|
|
36
|
+
for group in result.groups:
|
|
37
|
+
_print_group(config, group)
|
|
38
|
+
|
|
39
|
+
_print_total(config, result.total, False)
|
|
40
|
+
if config.has_share():
|
|
41
|
+
_print_total(config, result.total_share, True)
|
|
42
|
+
|
|
43
|
+
_print_errors(config, workers)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _print_errors(config: Config, workers: Workers) -> None:
|
|
47
|
+
error_tasks = workers.get_errors()
|
|
48
|
+
if not error_tasks:
|
|
49
|
+
return
|
|
50
|
+
rows = []
|
|
51
|
+
for task in error_tasks:
|
|
52
|
+
group = config.groups[task.group_index]
|
|
53
|
+
rows.append([group.ticker + " / " + group.network, task.wallet_address, task.balance.err]) # type: ignore[union-attr]
|
|
54
|
+
print_table("Errors", ["coin", "address", "error"], rows)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_total(config: Config, total: Total, is_share_total: bool) -> None:
|
|
58
|
+
table_name = "Total, share" if is_share_total else "Total"
|
|
59
|
+
headers = ["coin", "balance"]
|
|
60
|
+
|
|
61
|
+
rows = []
|
|
62
|
+
for ticker, balance in total.coin_balances.items():
|
|
63
|
+
balance_str = format_number(balance, config.format_number_separator)
|
|
64
|
+
row = [ticker, balance_str]
|
|
65
|
+
if config.price:
|
|
66
|
+
usd_value_str = format_number(total.coin_usd_values[ticker], config.format_number_separator, "$")
|
|
67
|
+
portfolio_share = total.portfolio_share[ticker]
|
|
68
|
+
row += [usd_value_str, f"{portfolio_share}%"]
|
|
69
|
+
rows.append(row)
|
|
70
|
+
|
|
71
|
+
if config.price:
|
|
72
|
+
headers += ["usd", "portfolio_share"]
|
|
73
|
+
if total.stablecoin_sum > 0:
|
|
74
|
+
rows.append(["stablecoin_sum", format_number(total.stablecoin_sum, config.format_number_separator, "$")])
|
|
75
|
+
rows.append(["total_usd_sum", format_number(total.total_usd_sum, config.format_number_separator, "$")])
|
|
76
|
+
|
|
77
|
+
print_table(table_name, headers, rows)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _print_group(config: Config, group: GroupResult) -> None:
|
|
81
|
+
group_name = group.ticker
|
|
82
|
+
if group.comment:
|
|
83
|
+
group_name += " / " + group.comment
|
|
84
|
+
group_name += " / " + group.network
|
|
85
|
+
|
|
86
|
+
rows = []
|
|
87
|
+
for address in group.addresses:
|
|
88
|
+
if isinstance(address.balance, str):
|
|
89
|
+
rows.append([address.address, address.balance])
|
|
90
|
+
else:
|
|
91
|
+
if config.skip_empty and address.balance.balance == Decimal(0):
|
|
92
|
+
continue
|
|
93
|
+
balance_str = format_number(address.balance.balance, config.format_number_separator)
|
|
94
|
+
row = [address.address, balance_str]
|
|
95
|
+
if config.price:
|
|
96
|
+
usd_value_str = format_number(address.balance.usd_value, config.format_number_separator, "$")
|
|
97
|
+
row.append(usd_value_str)
|
|
98
|
+
rows.append(row)
|
|
99
|
+
|
|
100
|
+
sum_row = ["sum", format_number(group.balance_sum, config.format_number_separator)]
|
|
101
|
+
if config.price:
|
|
102
|
+
sum_row.append(format_number(group.usd_sum, config.format_number_separator, "$"))
|
|
103
|
+
rows.append(sum_row)
|
|
104
|
+
|
|
105
|
+
if group.share < Decimal(1):
|
|
106
|
+
sum_share_str = format_number(group.balance_sum_share, config.format_number_separator)
|
|
107
|
+
sum_share_row = [f"sum_share, {group.share}", sum_share_str]
|
|
108
|
+
if config.price:
|
|
109
|
+
sum_share_row.append(format_number(group.usd_sum_share, config.format_number_separator, "$"))
|
|
110
|
+
rows.append(sum_share_row)
|
|
111
|
+
|
|
112
|
+
table_headers = ["address", "balance"]
|
|
113
|
+
if config.price:
|
|
114
|
+
table_headers += ["usd"]
|
|
115
|
+
print_table(group_name, table_headers, rows)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskID, TextColumn
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_number(value: Decimal, separator: str, extra: str | None = None) -> str:
|
|
7
|
+
str_value = f"{value:,}".replace(",", separator)
|
|
8
|
+
if extra == "$":
|
|
9
|
+
return "$" + str_value
|
|
10
|
+
elif extra == "%":
|
|
11
|
+
return str_value + "%"
|
|
12
|
+
return str_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_progress_bar(disable: bool) -> Progress:
|
|
16
|
+
return Progress(TextColumn("[progress.description]{task.description}"), BarColumn(), MofNCompleteColumn(), disable=disable)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_progress_task(progress: Progress, description: str, total: int) -> TaskID:
|
|
20
|
+
return progress.add_task("[green]" + description, total=total)
|
|
@@ -5,7 +5,7 @@ from mm_std import fatal, hr
|
|
|
5
5
|
from mm_std.random_ import random_str_choice
|
|
6
6
|
|
|
7
7
|
from mm_balance.config import Config, Group
|
|
8
|
-
from mm_balance.constants import RETRIES_COINGECKO_PRICES,
|
|
8
|
+
from mm_balance.constants import RETRIES_COINGECKO_PRICES, TICKER_TO_COINGECKO_ID
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Prices(dict[str, Decimal]):
|
|
@@ -38,29 +38,11 @@ def get_prices(config: Config) -> Prices:
|
|
|
38
38
|
return result
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
TICKER_TO_COINGECKO_ID = {
|
|
42
|
-
"BTC": "bitcoin",
|
|
43
|
-
"ETH": "ethereum",
|
|
44
|
-
"USDT": "tether",
|
|
45
|
-
"USDC": "usd-coin",
|
|
46
|
-
"SOL": "solana",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
41
|
def get_coingecko_id(group: Group) -> str:
|
|
51
42
|
if group.coingecko_id:
|
|
52
43
|
return group.coingecko_id
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return "ethereum"
|
|
57
|
-
elif group.ticker == "ETH":
|
|
58
|
-
return "ethereum"
|
|
59
|
-
elif group.ticker == "USDT":
|
|
60
|
-
return "tether"
|
|
61
|
-
elif group.ticker == "USDC":
|
|
62
|
-
return "usd-coin"
|
|
63
|
-
elif group.ticker == "SOL":
|
|
64
|
-
return "solana"
|
|
44
|
+
coingecko_id = TICKER_TO_COINGECKO_ID.get(group.ticker)
|
|
45
|
+
if coingecko_id:
|
|
46
|
+
return coingecko_id
|
|
65
47
|
|
|
66
|
-
|
|
48
|
+
fatal(f"Can't get coingecko_id for {group.ticker}. Please add coingecko_id to the config.")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from mm_std import Ok
|
|
6
|
+
|
|
7
|
+
from mm_balance.config import Config, Group
|
|
8
|
+
from mm_balance.constants import USD_STABLECOINS, Network
|
|
9
|
+
from mm_balance.price import Prices
|
|
10
|
+
from mm_balance.workers import Task, Workers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Balance:
|
|
15
|
+
balance: Decimal
|
|
16
|
+
usd_value: Decimal # 0 if config.price is False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AddressBalance:
|
|
21
|
+
address: str
|
|
22
|
+
balance: Balance | str # balance value or error message
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class GroupResult:
|
|
27
|
+
ticker: str
|
|
28
|
+
network: Network
|
|
29
|
+
comment: str
|
|
30
|
+
share: Decimal
|
|
31
|
+
addresses: list[AddressBalance]
|
|
32
|
+
balance_sum: Decimal # sum of all balances in the group
|
|
33
|
+
usd_sum: Decimal # sum of all usd values in the group
|
|
34
|
+
balance_sum_share: Decimal # sum of all balances in the group multiplied by share
|
|
35
|
+
usd_sum_share: Decimal # sum of all usd values in the group multiplied by share
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Total:
|
|
40
|
+
coin_balances: dict[str, Decimal]
|
|
41
|
+
coin_usd_values: dict[str, Decimal]
|
|
42
|
+
portfolio_share: dict[str, Decimal] # ticker -> usd value % from total usd value
|
|
43
|
+
stablecoin_sum: Decimal # sum of usd stablecoins: usdt, usdc, etc..
|
|
44
|
+
total_usd_sum: Decimal # sum of all coins in USD
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class BalancesResult:
|
|
49
|
+
groups: list[GroupResult]
|
|
50
|
+
total: Total
|
|
51
|
+
total_share: Total
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_balances_result(config: Config, prices: Prices, workers: Workers) -> BalancesResult:
|
|
55
|
+
groups = []
|
|
56
|
+
for group_index, group in enumerate(config.groups):
|
|
57
|
+
tasks = workers.get_group_tasks(group_index, group.network)
|
|
58
|
+
groups.append(_create_group_result(config, group, tasks, prices))
|
|
59
|
+
|
|
60
|
+
total = _create_total(False, groups)
|
|
61
|
+
total_share = _create_total(True, groups)
|
|
62
|
+
return BalancesResult(groups=groups, total=total, total_share=total_share)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _create_total(use_share: bool, groups: list[GroupResult]) -> Total:
|
|
66
|
+
coin_balances: dict[str, Decimal] = defaultdict(Decimal) # ticker -> balance
|
|
67
|
+
coin_usd_values: dict[str, Decimal] = defaultdict(Decimal) # ticker -> usd value
|
|
68
|
+
portfolio_share: dict[str, Decimal] = defaultdict(Decimal) # ticker -> usd value % from total usd value
|
|
69
|
+
total_usd_sum = Decimal(0)
|
|
70
|
+
stablecoin_sum = Decimal(0)
|
|
71
|
+
|
|
72
|
+
for group in groups:
|
|
73
|
+
balance_value = group.balance_sum_share if use_share else group.balance_sum
|
|
74
|
+
usd_value = group.usd_sum_share if use_share else group.usd_sum
|
|
75
|
+
coin_balances[group.ticker] += balance_value
|
|
76
|
+
coin_usd_values[group.ticker] += usd_value
|
|
77
|
+
if group.ticker in USD_STABLECOINS:
|
|
78
|
+
stablecoin_sum += usd_value # TODO: or balance_value?
|
|
79
|
+
total_usd_sum += usd_value
|
|
80
|
+
|
|
81
|
+
if total_usd_sum > 0:
|
|
82
|
+
for ticker, usd_value in coin_usd_values.items():
|
|
83
|
+
if ticker in USD_STABLECOINS:
|
|
84
|
+
portfolio_share[ticker] = round(stablecoin_sum * 100 / total_usd_sum, 2)
|
|
85
|
+
else:
|
|
86
|
+
portfolio_share[ticker] = round(usd_value * 100 / total_usd_sum, 2)
|
|
87
|
+
|
|
88
|
+
return Total(
|
|
89
|
+
coin_balances=coin_balances,
|
|
90
|
+
coin_usd_values=coin_usd_values,
|
|
91
|
+
portfolio_share=portfolio_share,
|
|
92
|
+
stablecoin_sum=stablecoin_sum,
|
|
93
|
+
total_usd_sum=total_usd_sum,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _create_group_result(config: Config, group: Group, tasks: list[Task], prices: Prices) -> GroupResult:
|
|
98
|
+
addresses = []
|
|
99
|
+
balance_sum = Decimal(0)
|
|
100
|
+
usd_sum = Decimal(0)
|
|
101
|
+
for task in tasks:
|
|
102
|
+
balance: Balance | str
|
|
103
|
+
if task.balance is None:
|
|
104
|
+
balance = "balance is None! Something went wrong."
|
|
105
|
+
else:
|
|
106
|
+
if isinstance(task.balance, Ok):
|
|
107
|
+
coin_value = task.balance.ok
|
|
108
|
+
usd_value = Decimal(0)
|
|
109
|
+
if group.ticker in prices:
|
|
110
|
+
usd_value = round(coin_value * prices[group.ticker], config.round_ndigits)
|
|
111
|
+
balance = Balance(balance=coin_value, usd_value=usd_value)
|
|
112
|
+
balance_sum += balance.balance
|
|
113
|
+
usd_sum += balance.usd_value
|
|
114
|
+
else:
|
|
115
|
+
balance = task.balance.err
|
|
116
|
+
addresses.append(AddressBalance(address=task.wallet_address, balance=balance))
|
|
117
|
+
|
|
118
|
+
balance_sum_share = balance_sum * group.share
|
|
119
|
+
usd_sum_share = usd_sum * group.share
|
|
120
|
+
|
|
121
|
+
return GroupResult(
|
|
122
|
+
ticker=group.ticker,
|
|
123
|
+
network=group.network,
|
|
124
|
+
comment=group.comment,
|
|
125
|
+
share=group.share,
|
|
126
|
+
addresses=addresses,
|
|
127
|
+
balance_sum=balance_sum,
|
|
128
|
+
usd_sum=usd_sum,
|
|
129
|
+
balance_sum_share=balance_sum_share,
|
|
130
|
+
usd_sum_share=usd_sum_share,
|
|
131
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_aptos import balance
|
|
4
|
+
from mm_std import Result
|
|
5
|
+
|
|
6
|
+
from mm_balance.constants import RETRIES_BALANCE, TIMEOUT_BALANCE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_balance(
|
|
10
|
+
nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
|
|
11
|
+
) -> Result[Decimal]:
|
|
12
|
+
if token is None:
|
|
13
|
+
token = "0x1::aptos_coin::AptosCoin" # nosec
|
|
14
|
+
return balance.get_decimal_balance_with_retries(
|
|
15
|
+
RETRIES_BALANCE,
|
|
16
|
+
nodes,
|
|
17
|
+
wallet,
|
|
18
|
+
token,
|
|
19
|
+
decimals=decimals,
|
|
20
|
+
timeout=TIMEOUT_BALANCE,
|
|
21
|
+
proxies=proxies,
|
|
22
|
+
round_ndigits=round_ndigits,
|
|
23
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_eth import erc20, rpc
|
|
4
|
+
from mm_std import Ok, Result
|
|
5
|
+
|
|
6
|
+
from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_balance(
|
|
10
|
+
nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
|
|
11
|
+
) -> Result[Decimal]:
|
|
12
|
+
if token is not None:
|
|
13
|
+
res = erc20.get_balance(
|
|
14
|
+
nodes,
|
|
15
|
+
token,
|
|
16
|
+
wallet,
|
|
17
|
+
proxies=proxies,
|
|
18
|
+
attempts=RETRIES_BALANCE,
|
|
19
|
+
timeout=TIMEOUT_BALANCE,
|
|
20
|
+
)
|
|
21
|
+
else:
|
|
22
|
+
res = rpc.eth_get_balance(nodes, wallet, proxies=proxies, attempts=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE)
|
|
23
|
+
return res.and_then(lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
|
|
27
|
+
return erc20.get_decimals(nodes, token_address, timeout=TIMEOUT_DECIMALS, proxies=proxies, attempts=RETRIES_DECIMALS)
|