mm-balance 0.1.10__py3-none-any.whl → 0.1.12__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
@@ -8,9 +8,9 @@ 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
13
  from mm_balance.token_decimals import TokenDecimals
13
- from mm_balance.types import Network
14
14
 
15
15
 
16
16
  class Balances:
@@ -32,9 +32,10 @@ class Balances:
32
32
  self.tasks[group.network].extend(task_list)
33
33
 
34
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
- )
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
+ )
38
39
 
39
40
  def process(self) -> None:
40
41
  with self.progress_bar:
@@ -57,14 +58,14 @@ class Balances:
57
58
  proxies = self.config.proxies
58
59
  token_decimals = self.token_decimals[network][token_address] if token_address else -1
59
60
  match network:
60
- case Network.BTC:
61
- res = btc.get_balance(wallet_address, self.config)
62
- case Network.ETH:
61
+ case Network.BITCOIN:
62
+ res = btc.get_balance(wallet_address, proxies, round_ndigits)
63
+ case Network.ETHEREUM | Network.ARBITRUM_ONE | Network.OP_MAINNET:
63
64
  if token_address is None:
64
65
  res = eth.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
65
66
  else:
66
67
  res = eth.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
67
- case Network.SOL:
68
+ case Network.SOLANA:
68
69
  if token_address is None:
69
70
  res = solana.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
70
71
  else:
@@ -79,91 +80,3 @@ class Balances:
79
80
  def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
80
81
  # TODO: can we get network by group_index?
81
82
  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
@@ -8,6 +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
12
  from mm_balance.price import Prices, get_prices
12
13
  from mm_balance.token_decimals import get_token_decimals
13
14
 
@@ -21,10 +22,20 @@ def example_callback(value: bool) -> None:
21
22
  raise typer.Exit
22
23
 
23
24
 
25
+ def networks_callback(value: bool) -> None:
26
+ if value:
27
+ for network in Network:
28
+ typer.echo(network)
29
+ raise typer.Exit
30
+
31
+
24
32
  @app.command()
25
33
  def cli(
26
34
  config_path: Annotated[pathlib.Path, typer.Argument()],
27
35
  _example: Annotated[bool | None, typer.Option("--example", callback=example_callback, help="Print a config example.")] = None,
36
+ _networks: Annotated[
37
+ bool | None, typer.Option("--networks", callback=networks_callback, help="Print supported networks.")
38
+ ] = None,
28
39
  ) -> None:
29
40
  zip_password = "" # nosec
30
41
  if config_path.name.endswith(".zip"):
@@ -32,11 +43,12 @@ def cli(
32
43
  config = Config.read_config(config_path, zip_password=zip_password)
33
44
 
34
45
  prices = get_prices(config) if config.price else Prices()
46
+ output.print_prices(config, prices)
47
+
35
48
  token_decimals = get_token_decimals(config)
36
49
  balances = Balances(config, token_decimals)
37
50
  balances.process()
38
51
 
39
- output.print_prices(config, prices)
40
52
  output.print_groups(balances, config, prices)
41
53
  output.print_total(config, balances, prices)
42
54
 
@@ -1,14 +1,16 @@
1
- groups:
2
- - coin: sol
1
+ coins:
2
+ - ticker: SOL
3
+ network: solana
3
4
  addresses:
4
5
  - 2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S # binance
5
6
 
6
- - coin: usdt
7
- network: sol
7
+ - ticker: USDT
8
+ network: solana
8
9
  addresses:
9
10
  - 2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S # binance
10
11
 
11
- - coin: btc
12
+ - ticker: BTC
13
+ network: bitcoin
12
14
  comment: coldwallets
13
15
  addresses: |
14
16
  34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo # binance
@@ -16,19 +18,23 @@ groups:
16
18
  bc1ql49ydapnjafl5t2cp9zqpjwe6pdgmxy98859v2 # robinhood
17
19
  share: 0.1 # 10%
18
20
 
19
- - coin: eth
21
+ - ticker: ETH
22
+ network: ethereum
20
23
  comment: okx
21
24
  addresses: okx_eth
22
25
 
23
- - coin: usdt
26
+ - ticker: USDT
27
+ network: ethereum
24
28
  comment: okx
25
29
  addresses: okx_eth
26
30
 
27
- - coin: eth
31
+ - ticker: ETH
32
+ network: ethereum
28
33
  comment: binance
29
34
  addresses: binance_eth
30
35
 
31
- - coin: usdt
36
+ - ticker: USDT
37
+ network: ethereum
32
38
  comment: binance
33
39
  addresses: binance_eth
34
40
 
@@ -53,4 +59,4 @@ addresses:
53
59
  #- http://123.123.123.123
54
60
  #- http://123.123.123.124
55
61
  #round_ndigits: 4
56
- #price: no
62
+ #price: yes
mm_balance/config.py CHANGED
@@ -1,73 +1,82 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from decimal import Decimal
4
- from typing import Any, Self
4
+ from typing import Self
5
5
 
6
6
  import pydash
7
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, SolTokenAddress
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
+ )
17
+
18
+
19
+ class Group(BaseConfig):
20
+ comment: str = ""
21
+ ticker: str
22
+ network: Network
23
+ token_address: str | None = None
24
+ coingecko_id: str | None = None
25
+ addresses: list[str] = Field(default_factory=list)
26
+ share: Decimal = Decimal(1)
27
+
28
+ @property
29
+ def name(self) -> str:
30
+ result = self.ticker
31
+ if self.comment:
32
+ result += " / " + self.comment
33
+ result += " / " + self.network.value
34
+ return result
35
+
36
+ @field_validator("ticker", mode="after")
37
+ def coin_validator(cls, v: str) -> str:
38
+ return v.upper()
39
+
40
+ @field_validator("addresses", mode="before")
41
+ def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
42
+ return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
43
+
44
+ # @model_validator(mode="before")
45
+ # def before_all(cls, data: Any) -> Any:
46
+ # if "network" not in data:
47
+ # data["network"] = detect_network(data["coin"])
48
+ # return data
49
+
50
+ @model_validator(mode="after")
51
+ def final_validator(self) -> Self:
52
+ if self.token_address is None:
53
+ self.token_address = detect_token_address(self.ticker, self.network)
54
+ if self.token_address is not None and self.network is Network.ETHEREUM:
55
+ self.token_address = self.token_address.lower()
56
+ return self
57
+
58
+ def process_addresses(self, address_groups: list[AddressGroup]) -> None:
59
+ addresses: list[str] = []
60
+ for address in self.addresses:
61
+ if address_group := pydash.find(address_groups, lambda g: g.name == address): # noqa: B023
62
+ addresses.extend(address_group.addresses)
63
+ else:
64
+ # TODO: check address is valid
65
+ addresses.append(address)
66
+ self.addresses = addresses
67
+
68
+
69
+ class AddressGroup(BaseConfig):
70
+ name: str
71
+ addresses: list[str]
72
+
73
+ @field_validator("addresses", mode="before")
74
+ def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
75
+ return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
11
76
 
12
77
 
13
78
  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
- groups: list[Group]
79
+ groups: list[Group] = Field(alias="coins")
71
80
  addresses: list[AddressGroup] = Field(default_factory=list)
72
81
 
73
82
  proxies_url: str | None = None
@@ -79,15 +88,6 @@ class Config(BaseConfig):
79
88
 
80
89
  workers: dict[Network, int] = {network: 5 for network in Network}
81
90
 
82
- def btc_groups(self) -> list[Group]:
83
- return [g for g in self.groups if g.network == Network.BTC]
84
-
85
- def eth_groups(self) -> list[Group]:
86
- return [g for g in self.groups if g.network == Network.ETH]
87
-
88
- def sol_groups(self) -> list[Group]:
89
- return [g for g in self.groups if g.network == Network.SOL]
90
-
91
91
  def has_share(self) -> bool:
92
92
  return any(g.share != Decimal(1) for g in self.groups)
93
93
 
@@ -102,40 +102,36 @@ class Config(BaseConfig):
102
102
  group.process_addresses(self.addresses)
103
103
 
104
104
  # load default rpc nodes
105
- if Network.BTC not in self.nodes:
106
- self.nodes[Network.BTC] = []
107
- if Network.ETH not in self.nodes:
108
- self.nodes[Network.ETH] = DEFAULT_ETH_NODES
109
- if Network.SOL not in self.nodes:
110
- self.nodes[Network.SOL] = DEFAULT_SOL_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
111
115
 
112
116
  return self
113
117
 
114
118
 
115
- def detect_network(coin: str) -> Network:
116
- coin = coin.lower()
117
- if coin == "btc":
118
- return Network.BTC
119
- if coin == "eth":
120
- return Network.ETH
121
- if coin == "sol":
122
- return Network.SOL
123
- return Network.ETH
124
- # raise ValueError(f"can't get network for the coin: {coin}")
125
-
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}")
126
130
 
127
- def detect_token_address(coin: str, network: str) -> str | None:
128
- if network == Network.ETH.lower():
129
- if coin.lower() == "usdt":
130
- return EthTokenAddress.USDT
131
- if coin.lower() == "usdc":
132
- return EthTokenAddress.USDC
133
131
 
134
- if network == Network.SOL.lower():
135
- if coin.lower() == "usdt":
136
- return SolTokenAddress.USDT
137
- if coin.lower() == "usdc":
138
- return SolTokenAddress.USDC
132
+ def detect_token_address(coin: str, network: Network) -> str | None:
133
+ if network in TOKEN_ADDRESS:
134
+ return TOKEN_ADDRESS[network].get(coin)
139
135
 
140
136
 
141
137
  def get_proxies(proxies_url: str) -> list[str]:
@@ -149,5 +145,5 @@ def get_proxies(proxies_url: str) -> list[str]:
149
145
  fatal(f"Can't get proxies: {err}")
150
146
 
151
147
 
152
- def get_address_group_by_name(address_groups: list[Config.AddressGroup], name: str) -> Config.AddressGroup | None:
148
+ def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
153
149
  return pydash.find(address_groups, lambda g: g.name == name)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum, unique
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 Network(str, Enum):
14
+ ARBITRUM_ONE = "arbitrum-one"
15
+ BITCOIN = "bitcoin"
16
+ ETHEREUM = "ethereum"
17
+ SOLANA = "solana"
18
+ OP_MAINNET = "op-mainnet" # Optimism mainnet
19
+
20
+
21
+ TOKEN_ADDRESS: dict[Network, dict[str, str]] = {
22
+ Network.ETHEREUM: {
23
+ "USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
24
+ "USDC": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
25
+ },
26
+ Network.SOLANA: {
27
+ "USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
28
+ "USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
29
+ },
30
+ # TODO: Add for Arbitrum and Optimism, usdt + usdc
31
+ }
32
+
33
+ TICKER_TO_COINGECKO_ID = {
34
+ "BTC": "bitcoin",
35
+ "ETH": "ethereum",
36
+ "USDT": "tether",
37
+ "USDC": "usd-coin",
38
+ "SOL": "solana",
39
+ }
40
+
41
+ USD_STABLECOINS = ["USDT", "USDC"]
42
+
43
+ DEFAULT_ETHEREUM_NODES = ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"]
44
+ DEFAULT_SOLANA_NODES = ["https://api.mainnet-beta.solana.com"]
45
+ DEFAULT_ARBITRUM_ONE_NODES = ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com"]
46
+ DEFAULT_OP_MAINNET_NODES = ["https://mainnet.optimism.io", "https://optimism.llamarpc.com"]
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)
@@ -24,7 +24,7 @@ def _print_group(group: Config.Group, group_balances: list[Balances.Balance], co
24
24
  if isinstance(address_task.balance, Ok):
25
25
  balance_sum += address_task.balance.ok
26
26
  if config.price:
27
- balance_usd = round(address_task.balance.ok * prices[group.coin], config.round_ndigits)
27
+ balance_usd = round(address_task.balance.ok * prices[group.ticker], config.round_ndigits)
28
28
  usd_sum += balance_usd
29
29
  row.append(f"${balance_usd}")
30
30
  rows.append(row)
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, Network
11
9
 
12
10
 
13
11
  class Prices(dict[str, Decimal]):
@@ -21,63 +19,48 @@ 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.ticker 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")))
41
+ TICKER_TO_COINGECKO_ID = {
42
+ "BTC": "bitcoin",
43
+ "ETH": "ethereum",
44
+ "USDT": "tether",
45
+ "USDC": "usd-coin",
46
+ "SOL": "solana",
47
+ }
63
48
 
64
- if not proxies:
65
- time.sleep(10)
66
- return Err(error, data=data)
67
49
 
68
-
69
- def get_coingecko_id(group: Config.Group) -> str:
50
+ def get_coingecko_id(group: Group) -> str:
70
51
  if group.coingecko_id:
71
52
  return group.coingecko_id
72
- elif group.network is Network.BTC:
53
+ elif group.network is Network.BITCOIN:
73
54
  return "bitcoin"
74
- elif group.network is Network.ETH and group.token_address is None:
55
+ elif group.network is Network.ETHEREUM and group.token_address is None:
56
+ return "ethereum"
57
+ elif group.ticker == "ETH":
75
58
  return "ethereum"
76
- elif group.coin.lower() == "usdt" or (group.token_address is not None and group.token_address == EthTokenAddress.USDT):
59
+ elif group.ticker == "USDT":
77
60
  return "tether"
78
- elif group.coin.lower() == "usdc" or (group.token_address is not None and group.token_address == EthTokenAddress.USDC):
61
+ elif group.ticker == "USDC":
79
62
  return "usd-coin"
80
- elif group.coin.lower() == "sol":
63
+ elif group.ticker == "SOL":
81
64
  return "solana"
82
65
 
83
- raise ValueError(f"can't get coingecko_id for {group.coin}")
66
+ raise ValueError(f"can't get coingecko_id for {group.ticker}")
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
@@ -3,9 +3,11 @@ from decimal import Decimal
3
3
  from mm_eth import erc20, rpc
4
4
  from mm_std import Ok, Result
5
5
 
6
+ from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
7
+
6
8
 
7
9
  def get_native_balance(nodes: list[str], address: str, proxies: list[str], round_ndigits: int) -> Result[Decimal]:
8
- return rpc.eth_get_balance(nodes, address, proxies=proxies, attempts=5, timeout=10).and_then(
10
+ return rpc.eth_get_balance(nodes, address, proxies=proxies, attempts=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE).and_then(
9
11
  lambda b: Ok(round(Decimal(b / 10**18), round_ndigits)),
10
12
  )
11
13
 
@@ -18,29 +20,12 @@ def get_token_balance(
18
20
  token_address,
19
21
  wallet_address,
20
22
  proxies=proxies,
21
- attempts=5,
22
- timeout=10,
23
+ attempts=RETRIES_BALANCE,
24
+ timeout=TIMEOUT_BALANCE,
23
25
  ).and_then(
24
26
  lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)),
25
27
  )
26
28
 
27
29
 
28
30
  def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
29
- return erc20.get_decimals(nodes, token_address, timeout=10, proxies=proxies, attempts=5)
30
-
31
-
32
- # def get_balance(
33
- # address: str, token_address: str | None, config: Config, progress: Progress | None = None, task_id: TaskID | None = None
34
- # ) -> Result[Decimal]:
35
- # res: Result[Decimal]
36
- #
37
- # if token_address is not None:
38
- #
39
- # else:
40
- # res = rpc.eth_get_balance(config.nodes[Network.ETH], address, proxies=config.proxies, attempts=5, timeout=10).and_then(
41
- # lambda b: Ok(round(Decimal(b / 10 ** 18), config.round_ndigits)),
42
- # )
43
- #
44
- # if task_id is not None and progress is not None:
45
- # progress.update(task_id, advance=1)
46
- # return res
31
+ return erc20.get_decimals(nodes, token_address, timeout=TIMEOUT_DECIMALS, proxies=proxies, attempts=RETRIES_DECIMALS)
mm_balance/rpc/solana.py CHANGED
@@ -3,20 +3,24 @@ from decimal import Decimal
3
3
  from mm_solana import balance, token
4
4
  from mm_std import Ok, Result
5
5
 
6
+ from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
7
+
6
8
 
7
9
  def get_native_balance(nodes: list[str], address: str, proxies: list[str], round_ndigits: int) -> Result[Decimal]:
8
- return balance.get_balance_with_retries(nodes, address, retries=5, timeout=5, proxies=proxies).and_then(
9
- lambda b: Ok(round(Decimal(b / 1_000_000_000), round_ndigits)),
10
- )
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)))
11
13
 
12
14
 
13
15
  def get_token_balance(
14
16
  nodes: list[str], wallet_address: str, token_address: str, decimals: int, proxies: list[str], round_ndigits: int
15
17
  ) -> Result[Decimal]:
16
- return token.get_balance_with_retries(nodes, wallet_address, token_address, retries=5, timeout=5, proxies=proxies).and_then(
17
- lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits))
18
- )
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)))
19
21
 
20
22
 
21
23
  def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
22
- return token.get_decimals_with_retries(nodes, token_address, retries=5, timeout=5, proxies=proxies)
24
+ return token.get_decimals_with_retries(
25
+ nodes, token_address, retries=RETRIES_DECIMALS, timeout=TIMEOUT_DECIMALS, proxies=proxies
26
+ )
@@ -1,8 +1,8 @@
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
4
5
  from mm_balance.rpc import eth, solana
5
- from mm_balance.types import Network
6
6
 
7
7
 
8
8
  class TokenDecimals(dict[Network, dict[str, int]]):
@@ -23,15 +23,15 @@ def get_token_decimals(config: Config) -> TokenDecimals:
23
23
  proxies = config.proxies
24
24
 
25
25
  match group.network:
26
- case Network.ETH:
26
+ case Network.ETHEREUM:
27
27
  decimals_res = eth.get_token_decimals(nodes, group.token_address, proxies)
28
- case Network.SOL:
28
+ case Network.SOLANA:
29
29
  decimals_res = solana.get_token_decimals(nodes, group.token_address, proxies)
30
30
  case _:
31
31
  raise ValueError(f"unsupported network: {group.network}. Cant get token decimals for {group.token_address}")
32
32
 
33
33
  if isinstance(decimals_res, Err):
34
- fatal(f"can't get decimals for token {group.coin} / {group.token_address}, error={decimals_res.err}")
34
+ fatal(f"can't get decimals for token {group.ticker} / {group.token_address}, error={decimals_res.err}")
35
35
  result[group.network][group.token_address] = decimals_res.ok
36
36
 
37
37
  return result
mm_balance/total.py CHANGED
@@ -7,8 +7,8 @@ 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 USD_STABLECOINS
10
11
  from mm_balance.price import Prices
11
- from mm_balance.types import Coin
12
12
 
13
13
 
14
14
  @dataclass
@@ -38,16 +38,16 @@ class Total:
38
38
  for address_task in balances.get_group_balances(group_index, group.network):
39
39
  if isinstance(address_task.balance, Ok):
40
40
  balance_sum += address_task.balance.ok
41
- if group.coin in Coin.usd_coins():
41
+ if group.ticker in USD_STABLECOINS:
42
42
  stablecoin_sum += address_task.balance.ok
43
43
  stablecoin_sum_share += address_task.balance.ok * group.share
44
44
  if config.price:
45
- balance_usd = round(address_task.balance.ok * prices[group.coin], config.round_ndigits)
45
+ balance_usd = round(address_task.balance.ok * prices[group.ticker], config.round_ndigits)
46
46
  usd_sum += balance_usd
47
47
  usd_sum_share += group.share * balance_usd
48
48
 
49
- coins[group.coin] += balance_sum
50
- coins_share[group.coin] += round(balance_sum * group.share, config.round_ndigits)
49
+ coins[group.ticker] += balance_sum
50
+ coins_share[group.ticker] += round(balance_sum * group.share, config.round_ndigits)
51
51
  return cls(
52
52
  coins=coins,
53
53
  coins_share=coins_share,
@@ -78,7 +78,7 @@ class Total:
78
78
  rows = []
79
79
  for key, value in self.coins.items():
80
80
  usd_value = round(value * self.prices[key], self.config.round_ndigits)
81
- if key in Coin.usd_coins():
81
+ if key in USD_STABLECOINS:
82
82
  usd_share = round(self.stablecoin_sum * 100 / self.usd_sum, self.config.round_ndigits)
83
83
  else:
84
84
  usd_share = round(usd_value * 100 / self.usd_sum, self.config.round_ndigits)
@@ -97,7 +97,7 @@ class Total:
97
97
  rows = []
98
98
  for key, _ in self.coins.items():
99
99
  usd_value = round(self.coins_share[key] * self.prices[key], self.config.round_ndigits)
100
- if key in Coin.usd_coins():
100
+ if key in USD_STABLECOINS:
101
101
  usd_share = round(self.stablecoin_sum_share * 100 / self.usd_sum_share, self.config.round_ndigits)
102
102
  else:
103
103
  usd_share = round(usd_value * 100 / self.usd_sum_share, self.config.round_ndigits)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mm-balance
3
- Version: 0.1.10
3
+ Version: 0.1.12
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: mm-btc==0.1.0
6
6
  Requires-Dist: mm-eth==0.1.3
@@ -0,0 +1,18 @@
1
+ mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mm_balance/balances.py,sha256=4PjyxEynkv_59cs6-HujU3rn18DebotnWIu8Sa1Jj9s,3662
3
+ mm_balance/cli.py,sha256=GZB3MgYxOV5AWhX2NvU9LWKF7I2iNQzyhgC2oKlOODU,1700
4
+ mm_balance/config.py,sha256=I1QmL5Nlyyifn78wmndnw5zi-zk_B-paK42KTd4pRb4,4995
5
+ mm_balance/constants.py,sha256=J0N8GmWAagKGyJ8DMnmxhU6LXEkvKhYiJpacKqxkQ4s,1308
6
+ mm_balance/output.py,sha256=5y35jJUDJqrz86EVNklBVa06b1ia0ppy2ypTy9Jkbb0,2570
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=KxSmjIKY1LXjUzfA4sv3j352fVePRDEeqG48xEwKWkc,1482
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.12.dist-info/METADATA,sha256=fYBCpx8m2vk02CTT-tJskdEZoi_wZkBdXc8cprDU4_8,198
16
+ mm_balance-0.1.12.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
+ mm_balance-0.1.12.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
18
+ mm_balance-0.1.12.dist-info/RECORD,,
mm_balance/types.py DELETED
@@ -1,38 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from enum import Enum, unique
4
-
5
- DEFAULT_ETH_NODES = ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"]
6
- DEFAULT_SOL_NODES = ["https://api.mainnet-beta.solana.com"]
7
-
8
-
9
- @unique
10
- class Coin(str, Enum):
11
- BTC = "BTC"
12
- ETH = "ETH"
13
- SOL = "SOL"
14
- USDT = "USDT"
15
- USDC = "USDC"
16
-
17
- @classmethod
18
- def usd_coins(cls) -> list[Coin]:
19
- return [Coin.USDT, Coin.USDC]
20
-
21
-
22
- @unique
23
- class Network(str, Enum):
24
- BTC = "btc"
25
- ETH = "eth"
26
- SOL = "sol"
27
-
28
-
29
- @unique
30
- class EthTokenAddress(str, Enum):
31
- USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7"
32
- USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
33
-
34
-
35
- @unique
36
- class SolTokenAddress(str, Enum):
37
- USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
38
- USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
@@ -1,18 +0,0 @@
1
- mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- mm_balance/balances.py,sha256=iERBZ_kTqj1wSlwUv5O2OteEhK2FMttD8Enx2avQHm0,7572
3
- mm_balance/cli.py,sha256=qKmupsRR3dGKZzA5hrKgmd4N4SoRMKTI8HAiwhSvgXI,1359
4
- mm_balance/config.py,sha256=n6NbsgnB-Uus4kOZJr37n7BnHc539LH8LT6BpJbqhVU,5370
5
- mm_balance/output.py,sha256=Sb0pccEBNOCR9fuMigO9GJcyTNw9XPRZXTg8iznJKFQ,2568
6
- mm_balance/price.py,sha256=KyMx1T57SczKbYmbghpGty9BrPebrZdjBW0_BaabKDk,2926
7
- mm_balance/token_decimals.py,sha256=UlYzLFR_w-jAsotq8CjGJc38KwklWG0Tr0JK-_wK6QQ,1293
8
- mm_balance/total.py,sha256=OuXfKKy18X9f7HO_RJNFh4M8VPVH0MBb3soSRsqS_RY,4696
9
- mm_balance/types.py,sha256=O6mjf2UYSl2XzwMU8Zsg_T2iYdSxZPg2-7UV1UORaMU,822
10
- mm_balance/config/example.yml,sha256=A3pGt8BVykKNajfhHziTM7RLM1KSZOm_kmhCQExQkgo,1336
11
- mm_balance/rpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- mm_balance/rpc/btc.py,sha256=OozGSF8CzR13jxTYjvBqkC8d0pdKkyEHxM7e7M_WbV0,681
13
- mm_balance/rpc/eth.py,sha256=vWjgvP2P_ejwK_dsxS3NZCP0PJyLFDndvfVaZIePXOU,1578
14
- mm_balance/rpc/solana.py,sha256=NIa494HjvHYONutKV_6HGcJV5tqiRvA1099JVkrrdd8,972
15
- mm_balance-0.1.10.dist-info/METADATA,sha256=fDZjEFqUMNBwhqwoyG0ldd65jlV2EEqnKhZ00PynqN0,198
16
- mm_balance-0.1.10.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
- mm_balance-0.1.10.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
18
- mm_balance-0.1.10.dist-info/RECORD,,