mm-balance 0.1.9__tar.gz → 0.1.11__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 (29) hide show
  1. {mm_balance-0.1.9 → mm_balance-0.1.11}/PKG-INFO +2 -2
  2. {mm_balance-0.1.9 → mm_balance-0.1.11}/pyproject.toml +6 -5
  3. mm_balance-0.1.11/src/mm_balance/balances.py +169 -0
  4. {mm_balance-0.1.9 → mm_balance-0.1.11}/src/mm_balance/cli.py +5 -2
  5. {mm_balance-0.1.9 → mm_balance-0.1.11}/src/mm_balance/config/example.yml +7 -2
  6. mm_balance-0.1.11/src/mm_balance/config.py +146 -0
  7. mm_balance-0.1.9/src/mm_balance/types.py → mm_balance-0.1.11/src/mm_balance/constants.py +19 -6
  8. {mm_balance-0.1.9 → mm_balance-0.1.11}/src/mm_balance/output.py +2 -2
  9. mm_balance-0.1.11/src/mm_balance/price.py +55 -0
  10. mm_balance-0.1.11/src/mm_balance/rpc/btc.py +16 -0
  11. mm_balance-0.1.11/src/mm_balance/rpc/eth.py +31 -0
  12. mm_balance-0.1.11/src/mm_balance/rpc/solana.py +26 -0
  13. mm_balance-0.1.11/src/mm_balance/token_decimals.py +37 -0
  14. {mm_balance-0.1.9 → mm_balance-0.1.11}/src/mm_balance/total.py +1 -4
  15. {mm_balance-0.1.9 → mm_balance-0.1.11}/uv.lock +79 -63
  16. mm_balance-0.1.9/src/mm_balance/balances.py +0 -98
  17. mm_balance-0.1.9/src/mm_balance/config.py +0 -167
  18. mm_balance-0.1.9/src/mm_balance/price.py +0 -83
  19. mm_balance-0.1.9/src/mm_balance/rpc/btc.py +0 -20
  20. mm_balance-0.1.9/src/mm_balance/rpc/eth.py +0 -38
  21. mm_balance-0.1.9/src/mm_balance/rpc/solana.py +0 -19
  22. {mm_balance-0.1.9 → mm_balance-0.1.11}/.gitignore +0 -0
  23. {mm_balance-0.1.9 → mm_balance-0.1.11}/README.md +0 -0
  24. {mm_balance-0.1.9 → mm_balance-0.1.11}/justfile +0 -0
  25. {mm_balance-0.1.9 → mm_balance-0.1.11}/src/mm_balance/__init__.py +0 -0
  26. {mm_balance-0.1.9 → mm_balance-0.1.11}/src/mm_balance/rpc/__init__.py +0 -0
  27. {mm_balance-0.1.9 → mm_balance-0.1.11}/tests/__init__.py +0 -0
  28. {mm_balance-0.1.9 → mm_balance-0.1.11}/tests/conftest.py +0 -0
  29. {mm_balance-0.1.9 → mm_balance-0.1.11}/tests/test_dummy.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mm-balance
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: mm-btc==0.1.0
6
6
  Requires-Dist: mm-eth==0.1.3
7
- Requires-Dist: mm-solana==0.1.2
7
+ Requires-Dist: mm-solana==0.1.4
8
8
  Requires-Dist: typer>=0.12.5
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "mm-balance"
3
- version = "0.1.9"
3
+ version = "0.1.11"
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.2",
9
+ "mm-solana==0.1.4",
10
10
  "typer>=0.12.5",
11
11
  ]
12
12
  [project.scripts]
@@ -21,11 +21,11 @@ dev-dependencies = [
21
21
  "pytest~=8.3.3",
22
22
  "pytest-xdist~=3.6.1",
23
23
  "pytest-httpserver~=1.1.0",
24
- "coverage~=7.6.0",
24
+ "coverage~=7.6.3",
25
25
  "ruff~=0.6.9",
26
26
  "pip-audit~=2.7.0",
27
27
  "bandit~=1.7.10",
28
- "mypy~=1.11.2",
28
+ "mypy~=1.12.0",
29
29
  "types-python-dateutil~=2.9.0.20241003",
30
30
  "types-PyYAML~=6.0.12.20240917",
31
31
  ]
@@ -56,7 +56,8 @@ lint.ignore = [
56
56
  "A003", # builtin-attribute-shadowing
57
57
  # "B008", # function-call-argument-default
58
58
  "UP040", # non-pep695-type-alias
59
- "COM812"
59
+ "COM812",
60
+ "RUF012"
60
61
  ]
61
62
 
62
63
  [tool.bandit]
@@ -0,0 +1,169 @@
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
+ self.progress_bar_task[network] = output.create_progress_task(
36
+ self.progress_bar, network.value, len(self.tasks[network])
37
+ )
38
+
39
+ def process(self) -> None:
40
+ with self.progress_bar:
41
+ job = ConcurrentTasks(max_workers=10)
42
+ for network in Network:
43
+ job.add_task(network.value, self._process_network, args=(network,))
44
+ job.execute()
45
+
46
+ def _process_network(self, network: Network) -> None:
47
+ job = ConcurrentTasks(max_workers=self.config.workers[network])
48
+ for idx, task in enumerate(self.tasks[network]):
49
+ job.add_task(str(idx), self._get_balance, args=(network, task.address, task.token_address))
50
+ job.execute()
51
+ for idx, _task in enumerate(self.tasks[network]):
52
+ self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
53
+
54
+ def _get_balance(self, network: Network, wallet_address: str, token_address: str | None) -> Result[Decimal]:
55
+ nodes = self.config.nodes[network]
56
+ round_ndigits = self.config.round_ndigits
57
+ proxies = self.config.proxies
58
+ token_decimals = self.token_decimals[network][token_address] if token_address else -1
59
+ match network:
60
+ case Network.BTC:
61
+ res = btc.get_balance(wallet_address, proxies, round_ndigits)
62
+ case Network.ETH:
63
+ if token_address is None:
64
+ res = eth.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
65
+ else:
66
+ res = eth.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
67
+ case Network.SOL:
68
+ if token_address is None:
69
+ res = solana.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
70
+ else:
71
+ res = solana.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
72
+
73
+ case _:
74
+ raise ValueError
75
+
76
+ self.progress_bar.update(self.progress_bar_task[network], advance=1)
77
+ return res
78
+
79
+ def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
80
+ # TODO: can we get network by group_index?
81
+ return [b for b in self.tasks[network] if b.group_index == group_index]
82
+
83
+
84
+ # class Balances2(BaseModel):
85
+ # class Balance(BaseModel):
86
+ # group_index: int
87
+ # address: str
88
+ # token_address: str | None
89
+ # balance: Result[Decimal] | None = None
90
+ #
91
+ # config: Config
92
+ # token_decimals: TokenDecimals
93
+ # # separate balance tasks on networks
94
+ # btc: list[Balance]
95
+ # eth: list[Balance]
96
+ # sol: list[Balance]
97
+ #
98
+ # def network_tasks(self, network: Network) -> list[Balance]:
99
+ # if network == Network.BTC:
100
+ # return self.btc
101
+ # elif network == Network.ETH:
102
+ # return self.eth
103
+ # elif network == Network.SOL:
104
+ # return self.sol
105
+ # else:
106
+ # raise ValueError
107
+ #
108
+ # def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
109
+ # # TODO: can we get network by group_index?
110
+ # if network == Network.BTC:
111
+ # network_balances = self.btc
112
+ # elif network == Network.ETH:
113
+ # network_balances = self.eth
114
+ # elif network == Network.SOL:
115
+ # network_balances = self.sol
116
+ # else:
117
+ # raise ValueError
118
+ #
119
+ # return [b for b in network_balances if b.group_index == group_index]
120
+ #
121
+ # def process(self) -> None:
122
+ # progress = output.create_progress_bar()
123
+ # task_btc = output.create_progress_task(progress, "btc", len(self.btc))
124
+ # task_eth = output.create_progress_task(progress, "eth", len(self.eth))
125
+ # task_sol = output.create_progress_task(progress, "sol", len(self.sol))
126
+ # with progress:
127
+ # job = ConcurrentTasks()
128
+ # job.add_task("btc", self._process_btc, args=(progress, task_btc))
129
+ # job.add_task("eth", self._process_eth, args=(progress, task_eth))
130
+ # job.add_task("sol", self._process_sol, args=(progress, task_sol))
131
+ # job.execute()
132
+ #
133
+ # def _process_btc(self, progress: Progress, task_id: TaskID) -> None:
134
+ # job = ConcurrentTasks(max_workers=self.config.workers.btc)
135
+ # for idx, task in enumerate(self.btc):
136
+ # job.add_task(str(idx), btc.get_balance, args=(task.address, self.config, progress, task_id))
137
+ # job.execute()
138
+ # for idx, _task in enumerate(self.btc):
139
+ # self.btc[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
140
+ #
141
+ # def _process_eth(self, progress: Progress, task_id: TaskID) -> None:
142
+ # job = ConcurrentTasks(max_workers=self.config.workers.eth)
143
+ # for idx, task in enumerate(self.eth):
144
+ # job.add_task(str(idx), self._get_balance,
145
+ # args=(Network.ETH, task.address, task.token_address, self.config, progress, task_id))
146
+ # job.execute()
147
+ # for idx, _task in enumerate(self.eth):
148
+ # self.eth[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
149
+ #
150
+ # def _process_sol(self, progress: Progress, task_id: TaskID) -> None:
151
+ # job = ConcurrentTasks(max_workers=self.config.workers.sol)
152
+ # for idx, task in enumerate(self.sol):
153
+ # job.add_task(str(idx), solana.get_balance, args=(task.address, self.config, progress, task_id))
154
+ # job.execute()
155
+ # for idx, _task in enumerate(self.sol):
156
+ # self.sol[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
157
+ #
158
+ # @staticmethod
159
+ # def from_config(config: Config, token_decimals: TokenDecimals) -> Balances:
160
+ # tasks = Balances(config=config, btc=[], eth=[], sol=[], token_decimals=token_decimals)
161
+ # for idx, group in enumerate(config.groups):
162
+ # task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses] # noqa
163
+ # if group.network == Network.BTC:
164
+ # tasks.btc.extend(task_list)
165
+ # elif group.network == Network.ETH:
166
+ # tasks.eth.extend(task_list)
167
+ # elif group.network == Network.SOL:
168
+ # tasks.sol.extend(task_list)
169
+ # return tasks
@@ -9,6 +9,7 @@ from mm_balance import output
9
9
  from mm_balance.balances import Balances
10
10
  from mm_balance.config import Config
11
11
  from mm_balance.price import Prices, get_prices
12
+ from mm_balance.token_decimals import get_token_decimals
12
13
 
13
14
  app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
14
15
 
@@ -31,10 +32,12 @@ def cli(
31
32
  config = Config.read_config(config_path, zip_password=zip_password)
32
33
 
33
34
  prices = get_prices(config) if config.price else Prices()
34
- balances = Balances.from_config(config)
35
+ output.print_prices(config, prices)
36
+
37
+ token_decimals = get_token_decimals(config)
38
+ balances = Balances(config, token_decimals)
35
39
  balances.process()
36
40
 
37
- output.print_prices(config, prices)
38
41
  output.print_groups(balances, config, prices)
39
42
  output.print_total(config, balances, prices)
40
43
 
@@ -1,8 +1,13 @@
1
- groups:
1
+ coins:
2
2
  - coin: sol
3
3
  addresses:
4
4
  - 2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S # binance
5
5
 
6
+ - coin: usdt
7
+ network: sol
8
+ addresses:
9
+ - 2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S # binance
10
+
6
11
  - coin: btc
7
12
  comment: coldwallets
8
13
  addresses: |
@@ -48,4 +53,4 @@ addresses:
48
53
  #- http://123.123.123.123
49
54
  #- http://123.123.123.124
50
55
  #round_ndigits: 4
51
- #price: no
56
+ #price: yes
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+ from typing import Any, Self
5
+
6
+ import pydash
7
+ from mm_std import BaseConfig, PrintFormat, fatal, hr
8
+ from pydantic import Field, field_validator, model_validator
9
+
10
+ from mm_balance.constants import DEFAULT_ETH_NODES, DEFAULT_SOL_NODES, EthTokenAddress, Network, SolTokenAddress
11
+
12
+
13
+ class Group(BaseConfig):
14
+ comment: str = ""
15
+ coin: str
16
+ network: Network
17
+ token_address: str | None = None
18
+ coingecko_id: str | None = None
19
+ addresses: list[str] = Field(default_factory=list)
20
+ share: Decimal = Decimal(1)
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ result = self.coin
25
+ if self.comment:
26
+ result += " / " + self.comment
27
+ return result
28
+
29
+ @field_validator("coin", mode="after")
30
+ def coin_validator(cls, v: str) -> str:
31
+ return v.upper()
32
+
33
+ @field_validator("addresses", mode="before")
34
+ def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
35
+ return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
36
+
37
+ @model_validator(mode="before")
38
+ def before_all(cls, data: Any) -> Any:
39
+ if "network" not in data:
40
+ data["network"] = detect_network(data["coin"])
41
+ return data
42
+
43
+ @model_validator(mode="after")
44
+ def final_validator(self) -> Self:
45
+ if self.token_address is None:
46
+ self.token_address = detect_token_address(self.coin, self.network)
47
+ if self.token_address is not None and self.network is Network.ETH:
48
+ self.token_address = self.token_address.lower()
49
+ return self
50
+
51
+ def process_addresses(self, address_groups: list[AddressGroup]) -> None:
52
+ addresses: list[str] = []
53
+ for address in self.addresses:
54
+ if address_group := pydash.find(address_groups, lambda g: g.name == address): # noqa: B023
55
+ addresses.extend(address_group.addresses)
56
+ else:
57
+ # TODO: check address is valid
58
+ addresses.append(address)
59
+ self.addresses = addresses
60
+
61
+
62
+ class AddressGroup(BaseConfig):
63
+ name: str
64
+ addresses: list[str]
65
+
66
+ @field_validator("addresses", mode="before")
67
+ def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
68
+ return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
69
+
70
+
71
+ class Config(BaseConfig):
72
+ groups: list[Group] = Field(alias="coins")
73
+ addresses: list[AddressGroup] = Field(default_factory=list)
74
+
75
+ proxies_url: str | None = None
76
+ proxies: list[str] = Field(default_factory=list)
77
+ round_ndigits: int = 4
78
+ nodes: dict[Network, list[str]] = Field(default_factory=dict)
79
+ print_format: PrintFormat = PrintFormat.TABLE
80
+ price: bool = True
81
+
82
+ workers: dict[Network, int] = {network: 5 for network in Network}
83
+
84
+ def has_share(self) -> bool:
85
+ return any(g.share != Decimal(1) for g in self.groups)
86
+
87
+ @model_validator(mode="after")
88
+ def final_validator(self) -> Self:
89
+ # load from proxies_url
90
+ if self.proxies_url is not None:
91
+ self.proxies = get_proxies(self.proxies_url)
92
+
93
+ # load addresses from address_group
94
+ for group in self.groups:
95
+ group.process_addresses(self.addresses)
96
+
97
+ # load default rpc nodes
98
+ if Network.BTC not in self.nodes:
99
+ self.nodes[Network.BTC] = []
100
+ if Network.ETH not in self.nodes:
101
+ self.nodes[Network.ETH] = DEFAULT_ETH_NODES
102
+ if Network.SOL not in self.nodes:
103
+ self.nodes[Network.SOL] = DEFAULT_SOL_NODES
104
+
105
+ return self
106
+
107
+
108
+ def detect_network(coin: str) -> Network:
109
+ coin = coin.lower()
110
+ if coin == "btc":
111
+ return Network.BTC
112
+ if coin == "eth":
113
+ return Network.ETH
114
+ if coin == "sol":
115
+ return Network.SOL
116
+ return Network.ETH
117
+ # TODO: raise ValueError(f"can't get network for the coin: {coin}")
118
+
119
+
120
+ def detect_token_address(coin: str, network: str) -> str | None:
121
+ if network == Network.ETH.lower():
122
+ if coin.lower() == "usdt":
123
+ return EthTokenAddress.USDT
124
+ if coin.lower() == "usdc":
125
+ return EthTokenAddress.USDC
126
+
127
+ if network == Network.SOL.lower():
128
+ if coin.lower() == "usdt":
129
+ return SolTokenAddress.USDT
130
+ if coin.lower() == "usdc":
131
+ return SolTokenAddress.USDC
132
+
133
+
134
+ def get_proxies(proxies_url: str) -> list[str]:
135
+ try:
136
+ res = hr(proxies_url)
137
+ if res.is_error():
138
+ fatal(f"Can't get proxies: {res.error}")
139
+ proxies = [p.strip() for p in res.body.splitlines() if p.strip()]
140
+ return pydash.uniq(proxies)
141
+ except Exception as err:
142
+ fatal(f"Can't get proxies: {err}")
143
+
144
+
145
+ def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
146
+ return pydash.find(address_groups, lambda g: g.name == name)
@@ -2,6 +2,25 @@ from __future__ import annotations
2
2
 
3
3
  from enum import Enum, unique
4
4
 
5
+ RETRIES_BALANCE = 5
6
+ RETRIES_DECIMALS = 5
7
+ RETRIES_COINGECKO_PRICES = 5
8
+ TIMEOUT_BALANCE = 5
9
+ TIMEOUT_DECIMALS = 5
10
+
11
+
12
+ @unique
13
+ class EthTokenAddress(str, Enum):
14
+ USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7"
15
+ USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
16
+
17
+
18
+ @unique
19
+ class SolTokenAddress(str, Enum):
20
+ USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
21
+ USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
22
+
23
+
5
24
  DEFAULT_ETH_NODES = ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"]
6
25
  DEFAULT_SOL_NODES = ["https://api.mainnet-beta.solana.com"]
7
26
 
@@ -24,9 +43,3 @@ class Network(str, Enum):
24
43
  BTC = "btc"
25
44
  ETH = "eth"
26
45
  SOL = "sol"
27
-
28
-
29
- @unique
30
- class EthTokenAddress(str, Enum):
31
- USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7"
32
- USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
@@ -4,7 +4,7 @@ from mm_std import Ok, print_table
4
4
  from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskID, TextColumn
5
5
 
6
6
  from mm_balance.balances import Balances
7
- from mm_balance.config import Config
7
+ from mm_balance.config import Config, Group
8
8
  from mm_balance.price import Prices
9
9
  from mm_balance.total import Total
10
10
 
@@ -15,7 +15,7 @@ def print_groups(balances: Balances, config: Config, prices: Prices) -> None:
15
15
  _print_group(group, group_balances, config, prices)
16
16
 
17
17
 
18
- def _print_group(group: Config.Group, group_balances: list[Balances.Balance], config: Config, prices: Prices) -> None:
18
+ def _print_group(group: Group, group_balances: list[Balances.Balance], config: Config, prices: Prices) -> None:
19
19
  rows = []
20
20
  balance_sum = Decimal(0)
21
21
  usd_sum = Decimal(0)
@@ -0,0 +1,55 @@
1
+ from decimal import Decimal
2
+
3
+ import pydash
4
+ from mm_std import fatal, hr
5
+ from mm_std.random_ import random_str_choice
6
+
7
+ from mm_balance.config import Config, Group
8
+ from mm_balance.constants import RETRIES_COINGECKO_PRICES, EthTokenAddress, Network
9
+
10
+
11
+ class Prices(dict[str, Decimal]):
12
+ """
13
+ A Prices class representing a mapping from coin names to their prices.
14
+
15
+ Inherits from:
16
+ Dict[str, Decimal]: A dictionary with coin names as keys and their prices as Decimal values.
17
+ """
18
+
19
+
20
+ def get_prices(config: Config) -> Prices:
21
+ result = Prices()
22
+
23
+ coins = pydash.uniq([group.coin for group in config.groups])
24
+ coingecko_ids = pydash.uniq([get_coingecko_id(group) for group in config.groups])
25
+
26
+ url = f"https://api.coingecko.com/api/v3/simple/price?ids={",".join(coingecko_ids)}&vs_currencies=usd"
27
+ for _ in range(RETRIES_COINGECKO_PRICES):
28
+ res = hr(url, proxy=random_str_choice(config.proxies))
29
+ if res.code != 200:
30
+ continue
31
+
32
+ for idx, coin in enumerate(coins):
33
+ if coingecko_ids[idx] in res.json:
34
+ result[coin] = Decimal(str(pydash.get(res.json, f"{coingecko_ids[idx]}.usd")))
35
+ else:
36
+ fatal("Can't get price for {coin} from coingecko, coingecko_id={coingecko_ids[idx]}")
37
+
38
+ return result
39
+
40
+
41
+ def get_coingecko_id(group: Group) -> str:
42
+ if group.coingecko_id:
43
+ return group.coingecko_id
44
+ elif group.network is Network.BTC:
45
+ return "bitcoin"
46
+ elif group.network is Network.ETH and group.token_address is None:
47
+ return "ethereum"
48
+ elif group.coin.lower() == "usdt" or (group.token_address is not None and group.token_address == EthTokenAddress.USDT):
49
+ return "tether"
50
+ elif group.coin.lower() == "usdc" or (group.token_address is not None and group.token_address == EthTokenAddress.USDC):
51
+ return "usd-coin"
52
+ elif group.coin.lower() == "sol":
53
+ return "solana"
54
+
55
+ raise ValueError(f"can't get coingecko_id for {group.coin}")
@@ -0,0 +1,16 @@
1
+ from decimal import Decimal
2
+
3
+ from mm_btc.blockstream import BlockstreamClient
4
+ from mm_std import Ok, Result
5
+
6
+ from mm_balance.constants import RETRIES_BALANCE
7
+
8
+
9
+ def get_balance(address: str, proxies: list[str], round_ndigits: int) -> Result[Decimal]:
10
+ return (
11
+ BlockstreamClient(proxies=proxies, attempts=RETRIES_BALANCE)
12
+ .get_confirmed_balance(address)
13
+ .and_then(
14
+ lambda b: Ok(round(Decimal(b / 100_000_000), round_ndigits)),
15
+ )
16
+ )
@@ -0,0 +1,31 @@
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)
@@ -0,0 +1,26 @@
1
+ from decimal import Decimal
2
+
3
+ from mm_solana import balance, token
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 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
17
+ ) -> 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)))
21
+
22
+
23
+ def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
24
+ return token.get_decimals_with_retries(
25
+ nodes, token_address, retries=RETRIES_DECIMALS, timeout=TIMEOUT_DECIMALS, proxies=proxies
26
+ )
@@ -0,0 +1,37 @@
1
+ from mm_std import Err, fatal
2
+
3
+ from mm_balance.config import Config
4
+ from mm_balance.constants import Network
5
+ from mm_balance.rpc import eth, solana
6
+
7
+
8
+ class TokenDecimals(dict[Network, dict[str, int]]):
9
+ def __init__(self) -> None:
10
+ super().__init__()
11
+ for network in Network:
12
+ self[network] = {}
13
+
14
+
15
+ def get_token_decimals(config: Config) -> TokenDecimals:
16
+ result = TokenDecimals()
17
+
18
+ for group in config.groups:
19
+ if group.token_address is None or group.token_address in result[group.network]:
20
+ continue
21
+
22
+ nodes = config.nodes[group.network]
23
+ proxies = config.proxies
24
+
25
+ match group.network:
26
+ case Network.ETH:
27
+ decimals_res = eth.get_token_decimals(nodes, group.token_address, proxies)
28
+ case Network.SOL:
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.coin} / {group.token_address}, error={decimals_res.err}")
35
+ result[group.network][group.token_address] = decimals_res.ok
36
+
37
+ return result
@@ -7,15 +7,14 @@ from mm_std import Ok, PrintFormat, print_table
7
7
 
8
8
  from mm_balance.balances import Balances
9
9
  from mm_balance.config import Config
10
+ from mm_balance.constants import Coin
10
11
  from mm_balance.price import Prices
11
- from mm_balance.types import Coin
12
12
 
13
13
 
14
14
  @dataclass
15
15
  class Total:
16
16
  coins: dict[str, Decimal]
17
17
  coins_share: dict[str, Decimal]
18
- # usd_share: dict[str, Decimal] # all stablecoins have key 'usd'
19
18
  usd_sum: Decimal # sum of all coins in USD
20
19
  usd_sum_share: Decimal
21
20
 
@@ -29,7 +28,6 @@ class Total:
29
28
  def calc(cls, balances: Balances, prices: Prices, config: Config) -> Self:
30
29
  coins: dict[str, Decimal] = defaultdict(Decimal)
31
30
  coins_share: dict[str, Decimal] = defaultdict(Decimal)
32
- # usd_share: dict[str, Decimal] = defaultdict(Decimal)
33
31
  usd_sum = Decimal(0)
34
32
  usd_sum_share = Decimal(0)
35
33
 
@@ -37,7 +35,6 @@ class Total:
37
35
  stablecoin_sum_share = Decimal(0)
38
36
  for group_index, group in enumerate(config.groups):
39
37
  balance_sum = Decimal(0)
40
- # for address_task in [t for t in tasks.network_tasks(group.network) if t.group_index == group_index]:
41
38
  for address_task in balances.get_group_balances(group_index, group.network):
42
39
  if isinstance(address_task.balance, Ok):
43
40
  balance_sum += address_task.balance.ok