mm-balance 0.1.9__py3-none-any.whl → 0.1.11__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/balances.py CHANGED
@@ -4,95 +4,166 @@ from decimal import Decimal
4
4
 
5
5
  from mm_std import ConcurrentTasks, Result
6
6
  from pydantic import BaseModel
7
- from rich.progress import Progress, TaskID
7
+ 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
11
12
  from mm_balance.rpc import btc, eth, solana
12
- from mm_balance.types import Network
13
+ from mm_balance.token_decimals import TokenDecimals
13
14
 
14
15
 
15
- class Balances(BaseModel):
16
+ class Balances:
16
17
  class Balance(BaseModel):
17
18
  group_index: int
18
19
  address: str
19
20
  token_address: str | None
20
21
  balance: Result[Decimal] | None = None
21
22
 
22
- config: Config
23
- # separate balance tasks on networks
24
- btc: list[Balance]
25
- eth: list[Balance]
26
- sol: list[Balance]
27
-
28
- def network_tasks(self, network: Network) -> list[Balance]:
29
- if network == Network.BTC:
30
- return self.btc
31
- elif network == Network.ETH:
32
- return self.eth
33
- elif network == Network.SOL:
34
- return self.sol
35
- else:
36
- raise ValueError
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] = {}
37
29
 
38
- def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
39
- # TODO: can we get network by group_index?
40
- if network == Network.BTC:
41
- network_balances = self.btc
42
- elif network == Network.ETH:
43
- network_balances = self.eth
44
- elif network == Network.SOL:
45
- network_balances = self.sol
46
- else:
47
- raise ValueError
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)
48
33
 
49
- return [b for b in network_balances if b.group_index == group_index]
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
+ )
50
38
 
51
39
  def process(self) -> None:
52
- progress = output.create_progress_bar()
53
- task_btc = output.create_progress_task(progress, "btc", len(self.btc))
54
- task_eth = output.create_progress_task(progress, "eth", len(self.eth))
55
- task_sol = output.create_progress_task(progress, "sol", len(self.sol))
56
- with progress:
57
- job = ConcurrentTasks()
58
- job.add_task("btc", self._process_btc, args=(progress, task_btc))
59
- job.add_task("eth", self._process_eth, args=(progress, task_eth))
60
- job.add_task("sol", self._process_sol, args=(progress, task_sol))
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,))
61
44
  job.execute()
62
45
 
63
- def _process_btc(self, progress: Progress, task_id: TaskID) -> None:
64
- job = ConcurrentTasks(max_workers=self.config.workers.btc)
65
- for idx, task in enumerate(self.btc):
66
- job.add_task(str(idx), btc.get_balance, args=(task.address, self.config, progress, task_id))
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))
67
50
  job.execute()
68
- for idx, _task in enumerate(self.btc):
69
- self.btc[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
51
+ for idx, _task in enumerate(self.tasks[network]):
52
+ self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
70
53
 
71
- def _process_eth(self, progress: Progress, task_id: TaskID) -> None:
72
- job = ConcurrentTasks(max_workers=self.config.workers.eth)
73
- for idx, task in enumerate(self.eth):
74
- job.add_task(str(idx), eth.get_balance, args=(task.address, task.token_address, self.config, progress, task_id))
75
- job.execute()
76
- for idx, _task in enumerate(self.eth):
77
- self.eth[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
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)
78
72
 
79
- def _process_sol(self, progress: Progress, task_id: TaskID) -> None:
80
- job = ConcurrentTasks(max_workers=self.config.workers.sol)
81
- for idx, task in enumerate(self.sol):
82
- job.add_task(str(idx), solana.get_balance, args=(task.address, self.config, progress, task_id))
83
- job.execute()
84
- for idx, _task in enumerate(self.sol):
85
- self.sol[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
73
+ case _:
74
+ raise ValueError
86
75
 
87
- @staticmethod
88
- def from_config(config: Config) -> Balances:
89
- tasks = Balances(config=config, btc=[], eth=[], sol=[])
90
- for idx, group in enumerate(config.groups):
91
- task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses]
92
- if group.network == Network.BTC:
93
- tasks.btc.extend(task_list)
94
- elif group.network == Network.ETH:
95
- tasks.eth.extend(task_list)
96
- elif group.network == Network.SOL:
97
- tasks.sol.extend(task_list)
98
- return tasks
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
mm_balance/cli.py CHANGED
@@ -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
mm_balance/config.py CHANGED
@@ -4,78 +4,72 @@ from decimal import Decimal
4
4
  from typing import Any, Self
5
5
 
6
6
  import pydash
7
- from mm_std import BaseConfig, Err, PrintFormat, fatal, hr
7
+ from mm_std import BaseConfig, PrintFormat, fatal, hr
8
8
  from pydantic import Field, field_validator, model_validator
9
9
 
10
- from mm_balance.types import DEFAULT_ETH_NODES, DEFAULT_SOL_NODES, EthTokenAddress, Network
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)
11
69
 
12
70
 
13
71
  class Config(BaseConfig):
14
- class Group(BaseConfig):
15
- comment: str = ""
16
- coin: str
17
- network: Network
18
- token_address: str | None = None
19
- coingecko_id: str | None = None
20
- addresses: list[str] = Field(default_factory=list)
21
- share: Decimal = Decimal(1)
22
-
23
- @property
24
- def name(self) -> str:
25
- result = self.coin
26
- if self.comment:
27
- result += " / " + self.comment
28
- return result
29
-
30
- @field_validator("coin", mode="after")
31
- def coin_validator(cls, v: str) -> str:
32
- return v.upper()
33
-
34
- @field_validator("addresses", mode="before")
35
- def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
36
- return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
37
-
38
- @model_validator(mode="before")
39
- def before_all(cls, data: Any) -> Any:
40
- if "network" not in data:
41
- data["network"] = detect_network(data["coin"])
42
- return data
43
-
44
- @model_validator(mode="after")
45
- def final_validator(self) -> Self:
46
- if self.token_address is None:
47
- self.token_address = detect_token_address(self.coin, self.network)
48
- if self.token_address is not None and self.network is Network.ETH:
49
- self.token_address = self.token_address.lower()
50
- return self
51
-
52
- def process_addresses(self, address_groups: list[Config.AddressGroup]) -> None:
53
- addresses: list[str] = []
54
- for address in self.addresses:
55
- if address_group := pydash.find(address_groups, lambda g: g.name == address): # noqa: B023
56
- addresses.extend(address_group.addresses)
57
- else:
58
- # TODO: check address is valid
59
- addresses.append(address)
60
- self.addresses = addresses
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
- class Workers(BaseConfig):
71
- btc: int = 5
72
- eth: int = 5
73
- sol: int = 5
74
-
75
- class TokenDecimals(BaseConfig):
76
- eth: dict[str, int] = Field(default_factory=dict)
77
-
78
- groups: list[Group]
72
+ groups: list[Group] = Field(alias="coins")
79
73
  addresses: list[AddressGroup] = Field(default_factory=list)
80
74
 
81
75
  proxies_url: str | None = None
@@ -85,18 +79,7 @@ class Config(BaseConfig):
85
79
  print_format: PrintFormat = PrintFormat.TABLE
86
80
  price: bool = True
87
81
 
88
- # non configs
89
- workers: Workers = Workers()
90
- token_decimals: TokenDecimals = TokenDecimals()
91
-
92
- def btc_groups(self) -> list[Group]:
93
- return [g for g in self.groups if g.network == Network.BTC]
94
-
95
- def eth_groups(self) -> list[Group]:
96
- return [g for g in self.groups if g.network == Network.ETH]
97
-
98
- def sol_groups(self) -> list[Group]:
99
- return [g for g in self.groups if g.network == Network.SOL]
82
+ workers: dict[Network, int] = {network: 5 for network in Network}
100
83
 
101
84
  def has_share(self) -> bool:
102
85
  return any(g.share != Decimal(1) for g in self.groups)
@@ -119,16 +102,6 @@ class Config(BaseConfig):
119
102
  if Network.SOL not in self.nodes:
120
103
  self.nodes[Network.SOL] = DEFAULT_SOL_NODES
121
104
 
122
- # load token decimals
123
- for group in self.groups:
124
- if group.network == Network.ETH and group.token_address is not None:
125
- from mm_balance.rpc import eth
126
-
127
- decimals_res = eth.get_token_decimals(group.token_address, self)
128
- if isinstance(decimals_res, Err):
129
- fatal(f"can't get decimals for token {group.coin} / {group.token_address}, error={decimals_res.err}")
130
- self.token_decimals.eth[group.token_address] = decimals_res.ok
131
-
132
105
  return self
133
106
 
134
107
 
@@ -141,7 +114,7 @@ def detect_network(coin: str) -> Network:
141
114
  if coin == "sol":
142
115
  return Network.SOL
143
116
  return Network.ETH
144
- # raise ValueError(f"can't get network for the coin: {coin}")
117
+ # TODO: raise ValueError(f"can't get network for the coin: {coin}")
145
118
 
146
119
 
147
120
  def detect_token_address(coin: str, network: str) -> str | None:
@@ -151,6 +124,12 @@ def detect_token_address(coin: str, network: str) -> str | None:
151
124
  if coin.lower() == "usdc":
152
125
  return EthTokenAddress.USDC
153
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
+
154
133
 
155
134
  def get_proxies(proxies_url: str) -> list[str]:
156
135
  try:
@@ -163,5 +142,5 @@ def get_proxies(proxies_url: str) -> list[str]:
163
142
  fatal(f"Can't get proxies: {err}")
164
143
 
165
144
 
166
- def get_address_group_by_name(address_groups: list[Config.AddressGroup], name: str) -> Config.AddressGroup | None:
145
+ def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
167
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"
mm_balance/output.py CHANGED
@@ -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)
mm_balance/price.py CHANGED
@@ -1,13 +1,11 @@
1
- import time
2
1
  from decimal import Decimal
3
2
 
4
3
  import pydash
5
- from mm_std import Err, Ok, Result, fatal, hr
4
+ from mm_std import fatal, hr
6
5
  from mm_std.random_ import random_str_choice
7
6
 
8
- from mm_balance import output
9
- from mm_balance.config import Config
10
- from mm_balance.types import EthTokenAddress, Network
7
+ from mm_balance.config import Config, Group
8
+ from mm_balance.constants import RETRIES_COINGECKO_PRICES, EthTokenAddress, Network
11
9
 
12
10
 
13
11
  class Prices(dict[str, Decimal]):
@@ -21,52 +19,26 @@ class Prices(dict[str, Decimal]):
21
19
 
22
20
  def get_prices(config: Config) -> Prices:
23
21
  result = Prices()
24
- coins_total = len(pydash.uniq([group.coin for group in config.groups]))
25
22
 
26
- progress = output.create_progress_bar()
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])
27
25
 
28
- with progress:
29
- task_id = output.create_progress_task(progress, "prices", total=coins_total)
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
30
31
 
31
- for group in config.groups:
32
- if group.coin in result:
33
- continue
34
-
35
- coingecko_id = get_coingecko_id(group)
36
- res = get_asset_price(coingecko_id, config.proxies)
37
- if isinstance(res, Ok):
38
- result[group.coin] = res.ok
39
- progress.update(task_id, advance=1)
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")))
40
35
  else:
41
- fatal(res.err)
42
- # raise ValueError(res.err)
36
+ fatal("Can't get price for {coin} from coingecko, coingecko_id={coingecko_ids[idx]}")
43
37
 
44
38
  return result
45
39
 
46
40
 
47
- def get_asset_price(coingecko_asset_id: str, proxies: list[str]) -> Result[Decimal]:
48
- url = f"https://api.coingecko.com/api/v3/simple/price?ids={coingecko_asset_id}&vs_currencies=usd"
49
- data = None
50
- error = f"error: can't get price for {coingecko_asset_id} from coingecko"
51
- for _ in range(3):
52
- res = hr(url, proxy=random_str_choice(proxies))
53
-
54
- # Check for Rate Limit
55
- if res.code == 429:
56
- error = f"error: can't get price for {coingecko_asset_id} from coingecko. You've exceeded the Rate Limit. Please add more proxies." # noqa: E501
57
- if not proxies:
58
- fatal(error) # Exit immidiately if no proxies are provided
59
-
60
- data = res.to_dict()
61
- if res.json and coingecko_asset_id in coingecko_asset_id in res.json:
62
- return Ok(Decimal(pydash.get(res.json, f"{coingecko_asset_id}.usd")))
63
-
64
- if not proxies:
65
- time.sleep(10)
66
- return Err(error, data=data)
67
-
68
-
69
- def get_coingecko_id(group: Config.Group) -> str:
41
+ def get_coingecko_id(group: Group) -> str:
70
42
  if group.coingecko_id:
71
43
  return group.coingecko_id
72
44
  elif group.network is Network.BTC:
mm_balance/rpc/btc.py CHANGED
@@ -2,19 +2,15 @@ from decimal import Decimal
2
2
 
3
3
  from mm_btc.blockstream import BlockstreamClient
4
4
  from mm_std import Ok, Result
5
- from rich.progress import Progress, TaskID
6
5
 
7
- from mm_balance.config import Config
6
+ from mm_balance.constants import RETRIES_BALANCE
8
7
 
9
8
 
10
- def get_balance(address: str, config: Config, progress: Progress | None = None, task_id: TaskID | None = None) -> Result[Decimal]:
11
- res: Result[Decimal] = (
12
- BlockstreamClient(proxies=config.proxies, attempts=3)
9
+ def get_balance(address: str, proxies: list[str], round_ndigits: int) -> Result[Decimal]:
10
+ return (
11
+ BlockstreamClient(proxies=proxies, attempts=RETRIES_BALANCE)
13
12
  .get_confirmed_balance(address)
14
13
  .and_then(
15
- lambda b: Ok(round(Decimal(b / 100_000_000), config.round_ndigits)),
14
+ lambda b: Ok(round(Decimal(b / 100_000_000), round_ndigits)),
16
15
  )
17
16
  )
18
- if task_id is not None and progress is not None:
19
- progress.update(task_id, advance=1)
20
- return res
mm_balance/rpc/eth.py CHANGED
@@ -2,37 +2,30 @@ from decimal import Decimal
2
2
 
3
3
  from mm_eth import erc20, rpc
4
4
  from mm_std import Ok, Result
5
- from rich.progress import Progress, TaskID
6
5
 
7
- from mm_balance.config import Config
8
- from mm_balance.types import Network
6
+ from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
9
7
 
10
8
 
11
- def get_balance(
12
- address: str, token_address: str | None, config: Config, progress: Progress | None = None, task_id: TaskID | None = None
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
13
17
  ) -> Result[Decimal]:
14
- res: Result[Decimal]
15
-
16
- if token_address is not None:
17
- res = erc20.get_balance(
18
- config.nodes[Network.ETH],
19
- token_address,
20
- address,
21
- proxies=config.proxies,
22
- attempts=5,
23
- timeout=10,
24
- ).and_then(
25
- lambda b: Ok(round(Decimal(b / 10 ** config.token_decimals.eth[token_address]), config.round_ndigits)),
26
- )
27
- else:
28
- res = rpc.eth_get_balance(config.nodes[Network.ETH], address, proxies=config.proxies, attempts=5, timeout=10).and_then(
29
- lambda b: Ok(round(Decimal(b / 10**18), config.round_ndigits)),
30
- )
31
-
32
- if task_id is not None and progress is not None:
33
- progress.update(task_id, advance=1)
34
- return res
35
-
36
-
37
- def get_token_decimals(token_address: str, config: Config) -> Result[int]:
38
- return erc20.get_decimals(config.nodes[Network.ETH], token_address, timeout=10, proxies=config.proxies, attempts=5)
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/rpc/solana.py CHANGED
@@ -1,19 +1,26 @@
1
1
  from decimal import Decimal
2
2
 
3
- from mm_solana.balance import sol_balance
3
+ from mm_solana import balance, token
4
4
  from mm_std import Ok, Result
5
- from rich.progress import Progress, TaskID
6
5
 
7
- from mm_balance.config import Config
8
- from mm_balance.types import Network
6
+ from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
9
7
 
10
8
 
11
- def get_balance(address: str, config: Config, progress: Progress | None = None, task_id: TaskID | None = None) -> Result[Decimal]:
12
- res: Result[Decimal] = sol_balance(
13
- address=address, nodes=config.nodes[Network.SOL], proxies=config.proxies, attempts=5, timeout=10
14
- ).and_then(
15
- lambda b: Ok(round(Decimal(b / 1_000_000_000), config.round_ndigits)),
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
16
26
  )
17
- if task_id is not None and progress is not None:
18
- progress.update(task_id, advance=1)
19
- return res
@@ -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
mm_balance/total.py CHANGED
@@ -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
@@ -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
@@ -0,0 +1,18 @@
1
+ mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mm_balance/balances.py,sha256=fawnQIfjWsljXo1MdvSxs3Xo7j19hBCa9jlyDXXWO7M,7587
3
+ mm_balance/cli.py,sha256=tjMXQhtKZiCzqoTw5rIygMaVCOwNMiNbkr4nzOY06wA,1360
4
+ mm_balance/config.py,sha256=lDizIS2qmZpiOWbrdPvn0DKUdN0FWuNdv2qKPJBfWEI,4866
5
+ mm_balance/constants.py,sha256=GnyqtIfxIpWLuzB80Y0mYrzrRk8XT3fMHh63Fek5LEA,935
6
+ mm_balance/output.py,sha256=GH5ESychKEDOc2gtaUH_JtKdNX1ZkXZD6kCk2mxd8_4,2568
7
+ mm_balance/price.py,sha256=uAm7pVAwMyzZ7bE4xxz4PD0mzLR7zFk6UtYSwGqWjAs,1996
8
+ mm_balance/token_decimals.py,sha256=8tAZiN5RpSFRtYb1VODABGg9x3JbLY73EA5BV0RqVv4,1297
9
+ mm_balance/total.py,sha256=3IDNBrcqGFaUYMlAzLpQCEBcWi1zdT0RSm7o7t1b4Tw,4700
10
+ mm_balance/config/example.yml,sha256=j3UzCgCIImkkzyMT_68LW7lRChsQOlwowhKjcwRBlyc,1336
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.11.dist-info/METADATA,sha256=ExgagHu4tCqpAHPjqivCaQ7-2Dj3HwrwOeyJct7DsXw,198
16
+ mm_balance-0.1.11.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
+ mm_balance-0.1.11.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
18
+ mm_balance-0.1.11.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- mm_balance/balances.py,sha256=lagu1y8OosgOPnMnp7DpY1vZmaarQZZABvvM3fK8Zjw,4053
3
- mm_balance/cli.py,sha256=VpiNJdwels7qWuklpV7kNEYm0lhsN4NJ7yENIsQLdK8,1250
4
- mm_balance/config.py,sha256=WH1YjOJxofv2jgklupd7sCzgz2nZtHDYCYhoSaZTHl0,5918
5
- mm_balance/output.py,sha256=Sb0pccEBNOCR9fuMigO9GJcyTNw9XPRZXTg8iznJKFQ,2568
6
- mm_balance/price.py,sha256=KyMx1T57SczKbYmbghpGty9BrPebrZdjBW0_BaabKDk,2926
7
- mm_balance/total.py,sha256=gdH9BzFgc-akVVrsCJ6NqN6cIS9mkEfp-TTKcn01I0U,4944
8
- mm_balance/types.py,sha256=8TflwL3KJ8HQW31qa8xrh-gyJT232lN1XznuNnIR6zM,662
9
- mm_balance/config/example.yml,sha256=6_S0hBdh7gMyO1ZPTGxmL21bBSzzmXuJHB0eWqKGueU,1225
10
- mm_balance/rpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- mm_balance/rpc/btc.py,sha256=OozGSF8CzR13jxTYjvBqkC8d0pdKkyEHxM7e7M_WbV0,681
12
- mm_balance/rpc/eth.py,sha256=yx31pwjvlu6MJUuWxtZtiK1vQvIkfMFwu5cNKaz1Yks,1317
13
- mm_balance/rpc/solana.py,sha256=tL51r_T1hNUDSB8vdjp9QB4ToLjd6yRLXE1RW0-eauw,710
14
- mm_balance-0.1.9.dist-info/METADATA,sha256=evs4MY6DDuZgIJKPbuVSqG87m7AguX5ExfdABT1gnk0,197
15
- mm_balance-0.1.9.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
16
- mm_balance-0.1.9.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
17
- mm_balance-0.1.9.dist-info/RECORD,,