mm-balance 0.1.13__py3-none-any.whl → 0.1.15__py3-none-any.whl

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/cli.py CHANGED
@@ -4,13 +4,15 @@ import pkgutil
4
4
  from typing import Annotated
5
5
 
6
6
  import typer
7
+ from mm_std import PrintFormat, fatal
7
8
 
8
- from mm_balance import output
9
- from mm_balance.balances import Balances
10
9
  from mm_balance.config import Config
11
- from mm_balance.constants import Network
10
+ from mm_balance.constants import NETWORKS
11
+ from mm_balance.output.formats import json_format, table_format
12
12
  from mm_balance.price import Prices, get_prices
13
+ from mm_balance.result import create_balances_result
13
14
  from mm_balance.token_decimals import get_token_decimals
15
+ from mm_balance.workers import Workers
14
16
 
15
17
  app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
16
18
 
@@ -24,7 +26,7 @@ def example_callback(value: bool) -> None:
24
26
 
25
27
  def networks_callback(value: bool) -> None:
26
28
  if value:
27
- for network in Network:
29
+ for network in NETWORKS:
28
30
  typer.echo(network)
29
31
  raise typer.Exit
30
32
 
@@ -32,6 +34,10 @@ def networks_callback(value: bool) -> None:
32
34
  @app.command()
33
35
  def cli(
34
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,
35
41
  _example: Annotated[bool | None, typer.Option("--example", callback=example_callback, help="Print a config example.")] = None,
36
42
  _networks: Annotated[
37
43
  bool | None, typer.Option("--networks", callback=networks_callback, help="Print supported networks.")
@@ -42,16 +48,36 @@ def cli(
42
48
  zip_password = getpass.getpass("zip password")
43
49
  config = Config.read_config(config_path, zip_password=zip_password)
44
50
 
45
- prices = get_prices(config) if config.price else Prices()
46
- output.print_prices(config, prices)
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)
47
62
 
48
63
  token_decimals = get_token_decimals(config)
49
- balances = Balances(config, token_decimals)
50
- balances.process()
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()
51
73
 
52
- output.print_groups(balances, config, prices)
53
- output.print_total(config, balances, prices)
54
- output.print_errors(config, balances)
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")
55
81
 
56
82
 
57
83
  if __name__ == "__main__":
@@ -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: ","
mm_balance/config.py CHANGED
@@ -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.value
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 is Network.ETHEREUM:
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
- workers: dict[Network, int] = {network: 5 for network in Network}
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 Network:
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
 
mm_balance/constants.py CHANGED
@@ -1,6 +1,7 @@
1
- from __future__ import annotations
1
+ from typing import Any
2
2
 
3
- from enum import Enum, unique
3
+ from pydantic import GetCoreSchemaHandler
4
+ from pydantic_core import CoreSchema, core_schema
4
5
 
5
6
  RETRIES_BALANCE = 5
6
7
  RETRIES_DECIMALS = 5
@@ -9,29 +10,38 @@ TIMEOUT_BALANCE = 5
9
10
  TIMEOUT_DECIMALS = 5
10
11
 
11
12
 
12
- @unique
13
- class Network(str, Enum):
14
- ARBITRUM_ONE = "arbitrum-one"
15
- BITCOIN = "bitcoin"
16
- ETHEREUM = "ethereum"
17
- SOLANA = "solana"
18
- OP_MAINNET = "op-mainnet" # Optimism mainnet
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]
19
29
 
20
30
 
21
31
  TOKEN_ADDRESS: dict[Network, dict[str, str]] = {
22
- Network.ETHEREUM: {
32
+ NETWORK_ETHEREUM: {
23
33
  "USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
24
34
  "USDC": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
25
35
  },
26
- Network.SOLANA: {
36
+ NETWORK_SOLANA: {
27
37
  "USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
28
38
  "USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
29
39
  },
30
- Network.ARBITRUM_ONE: {
40
+ NETWORK_ARBITRUM_ONE: {
31
41
  "USDT": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
32
42
  "USDC": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
33
43
  },
34
- Network.OP_MAINNET: {
44
+ NETWORK_OP_MAINNET: {
35
45
  "USDT": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
36
46
  "USDC": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
37
47
  },
@@ -43,14 +53,17 @@ TICKER_TO_COINGECKO_ID = {
43
53
  "USDT": "tether",
44
54
  "USDC": "usd-coin",
45
55
  "SOL": "solana",
56
+ "APT": "aptos",
57
+ "POL": "matic-network",
46
58
  }
47
59
 
48
60
  USD_STABLECOINS = ["USDT", "USDC"]
49
61
 
50
62
  DEFAULT_NODES: dict[Network, list[str]] = {
51
- Network.ARBITRUM_ONE: ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com"],
52
- Network.BITCOIN: [],
53
- Network.ETHEREUM: ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"],
54
- Network.SOLANA: ["https://api.mainnet-beta.solana.com"],
55
- Network.OP_MAINNET: ["https://mainnet.optimism.io", "https://optimism.llamarpc.com"],
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"],
56
69
  }
File without changes
@@ -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)
mm_balance/price.py CHANGED
@@ -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, Network
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
- elif group.network is Network.BITCOIN:
54
- return "bitcoin"
55
- elif group.network is Network.ETHEREUM and group.token_address is None:
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
- raise ValueError(f"can't get coingecko_id for {group.ticker}")
48
+ fatal(f"Can't get coingecko_id for {group.ticker}. Please add coingecko_id to the config.")
mm_balance/result.py ADDED
@@ -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
+ )
mm_balance/rpc/evm.py ADDED
@@ -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)
mm_balance/rpc/solana.py CHANGED
@@ -1,26 +1,26 @@
1
1
  from decimal import Decimal
2
2
 
3
- from mm_solana import balance, token
3
+ from mm_solana import balance
4
+ from mm_solana import token as solana_token
4
5
  from mm_std import Ok, Result
5
6
 
6
7
  from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
7
8
 
8
9
 
9
- def get_native_balance(nodes: list[str], address: str, proxies: list[str], round_ndigits: int) -> Result[Decimal]:
10
- return balance.get_balance_with_retries(
11
- nodes, address, retries=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE, proxies=proxies
12
- ).and_then(lambda b: Ok(round(Decimal(b / 1_000_000_000), round_ndigits)))
13
-
14
-
15
- def get_token_balance(
16
- nodes: list[str], wallet_address: str, token_address: str, decimals: int, proxies: list[str], round_ndigits: int
10
+ def get_balance(
11
+ nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
17
12
  ) -> Result[Decimal]:
18
- return token.get_balance_with_retries(
19
- nodes, wallet_address, token_address, retries=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE, proxies=proxies
20
- ).and_then(lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)))
13
+ if token is None:
14
+ res = balance.get_balance_with_retries(nodes, wallet, retries=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE, proxies=proxies)
15
+ else:
16
+ res = solana_token.get_balance_with_retries(
17
+ nodes, wallet, token, retries=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE, proxies=proxies
18
+ )
19
+
20
+ return res.and_then(lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)))
21
21
 
22
22
 
23
23
  def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
24
- return token.get_decimals_with_retries(
24
+ return solana_token.get_decimals_with_retries(
25
25
  nodes, token_address, retries=RETRIES_DECIMALS, timeout=TIMEOUT_DECIMALS, proxies=proxies
26
26
  )
@@ -1,37 +1,55 @@
1
1
  from mm_std import Err, fatal
2
2
 
3
3
  from mm_balance.config import Config
4
- from mm_balance.constants import Network
5
- from mm_balance.rpc import eth, solana
4
+ from mm_balance.constants import NETWORK_APTOS, NETWORK_BITCOIN, NETWORK_SOLANA, Network
5
+ from mm_balance.rpc import evm, solana
6
6
 
7
7
 
8
- class TokenDecimals(dict[Network, dict[str, int]]):
9
- def __init__(self) -> None:
8
+ class TokenDecimals(dict[Network, dict[str | None, int]]): # {network: {None: 18}} -- None is for native token, ex. ETH
9
+ def __init__(self, networks: list[Network]) -> None:
10
10
  super().__init__()
11
- for network in Network:
11
+ for network in networks:
12
12
  self[network] = {}
13
13
 
14
14
 
15
15
  def get_token_decimals(config: Config) -> TokenDecimals:
16
- result = TokenDecimals()
16
+ result = TokenDecimals(config.networks())
17
+ proxies = config.proxies
17
18
 
18
19
  for group in config.groups:
19
- if group.token_address is None or group.token_address in result[group.network]:
20
+ # token_decimals is already known
21
+ if group.token_decimals is not None:
22
+ result[group.network][group.token_address] = group.token_decimals
20
23
  continue
21
24
 
25
+ # get token_decimals for known native tokens
26
+ if group.token_address is None:
27
+ if group.network.is_evm_network():
28
+ result[group.network][None] = 18
29
+ elif group.network == NETWORK_SOLANA:
30
+ result[group.network][None] = 9
31
+ elif group.network == NETWORK_BITCOIN:
32
+ result[group.network][None] = 8
33
+ elif group.network == NETWORK_APTOS:
34
+ result[group.network][None] = 8
35
+ else:
36
+ fatal(f"Can't get token decimals for native token on network: {group.network}")
37
+ continue
38
+
39
+ # get token_decimals via RPC
40
+ # TODO: group.token_address must be in normalized form, otherwise it can be different for the same token
41
+ if group.token_address in result[group.network]:
42
+ continue # don't request for a token_decimals twice
43
+
22
44
  nodes = config.nodes[group.network]
23
- proxies = config.proxies
24
-
25
- match group.network:
26
- case Network.ETHEREUM:
27
- decimals_res = eth.get_token_decimals(nodes, group.token_address, proxies)
28
- case Network.SOLANA:
29
- decimals_res = solana.get_token_decimals(nodes, group.token_address, proxies)
30
- case _:
31
- raise ValueError(f"unsupported network: {group.network}. Cant get token decimals for {group.token_address}")
32
-
33
- if isinstance(decimals_res, Err):
34
- fatal(f"can't get decimals for token {group.ticker} / {group.token_address}, error={decimals_res.err}")
35
- result[group.network][group.token_address] = decimals_res.ok
45
+ if group.network.is_evm_network():
46
+ res = evm.get_token_decimals(nodes, group.token_address, proxies)
47
+ elif group.network == NETWORK_SOLANA:
48
+ res = solana.get_token_decimals(nodes, group.token_address, proxies)
49
+ else:
50
+ fatal(f"unsupported network: {group.network}. Cant get token decimals for {group.token_address}")
51
+ if isinstance(res, Err):
52
+ fatal(f"can't get decimals for token {group.ticker} / {group.token_address}, error={res.err}")
53
+ result[group.network][group.token_address] = res.ok
36
54
 
37
55
  return result
mm_balance/utils.py ADDED
@@ -0,0 +1,10 @@
1
+ from decimal import Decimal
2
+
3
+
4
+ def fnumber(value: Decimal, separator: str, extra: str | None = None) -> str:
5
+ str_value = f"{value:,}".replace(",", separator)
6
+ if extra == "$":
7
+ return "$" + str_value
8
+ elif extra == "%":
9
+ return str_value + "%"
10
+ return str_value
mm_balance/workers.py ADDED
@@ -0,0 +1,84 @@
1
+ from dataclasses import dataclass
2
+ from decimal import Decimal
3
+
4
+ from mm_std import ConcurrentTasks, PrintFormat, Result
5
+ from rich.progress import TaskID
6
+
7
+ from mm_balance.config import Config
8
+ from mm_balance.constants import NETWORK_APTOS, NETWORK_BITCOIN, NETWORK_SOLANA, Network
9
+ from mm_balance.output import utils
10
+ from mm_balance.rpc import aptos, btc, evm, solana
11
+ from mm_balance.token_decimals import TokenDecimals
12
+
13
+
14
+ @dataclass
15
+ class Task:
16
+ group_index: int
17
+ wallet_address: str
18
+ token_address: str | None
19
+ balance: Result[Decimal] | None = None
20
+
21
+
22
+ class Workers:
23
+ def __init__(self, config: Config, token_decimals: TokenDecimals):
24
+ self.config = config
25
+ self.token_decimals = token_decimals
26
+ self.tasks: dict[Network, list[Task]] = {network: [] for network in config.networks()}
27
+ self.progress_bar = utils.create_progress_bar(config.print_format is not PrintFormat.TABLE)
28
+ self.progress_bar_task: dict[Network, TaskID] = {}
29
+
30
+ for idx, group in enumerate(config.groups):
31
+ task_list = [Task(group_index=idx, wallet_address=a, token_address=group.token_address) for a in group.addresses]
32
+ self.tasks[group.network].extend(task_list)
33
+
34
+ for network in config.networks():
35
+ if self.tasks[network]:
36
+ self.progress_bar_task[network] = utils.create_progress_task(self.progress_bar, network, len(self.tasks[network]))
37
+
38
+ def process(self) -> None:
39
+ with self.progress_bar:
40
+ job = ConcurrentTasks(max_workers=10)
41
+ for network in self.config.networks():
42
+ job.add_task(network, self._process_network, args=(network,))
43
+ job.execute()
44
+
45
+ def get_group_tasks(self, group_index: int, network: Network) -> list[Task]:
46
+ # TODO: can we get network by group_index?
47
+ return [b for b in self.tasks[network] if b.group_index == group_index]
48
+
49
+ def get_errors(self) -> list[Task]:
50
+ result = []
51
+ for network in self.tasks:
52
+ for task in self.tasks[network]:
53
+ if task.balance is not None and task.balance.is_err():
54
+ result.append(task)
55
+ return result
56
+
57
+ def _process_network(self, network: Network) -> None:
58
+ job = ConcurrentTasks(max_workers=self.config.workers[network])
59
+ for idx, task in enumerate(self.tasks[network]):
60
+ job.add_task(str(idx), self._get_balance, args=(network, task.wallet_address, task.token_address))
61
+ job.execute()
62
+ # TODO: print job.exceptions if present
63
+ for idx, _task in enumerate(self.tasks[network]):
64
+ self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
65
+
66
+ def _get_balance(self, network: Network, wallet_address: str, token_address: str | None) -> Result[Decimal]:
67
+ nodes = self.config.nodes[network]
68
+ round_ndigits = self.config.round_ndigits
69
+ proxies = self.config.proxies
70
+ token_decimals = self.token_decimals[network][token_address]
71
+
72
+ if network.is_evm_network():
73
+ res = evm.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
74
+ elif network == NETWORK_BITCOIN:
75
+ res = btc.get_balance(wallet_address, proxies, round_ndigits)
76
+ elif network == NETWORK_APTOS:
77
+ res = aptos.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
78
+ elif network == NETWORK_SOLANA:
79
+ res = solana.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
80
+ else:
81
+ raise ValueError(f"Unsupported network: {network}")
82
+
83
+ self.progress_bar.update(self.progress_bar_task[network], advance=1)
84
+ return res
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mm-balance
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Requires-Python: >=3.12
5
+ Requires-Dist: mm-aptos==0.1.2
5
6
  Requires-Dist: mm-btc==0.1.0
6
7
  Requires-Dist: mm-eth==0.1.3
7
8
  Requires-Dist: mm-solana==0.1.5
@@ -0,0 +1,23 @@
1
+ mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mm_balance/cli.py,sha256=NJ-1d03faDO_1V1Nd96b6b3Y40LhuewFxCvnw0SupuY,3064
3
+ mm_balance/config.py,sha256=5qOsU1JjWu2TbH1VgikK4oZpBcuPMqsOe8DOwMRtrWM,5230
4
+ mm_balance/constants.py,sha256=4C2BxYRY_i83Rba5NOXeLLeA0ZTWR9WD6BeyQwiCj_U,2335
5
+ mm_balance/price.py,sha256=f6fYhpPVqZE9hH22ypfYb9m-nfE41LeVIvksEvqmz54,1615
6
+ mm_balance/result.py,sha256=fG63HrHXWS3kBLHmefxKsRh9tj53dloamO_nv3xFboM,4576
7
+ mm_balance/token_decimals.py,sha256=WAz9wbvwjPJEcQnrLyPAf23eGp89t9oP_xBmaEecEG0,2348
8
+ mm_balance/utils.py,sha256=YjQMfyAaLiiprjt9CuNbDQSJrLbRoTkb2rurAb6bHDc,288
9
+ mm_balance/workers.py,sha256=PGhLVhYYVm2jIAzIiJQYby_UqW0xsRv-wQJURqOUYxg,3767
10
+ mm_balance/config/example.yml,sha256=QRUYEYKmq33dqHv4RIdlPkjMRjU-oZ8LMeoS4064sY4,1894
11
+ mm_balance/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ mm_balance/output/utils.py,sha256=Vp-6ev-9YIa3kGZxKzMdoJZQB_KLUaAdQGvgOxmZtrc,718
13
+ mm_balance/output/formats/json_format.py,sha256=Jz2b2-FRvCie0at2loTIdJHCeUMb_Nn6uI0QONVXZ4g,820
14
+ mm_balance/output/formats/table_format.py,sha256=zs2rgj2djYoDhG6i9tE_2GYYsn2HbAneH4VXfmJuDwI,4427
15
+ mm_balance/rpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ mm_balance/rpc/aptos.py,sha256=6hKGKTs2DPQMa4RmPXzfvjCFjPjN8wjJhRRI6HdI9GM,628
17
+ mm_balance/rpc/btc.py,sha256=ugp90H7YW0kiXIh98bQWk9mQTW20yE-jBiCpRvfoH-U,481
18
+ mm_balance/rpc/evm.py,sha256=ewlMmRrcXKlky3DPNbnUBTVwnvyw7N9iCZLsCX2V14w,1007
19
+ mm_balance/rpc/solana.py,sha256=Lwl5Otqy48g4LWfz9uN7_rkICKUIYJvf88PbIAnfMdc,1051
20
+ mm_balance-0.1.15.dist-info/METADATA,sha256=vioUnF6B0nxZ73wZe13jRQtRMTVpYup5brkzIOs_9Ds,229
21
+ mm_balance-0.1.15.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
22
+ mm_balance-0.1.15.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
23
+ mm_balance-0.1.15.dist-info/RECORD,,
mm_balance/balances.py DELETED
@@ -1,90 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from decimal import Decimal
4
-
5
- from mm_std import ConcurrentTasks, Result
6
- from pydantic import BaseModel
7
- from rich.progress import TaskID
8
-
9
- from mm_balance import output
10
- from mm_balance.config import Config
11
- from mm_balance.constants import Network
12
- from mm_balance.rpc import btc, eth, solana
13
- from mm_balance.token_decimals import TokenDecimals
14
-
15
-
16
- class Balances:
17
- class Balance(BaseModel):
18
- group_index: int
19
- address: str
20
- token_address: str | None
21
- balance: Result[Decimal] | None = None
22
-
23
- def __init__(self, config: Config, token_decimals: TokenDecimals):
24
- self.config = config
25
- self.token_decimals = token_decimals
26
- self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in Network}
27
- self.progress_bar = output.create_progress_bar()
28
- self.progress_bar_task: dict[Network, TaskID] = {}
29
-
30
- for idx, group in enumerate(config.groups):
31
- task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses]
32
- self.tasks[group.network].extend(task_list)
33
-
34
- for network in Network:
35
- if self.tasks[network]:
36
- self.progress_bar_task[network] = output.create_progress_task(
37
- self.progress_bar, network.value, len(self.tasks[network])
38
- )
39
-
40
- def process(self) -> None:
41
- with self.progress_bar:
42
- job = ConcurrentTasks(max_workers=10)
43
- for network in Network:
44
- job.add_task(network.value, self._process_network, args=(network,))
45
- job.execute()
46
-
47
- def _process_network(self, network: Network) -> None:
48
- job = ConcurrentTasks(max_workers=self.config.workers[network])
49
- for idx, task in enumerate(self.tasks[network]):
50
- job.add_task(str(idx), self._get_balance, args=(network, task.address, task.token_address))
51
- job.execute()
52
- for idx, _task in enumerate(self.tasks[network]):
53
- self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
54
-
55
- def _get_balance(self, network: Network, wallet_address: str, token_address: str | None) -> Result[Decimal]:
56
- nodes = self.config.nodes[network]
57
- round_ndigits = self.config.round_ndigits
58
- proxies = self.config.proxies
59
- token_decimals = self.token_decimals[network][token_address] if token_address else -1
60
- match network:
61
- case Network.BITCOIN:
62
- res = btc.get_balance(wallet_address, proxies, round_ndigits)
63
- case Network.ETHEREUM | Network.ARBITRUM_ONE | Network.OP_MAINNET:
64
- if token_address is None:
65
- res = eth.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
66
- else:
67
- res = eth.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
68
- case Network.SOLANA:
69
- if token_address is None:
70
- res = solana.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
71
- else:
72
- res = solana.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
73
-
74
- case _:
75
- raise ValueError
76
-
77
- self.progress_bar.update(self.progress_bar_task[network], advance=1)
78
- return res
79
-
80
- def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
81
- # TODO: can we get network by group_index?
82
- return [b for b in self.tasks[network] if b.group_index == group_index]
83
-
84
- def get_errors(self) -> list[Balance]:
85
- result = []
86
- for network in self.tasks:
87
- for task in self.tasks[network]:
88
- if task.balance is not None and task.balance.is_err():
89
- result.append(task)
90
- return result
mm_balance/output.py DELETED
@@ -1,82 +0,0 @@
1
- from decimal import Decimal
2
-
3
- from mm_std import Ok, print_table
4
- from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskID, TextColumn
5
-
6
- from mm_balance.balances import Balances
7
- from mm_balance.config import Config, Group
8
- from mm_balance.price import Prices
9
- from mm_balance.total import Total
10
-
11
-
12
- def print_groups(balances: Balances, config: Config, prices: Prices) -> None:
13
- for group_index, group in enumerate(config.groups):
14
- group_balances = balances.get_group_balances(group_index, group.network)
15
- _print_group(group, group_balances, config, prices)
16
-
17
-
18
- def _print_group(group: Group, group_balances: list[Balances.Balance], config: Config, prices: Prices) -> None:
19
- rows = []
20
- balance_sum = Decimal(0)
21
- usd_sum = Decimal(0)
22
- for address_task in group_balances:
23
- if config.skip_empty and isinstance(address_task.balance, Ok) and address_task.balance.ok == Decimal(0):
24
- continue
25
- row = [address_task.address, address_task.balance.ok_or_err()] # type: ignore[union-attr]
26
- if isinstance(address_task.balance, Ok):
27
- balance_sum += address_task.balance.ok
28
- if config.price:
29
- balance_usd = round(address_task.balance.ok * prices[group.ticker], config.round_ndigits)
30
- usd_sum += balance_usd
31
- row.append(f"${balance_usd}")
32
- rows.append(row)
33
-
34
- sum_row = ["sum", round(balance_sum, config.round_ndigits)]
35
- if config.price:
36
- sum_row.append(f"${round(usd_sum, config.round_ndigits)}")
37
- rows.append(sum_row)
38
-
39
- if group.share < Decimal(1):
40
- sum_share_row = [f"sum_share, {group.share}", round(balance_sum * group.share, config.round_ndigits)]
41
- if config.price:
42
- sum_share_row.append(f"${round(usd_sum * group.share, config.round_ndigits)}")
43
- rows.append(sum_share_row)
44
-
45
- table_headers = ["address", "balance"]
46
- if config.price:
47
- table_headers += ["usd"]
48
- print_table(group.name, table_headers, rows)
49
-
50
-
51
- def print_prices(config: Config, prices: Prices) -> None:
52
- if config.price:
53
- rows = [[k, round(v, config.round_ndigits)] for (k, v) in prices.items()]
54
- print_table("Prices", ["coin", "usd"], rows)
55
-
56
-
57
- def print_total(config: Config, balances: Balances, prices: Prices) -> None:
58
- total = Total.calc(balances, prices, config)
59
- total.print()
60
-
61
-
62
- def print_errors(config: Config, balances: Balances) -> None:
63
- error_balances = balances.get_errors()
64
- if not error_balances:
65
- return
66
- rows = []
67
- for balance in error_balances:
68
- group = config.groups[balance.group_index]
69
- rows.append([group.ticker + " / " + group.network, balance.address, balance.balance.err]) # type: ignore[union-attr]
70
- print_table("Errors", ["coin", "address", "error"], rows)
71
-
72
-
73
- def create_progress_bar() -> Progress:
74
- return Progress(
75
- TextColumn("[progress.description]{task.description}"),
76
- BarColumn(),
77
- MofNCompleteColumn(),
78
- )
79
-
80
-
81
- def create_progress_task(progress: Progress, description: str, total: int) -> TaskID:
82
- return progress.add_task("[green]" + description, total=total)
mm_balance/rpc/eth.py DELETED
@@ -1,31 +0,0 @@
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_native_balance(nodes: list[str], address: str, proxies: list[str], round_ndigits: int) -> Result[Decimal]:
10
- return rpc.eth_get_balance(nodes, address, proxies=proxies, attempts=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE).and_then(
11
- lambda b: Ok(round(Decimal(b / 10**18), round_ndigits)),
12
- )
13
-
14
-
15
- def get_token_balance(
16
- nodes: list[str], wallet_address: str, token_address: str, decimals: int, proxies: list[str], round_ndigits: int
17
- ) -> Result[Decimal]:
18
- return erc20.get_balance(
19
- nodes,
20
- token_address,
21
- wallet_address,
22
- proxies=proxies,
23
- attempts=RETRIES_BALANCE,
24
- timeout=TIMEOUT_BALANCE,
25
- ).and_then(
26
- lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)),
27
- )
28
-
29
-
30
- def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
31
- return erc20.get_decimals(nodes, token_address, timeout=TIMEOUT_DECIMALS, proxies=proxies, attempts=RETRIES_DECIMALS)
mm_balance/total.py DELETED
@@ -1,112 +0,0 @@
1
- from collections import defaultdict
2
- from dataclasses import dataclass
3
- from decimal import Decimal
4
- from typing import Self
5
-
6
- from mm_std import Ok, PrintFormat, print_table
7
-
8
- from mm_balance.balances import Balances
9
- from mm_balance.config import Config
10
- from mm_balance.constants import USD_STABLECOINS
11
- from mm_balance.price import Prices
12
-
13
-
14
- @dataclass
15
- class Total:
16
- coins: dict[str, Decimal]
17
- coins_share: dict[str, Decimal]
18
- usd_sum: Decimal # sum of all coins in USD
19
- usd_sum_share: Decimal
20
-
21
- stablecoin_sum: Decimal # sum of usd stablecoins: usdt, usdc
22
- stablecoin_sum_share: Decimal
23
-
24
- config: Config
25
- prices: Prices
26
-
27
- @classmethod
28
- def calc(cls, balances: Balances, prices: Prices, config: Config) -> Self:
29
- coins: dict[str, Decimal] = defaultdict(Decimal)
30
- coins_share: dict[str, Decimal] = defaultdict(Decimal)
31
- usd_sum = Decimal(0)
32
- usd_sum_share = Decimal(0)
33
-
34
- stablecoin_sum = Decimal(0)
35
- stablecoin_sum_share = Decimal(0)
36
- for group_index, group in enumerate(config.groups):
37
- balance_sum = Decimal(0)
38
- for address_task in balances.get_group_balances(group_index, group.network):
39
- if isinstance(address_task.balance, Ok):
40
- balance_sum += address_task.balance.ok
41
- if group.ticker in USD_STABLECOINS:
42
- stablecoin_sum += address_task.balance.ok
43
- stablecoin_sum_share += address_task.balance.ok * group.share
44
- if config.price:
45
- balance_usd = round(address_task.balance.ok * prices[group.ticker], config.round_ndigits)
46
- usd_sum += balance_usd
47
- usd_sum_share += group.share * balance_usd
48
-
49
- coins[group.ticker] += balance_sum
50
- coins_share[group.ticker] += round(balance_sum * group.share, config.round_ndigits)
51
- return cls(
52
- coins=coins,
53
- coins_share=coins_share,
54
- usd_sum=usd_sum,
55
- usd_sum_share=usd_sum_share,
56
- # usd_share=usd_share,
57
- stablecoin_sum=stablecoin_sum,
58
- stablecoin_sum_share=stablecoin_sum_share,
59
- config=config,
60
- prices=prices,
61
- )
62
-
63
- def print(self) -> None:
64
- if self.config.print_format == PrintFormat.TABLE:
65
- if self.config.price:
66
- self._print_total_total_with_price()
67
-
68
- if self.config.has_share():
69
- self._print_share_total_with_price()
70
- else:
71
- self._print_total_total_without_price()
72
-
73
- if self.config.has_share():
74
- self._print_share_total_without_price()
75
-
76
- def _print_total_total_with_price(self) -> None:
77
- if self.config.print_format == PrintFormat.TABLE:
78
- rows = []
79
- for key, value in self.coins.items():
80
- usd_value = round(value * self.prices[key], self.config.round_ndigits)
81
- if key in USD_STABLECOINS:
82
- usd_share = round(self.stablecoin_sum * 100 / self.usd_sum, self.config.round_ndigits)
83
- else:
84
- usd_share = round(usd_value * 100 / self.usd_sum, self.config.round_ndigits)
85
- rows.append([key, value, f"${usd_value}", f"{usd_share}%"])
86
- rows.append(["usd_sum", f"${self.usd_sum}"])
87
- print_table("Total", ["coin", "balance", "usd", "usd_share"], rows)
88
-
89
- def _print_total_total_without_price(self) -> None:
90
- if self.config.print_format == PrintFormat.TABLE:
91
- rows = []
92
- for key, value in self.coins.items():
93
- rows.append([key, value])
94
- print_table("Total", ["coin", "balance"], rows)
95
-
96
- def _print_share_total_with_price(self) -> None:
97
- rows = []
98
- for key, _ in self.coins.items():
99
- usd_value = round(self.coins_share[key] * self.prices[key], self.config.round_ndigits)
100
- if key in USD_STABLECOINS:
101
- usd_share = round(self.stablecoin_sum_share * 100 / self.usd_sum_share, self.config.round_ndigits)
102
- else:
103
- usd_share = round(usd_value * 100 / self.usd_sum_share, self.config.round_ndigits)
104
- rows.append([key, self.coins_share[key], f"${usd_value}", f"{usd_share}%"])
105
- rows.append(["usd_sum", f"${self.usd_sum_share}"])
106
- print_table("Total, share", ["coin", "balance", "usd", "usd_share"], rows)
107
-
108
- def _print_share_total_without_price(self) -> None:
109
- rows = []
110
- for key, _ in self.coins.items():
111
- rows.append([key, self.coins_share[key]])
112
- print_table("Total, share", ["coin", "balance"], rows)
@@ -1,18 +0,0 @@
1
- mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- mm_balance/balances.py,sha256=yk4gqfdBiBuq8SX4BzObBz_yYh3d6DJ-iDcvaSZFrFM,3939
3
- mm_balance/cli.py,sha256=B8YDx2ltqIu1jJoxLA514Y1A-bcrNPEN6nK73ZYIyZc,1742
4
- mm_balance/config.py,sha256=4NOp8CploUsDCYAJedBBEfgyGtREhcmmg3r2q2emeX4,4744
5
- mm_balance/constants.py,sha256=vtISLm13dAVaJEtZYAyNlvZp8LwiYoEsXjcMBmr1xzE,1632
6
- mm_balance/output.py,sha256=r2h3NQ5cZ9uWyGP0bfzYwVgS8TnNwj5zTcJbcY01zBo,3141
7
- mm_balance/price.py,sha256=wlClQNW03BSzq8gxQxH9f5LnwRFZBqeI-tfY9vtlSJE,2005
8
- mm_balance/token_decimals.py,sha256=iNhAEEPfg8MvjxBGnFdxh82pYg3v26w4s9xro2Emoi8,1307
9
- mm_balance/total.py,sha256=PERRR9m77LBgu30tUEdyfGdtmVDFa-dICq8S9zcH1eA,4716
10
- mm_balance/config/example.yml,sha256=oMg0bBygKFprcf3anW5nb0lkpl0Bc3cQ5eKTeEpiM_k,1498
11
- mm_balance/rpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- mm_balance/rpc/btc.py,sha256=ugp90H7YW0kiXIh98bQWk9mQTW20yE-jBiCpRvfoH-U,481
13
- mm_balance/rpc/eth.py,sha256=G7aYjTw6xJwcsAyIp9eVW8NRVDUGeTCpdbd-CqgqHyw,1167
14
- mm_balance/rpc/solana.py,sha256=vmCAaeQKwxg95qdSmUEjFS9bAPUJVx8irtWv6nlRBvU,1174
15
- mm_balance-0.1.13.dist-info/METADATA,sha256=pEBa6u5nBDTruCLZzIDBjSub0hk6jaLzZuWTvE5v9dQ,198
16
- mm_balance-0.1.13.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
- mm_balance-0.1.13.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
18
- mm_balance-0.1.13.dist-info/RECORD,,