mm-balance 0.1.12__tar.gz → 0.1.14__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.
Files changed (30) hide show
  1. {mm_balance-0.1.12 → mm_balance-0.1.14}/PKG-INFO +3 -2
  2. {mm_balance-0.1.12 → mm_balance-0.1.14}/pyproject.toml +5 -4
  3. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/balances.py +27 -23
  4. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/cli.py +10 -3
  5. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/config/example.yml +11 -0
  6. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/config.py +39 -39
  7. mm_balance-0.1.14/src/mm_balance/constants.py +69 -0
  8. mm_balance-0.1.14/src/mm_balance/output.py +109 -0
  9. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/price.py +5 -23
  10. mm_balance-0.1.14/src/mm_balance/rpc/aptos.py +23 -0
  11. mm_balance-0.1.14/src/mm_balance/rpc/evm.py +27 -0
  12. mm_balance-0.1.14/src/mm_balance/rpc/solana.py +26 -0
  13. mm_balance-0.1.14/src/mm_balance/token_decimals.py +55 -0
  14. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/total.py +18 -8
  15. mm_balance-0.1.14/src/mm_balance/utils.py +10 -0
  16. {mm_balance-0.1.12 → mm_balance-0.1.14}/uv.lock +78 -64
  17. mm_balance-0.1.12/src/mm_balance/constants.py +0 -46
  18. mm_balance-0.1.12/src/mm_balance/output.py +0 -69
  19. mm_balance-0.1.12/src/mm_balance/rpc/eth.py +0 -31
  20. mm_balance-0.1.12/src/mm_balance/rpc/solana.py +0 -26
  21. mm_balance-0.1.12/src/mm_balance/token_decimals.py +0 -37
  22. {mm_balance-0.1.12 → mm_balance-0.1.14}/.gitignore +0 -0
  23. {mm_balance-0.1.12 → mm_balance-0.1.14}/README.md +0 -0
  24. {mm_balance-0.1.12 → mm_balance-0.1.14}/justfile +0 -0
  25. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/__init__.py +0 -0
  26. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/rpc/__init__.py +0 -0
  27. {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/rpc/btc.py +0 -0
  28. {mm_balance-0.1.12 → mm_balance-0.1.14}/tests/__init__.py +0 -0
  29. {mm_balance-0.1.12 → mm_balance-0.1.14}/tests/conftest.py +0 -0
  30. {mm_balance-0.1.12 → mm_balance-0.1.14}/tests/test_dummy.py +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mm-balance
3
- Version: 0.1.12
3
+ Version: 0.1.14
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
- Requires-Dist: mm-solana==0.1.4
8
+ Requires-Dist: mm-solana==0.1.5
8
9
  Requires-Dist: typer>=0.12.5
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "mm-balance"
3
- version = "0.1.12"
3
+ version = "0.1.14"
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
- "mm-solana==0.1.4",
9
+ "mm-solana==0.1.5",
10
+ "mm-aptos==0.1.2",
10
11
  "typer>=0.12.5",
11
12
  ]
12
13
  [project.scripts]
@@ -22,10 +23,10 @@ dev-dependencies = [
22
23
  "pytest-xdist~=3.6.1",
23
24
  "pytest-httpserver~=1.1.0",
24
25
  "coverage~=7.6.3",
25
- "ruff~=0.6.9",
26
+ "ruff~=0.7.0",
26
27
  "pip-audit~=2.7.0",
27
28
  "bandit~=1.7.10",
28
- "mypy~=1.12.0",
29
+ "mypy~=1.12.1",
29
30
  "types-python-dateutil~=2.9.0.20241003",
30
31
  "types-PyYAML~=6.0.12.20240917",
31
32
  ]
@@ -8,8 +8,8 @@ from rich.progress import TaskID
8
8
 
9
9
  from mm_balance import output
10
10
  from mm_balance.config import Config
11
- from mm_balance.constants import Network
12
- from mm_balance.rpc import btc, eth, solana
11
+ from mm_balance.constants import NETWORK_APTOS, NETWORK_BITCOIN, NETWORK_SOLANA, Network
12
+ from mm_balance.rpc import aptos, btc, evm, solana
13
13
  from mm_balance.token_decimals import TokenDecimals
14
14
 
15
15
 
@@ -23,7 +23,7 @@ class Balances:
23
23
  def __init__(self, config: Config, token_decimals: TokenDecimals):
24
24
  self.config = config
25
25
  self.token_decimals = token_decimals
26
- self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in Network}
26
+ self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in config.networks()}
27
27
  self.progress_bar = output.create_progress_bar()
28
28
  self.progress_bar_task: dict[Network, TaskID] = {}
29
29
 
@@ -31,17 +31,17 @@ class Balances:
31
31
  task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses]
32
32
  self.tasks[group.network].extend(task_list)
33
33
 
34
- for network in Network:
34
+ for network in config.networks():
35
35
  if self.tasks[network]:
36
36
  self.progress_bar_task[network] = output.create_progress_task(
37
- self.progress_bar, network.value, len(self.tasks[network])
37
+ self.progress_bar, network, len(self.tasks[network])
38
38
  )
39
39
 
40
40
  def process(self) -> None:
41
41
  with self.progress_bar:
42
42
  job = ConcurrentTasks(max_workers=10)
43
- for network in Network:
44
- job.add_task(network.value, self._process_network, args=(network,))
43
+ for network in self.config.networks():
44
+ job.add_task(network, self._process_network, args=(network,))
45
45
  job.execute()
46
46
 
47
47
  def _process_network(self, network: Network) -> None:
@@ -49,6 +49,7 @@ class Balances:
49
49
  for idx, task in enumerate(self.tasks[network]):
50
50
  job.add_task(str(idx), self._get_balance, args=(network, task.address, task.token_address))
51
51
  job.execute()
52
+ # TODO: print job.exceptions if present
52
53
  for idx, _task in enumerate(self.tasks[network]):
53
54
  self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
54
55
 
@@ -56,23 +57,18 @@ class Balances:
56
57
  nodes = self.config.nodes[network]
57
58
  round_ndigits = self.config.round_ndigits
58
59
  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)
60
+ token_decimals = self.token_decimals[network][token_address]
73
61
 
74
- case _:
75
- raise ValueError
62
+ if network.is_evm_network():
63
+ res = evm.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
64
+ elif network == NETWORK_BITCOIN:
65
+ res = btc.get_balance(wallet_address, proxies, round_ndigits)
66
+ elif network == NETWORK_APTOS:
67
+ res = aptos.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
68
+ elif network == NETWORK_SOLANA:
69
+ res = solana.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
70
+ else:
71
+ raise ValueError(f"Unsupported network: {network}")
76
72
 
77
73
  self.progress_bar.update(self.progress_bar_task[network], advance=1)
78
74
  return res
@@ -80,3 +76,11 @@ class Balances:
80
76
  def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
81
77
  # TODO: can we get network by group_index?
82
78
  return [b for b in self.tasks[network] if b.group_index == group_index]
79
+
80
+ def get_errors(self) -> list[Balance]:
81
+ result = []
82
+ for network in self.tasks:
83
+ for task in self.tasks[network]:
84
+ if task.balance is not None and task.balance.is_err():
85
+ result.append(task)
86
+ return result
@@ -8,7 +8,7 @@ import typer
8
8
  from mm_balance import output
9
9
  from mm_balance.balances import Balances
10
10
  from mm_balance.config import Config
11
- from mm_balance.constants import Network
11
+ from mm_balance.constants import NETWORKS
12
12
  from mm_balance.price import Prices, get_prices
13
13
  from mm_balance.token_decimals import get_token_decimals
14
14
 
@@ -24,7 +24,7 @@ def example_callback(value: bool) -> None:
24
24
 
25
25
  def networks_callback(value: bool) -> None:
26
26
  if value:
27
- for network in Network:
27
+ for network in NETWORKS:
28
28
  typer.echo(network)
29
29
  raise typer.Exit
30
30
 
@@ -42,15 +42,22 @@ def cli(
42
42
  zip_password = getpass.getpass("zip password")
43
43
  config = Config.read_config(config_path, zip_password=zip_password)
44
44
 
45
+ if config.print_debug:
46
+ output.print_nodes(config)
47
+
48
+ token_decimals = get_token_decimals(config)
49
+ if config.print_debug:
50
+ output.print_token_decimals(token_decimals)
51
+
45
52
  prices = get_prices(config) if config.price else Prices()
46
53
  output.print_prices(config, prices)
47
54
 
48
- token_decimals = get_token_decimals(config)
49
55
  balances = Balances(config, token_decimals)
50
56
  balances.process()
51
57
 
52
58
  output.print_groups(balances, config, prices)
53
59
  output.print_total(config, balances, prices)
60
+ output.print_errors(config, balances)
54
61
 
55
62
 
56
63
  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: |
@@ -60,3 +68,6 @@ addresses:
60
68
  #- http://123.123.123.124
61
69
  #round_ndigits: 4
62
70
  #price: yes
71
+ #skip_empty: no
72
+ #print_debug: no
73
+ #format_number_separator: ","
@@ -1,26 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from decimal import Decimal
4
+ from pathlib import Path
4
5
  from typing import Self
5
6
 
6
7
  import pydash
7
8
  from mm_std import BaseConfig, PrintFormat, fatal, hr
8
9
  from pydantic import Field, field_validator, model_validator
9
10
 
10
- from mm_balance.constants import (
11
- DEFAULT_ARBITRUM_ONE_NODES,
12
- DEFAULT_ETHEREUM_NODES,
13
- DEFAULT_SOLANA_NODES,
14
- TOKEN_ADDRESS,
15
- Network,
16
- )
11
+ from mm_balance.constants import DEFAULT_NODES, TOKEN_ADDRESS, Network
17
12
 
18
13
 
19
14
  class Group(BaseConfig):
20
15
  comment: str = ""
21
16
  ticker: str
22
17
  network: Network
23
- 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
24
20
  coingecko_id: str | None = None
25
21
  addresses: list[str] = Field(default_factory=list)
26
22
  share: Decimal = Decimal(1)
@@ -30,11 +26,11 @@ class Group(BaseConfig):
30
26
  result = self.ticker
31
27
  if self.comment:
32
28
  result += " / " + self.comment
33
- result += " / " + self.network.value
29
+ result += " / " + self.network
34
30
  return result
35
31
 
36
32
  @field_validator("ticker", mode="after")
37
- def coin_validator(cls, v: str) -> str:
33
+ def ticker_validator(cls, v: str) -> str:
38
34
  return v.upper()
39
35
 
40
36
  @field_validator("addresses", mode="before")
@@ -51,7 +47,7 @@ class Group(BaseConfig):
51
47
  def final_validator(self) -> Self:
52
48
  if self.token_address is None:
53
49
  self.token_address = detect_token_address(self.ticker, self.network)
54
- 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():
55
51
  self.token_address = self.token_address.lower()
56
52
  return self
57
53
 
@@ -63,7 +59,7 @@ class Group(BaseConfig):
63
59
  else:
64
60
  # TODO: check address is valid
65
61
  addresses.append(address)
66
- self.addresses = addresses
62
+ self.addresses = pydash.uniq(process_file_addresses(addresses))
67
63
 
68
64
 
69
65
  class AddressGroup(BaseConfig):
@@ -85,12 +81,17 @@ class Config(BaseConfig):
85
81
  nodes: dict[Network, list[str]] = Field(default_factory=dict)
86
82
  print_format: PrintFormat = PrintFormat.TABLE
87
83
  price: bool = True
88
-
89
- workers: dict[Network, int] = {network: 5 for network in Network}
84
+ skip_empty: bool = False # don't print the address with an empty balance
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)
90
88
 
91
89
  def has_share(self) -> bool:
92
90
  return any(g.share != Decimal(1) for g in self.groups)
93
91
 
92
+ def networks(self) -> list[Network]:
93
+ return pydash.uniq([group.network for group in self.groups])
94
+
94
95
  @model_validator(mode="after")
95
96
  def final_validator(self) -> Self:
96
97
  # load from proxies_url
@@ -102,36 +103,21 @@ class Config(BaseConfig):
102
103
  group.process_addresses(self.addresses)
103
104
 
104
105
  # load default rpc nodes
105
- if Network.BITCOIN not in self.nodes:
106
- self.nodes[Network.BITCOIN] = []
107
- if Network.ETHEREUM not in self.nodes:
108
- self.nodes[Network.ETHEREUM] = DEFAULT_ETHEREUM_NODES
109
- if Network.ARBITRUM_ONE not in self.nodes:
110
- self.nodes[Network.ARBITRUM_ONE] = DEFAULT_ARBITRUM_ONE_NODES
111
- if Network.OP_MAINNET not in self.nodes:
112
- self.nodes[Network.OP_MAINNET] = DEFAULT_ARBITRUM_ONE_NODES
113
- if Network.SOLANA not in self.nodes:
114
- self.nodes[Network.SOLANA] = DEFAULT_SOLANA_NODES
115
-
116
- return self
106
+ for network in self.networks():
107
+ if network not in self.nodes:
108
+ self.nodes[network] = DEFAULT_NODES[network]
117
109
 
110
+ # load default workers
111
+ for network in self.networks():
112
+ if network not in self.workers:
113
+ self.workers[network] = 5
118
114
 
119
- # def detect_network(coin: str) -> Network:
120
- #
121
- # # coin = coin.lower()
122
- # # if coin == "btc":
123
- # # return Network.BTC
124
- # # if coin == "eth":
125
- # # return Network.ETH
126
- # # if coin == "sol":
127
- # # return Network.SOL
128
- # # return Network.ETH
129
- # # # TODO: raise ValueError(f"can't get network for the coin: {coin}")
115
+ return self
130
116
 
131
117
 
132
- def detect_token_address(coin: str, network: Network) -> str | None:
118
+ def detect_token_address(ticker: str, network: Network) -> str | None:
133
119
  if network in TOKEN_ADDRESS:
134
- return TOKEN_ADDRESS[network].get(coin)
120
+ return TOKEN_ADDRESS[network].get(ticker)
135
121
 
136
122
 
137
123
  def get_proxies(proxies_url: str) -> list[str]:
@@ -147,3 +133,17 @@ def get_proxies(proxies_url: str) -> list[str]:
147
133
 
148
134
  def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
149
135
  return pydash.find(address_groups, lambda g: g.name == name)
136
+
137
+
138
+ def process_file_addresses(addresses: list[str]) -> list[str]:
139
+ result = []
140
+ for address in addresses:
141
+ if address.startswith("file://"):
142
+ path = Path(address.removeprefix("file://"))
143
+ if path.is_file():
144
+ result.extend(path.read_text().strip().splitlines())
145
+ else:
146
+ fatal(f"File with addresses not found: {path}")
147
+ else:
148
+ result.append(address)
149
+ return result
@@ -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,109 @@
1
+ from decimal import Decimal
2
+
3
+ from mm_std import Err, Ok, fatal, 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.token_decimals import TokenDecimals
10
+ from mm_balance.total import Total
11
+ from mm_balance.utils import fnumber
12
+
13
+
14
+ def print_groups(balances: Balances, config: Config, prices: Prices) -> None:
15
+ for group_index, group in enumerate(config.groups):
16
+ group_balances = balances.get_group_balances(group_index, group.network)
17
+ _print_group(group, group_balances, config, prices)
18
+
19
+
20
+ def _print_group(group: Group, group_balances: list[Balances.Balance], config: Config, prices: Prices) -> None:
21
+ rows = []
22
+ balance_sum = Decimal(0)
23
+ usd_sum = Decimal(0)
24
+ for address_task in group_balances:
25
+ if isinstance(address_task.balance, Err):
26
+ row = [address_task.address, address_task.balance.err]
27
+ elif isinstance(address_task.balance, Ok):
28
+ balance = address_task.balance.ok
29
+ balance_sum += balance
30
+ if config.skip_empty and balance == Decimal(0):
31
+ continue
32
+ row = [address_task.address, fnumber(balance, config.format_number_separator)]
33
+ if config.price:
34
+ balance_usd = round(balance * prices[group.ticker], config.round_ndigits)
35
+ usd_sum += balance_usd
36
+ row.append(fnumber(balance_usd, config.format_number_separator, "$"))
37
+ else:
38
+ fatal("address_task is None!")
39
+ rows.append(row)
40
+
41
+ balance_sum_str = fnumber(round(balance_sum, config.round_ndigits), config.format_number_separator)
42
+ sum_row = ["sum", balance_sum_str]
43
+ if config.price:
44
+ usd_sum_str = fnumber(round(usd_sum, config.round_ndigits), config.format_number_separator, "$")
45
+ sum_row.append(usd_sum_str)
46
+ rows.append(sum_row)
47
+
48
+ if group.share < Decimal(1):
49
+ sum_share_str = fnumber(round(balance_sum * group.share, config.round_ndigits), config.format_number_separator)
50
+ sum_share_row = [f"sum_share, {group.share}", sum_share_str]
51
+ if config.price:
52
+ usd_sum_share_str = fnumber(round(usd_sum * group.share, config.round_ndigits), config.format_number_separator, "$")
53
+ sum_share_row.append(usd_sum_share_str)
54
+ rows.append(sum_share_row)
55
+
56
+ table_headers = ["address", "balance"]
57
+ if config.price:
58
+ table_headers += ["usd"]
59
+ print_table(group.name, table_headers, rows)
60
+
61
+
62
+ def print_nodes(config: Config) -> None:
63
+ rows = []
64
+ for network, nodes in config.nodes.items():
65
+ rows.append([network, "\n".join(nodes)])
66
+ print_table("Nodes", ["network", "nodes"], rows)
67
+
68
+
69
+ def print_token_decimals(token_decimals: TokenDecimals) -> None:
70
+ rows = []
71
+ for network, decimals in token_decimals.items():
72
+ rows.append([network, decimals])
73
+ print_table("Token Decimals", ["network", "decimals"], rows)
74
+
75
+
76
+ def print_prices(config: Config, prices: Prices) -> None:
77
+ if config.price:
78
+ rows = []
79
+ for ticker, price in prices.items():
80
+ rows.append([ticker, fnumber(round(price, config.round_ndigits), config.format_number_separator, "$")])
81
+ print_table("Prices", ["coin", "usd"], rows)
82
+
83
+
84
+ def print_total(config: Config, balances: Balances, prices: Prices) -> None:
85
+ total = Total.calc(balances, prices, config)
86
+ total.print()
87
+
88
+
89
+ def print_errors(config: Config, balances: Balances) -> None:
90
+ error_balances = balances.get_errors()
91
+ if not error_balances:
92
+ return
93
+ rows = []
94
+ for balance in error_balances:
95
+ group = config.groups[balance.group_index]
96
+ rows.append([group.ticker + " / " + group.network, balance.address, balance.balance.err]) # type: ignore[union-attr]
97
+ print_table("Errors", ["coin", "address", "error"], rows)
98
+
99
+
100
+ def create_progress_bar() -> Progress:
101
+ return Progress(
102
+ TextColumn("[progress.description]{task.description}"),
103
+ BarColumn(),
104
+ MofNCompleteColumn(),
105
+ )
106
+
107
+
108
+ def create_progress_task(progress: Progress, description: str, total: int) -> TaskID:
109
+ 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, 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.")
@@ -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)
@@ -0,0 +1,26 @@
1
+ from decimal import Decimal
2
+
3
+ from mm_solana import balance
4
+ from mm_solana import token as solana_token
5
+ from mm_std import Ok, Result
6
+
7
+ from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
8
+
9
+
10
+ def get_balance(
11
+ nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
12
+ ) -> Result[Decimal]:
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
+
22
+
23
+ def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
24
+ return solana_token.get_decimals_with_retries(
25
+ nodes, token_address, retries=RETRIES_DECIMALS, timeout=TIMEOUT_DECIMALS, proxies=proxies
26
+ )