mm-balance 0.1.10__tar.gz → 0.1.12__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.
- {mm_balance-0.1.10 → mm_balance-0.1.12}/PKG-INFO +1 -1
- {mm_balance-0.1.10 → mm_balance-0.1.12}/pyproject.toml +1 -1
- mm_balance-0.1.12/src/mm_balance/balances.py +82 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/cli.py +13 -1
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/config/example.yml +16 -10
- mm_balance-0.1.12/src/mm_balance/config.py +149 -0
- mm_balance-0.1.12/src/mm_balance/constants.py +46 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/output.py +3 -3
- mm_balance-0.1.12/src/mm_balance/price.py +66 -0
- mm_balance-0.1.12/src/mm_balance/rpc/btc.py +16 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/rpc/eth.py +6 -21
- mm_balance-0.1.12/src/mm_balance/rpc/solana.py +26 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/token_decimals.py +4 -4
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/total.py +7 -7
- {mm_balance-0.1.10 → mm_balance-0.1.12}/uv.lock +1 -1
- mm_balance-0.1.10/src/mm_balance/balances.py +0 -169
- mm_balance-0.1.10/src/mm_balance/config.py +0 -153
- mm_balance-0.1.10/src/mm_balance/price.py +0 -83
- mm_balance-0.1.10/src/mm_balance/rpc/btc.py +0 -20
- mm_balance-0.1.10/src/mm_balance/rpc/solana.py +0 -22
- mm_balance-0.1.10/src/mm_balance/types.py +0 -38
- {mm_balance-0.1.10 → mm_balance-0.1.12}/.gitignore +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/README.md +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/justfile +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/__init__.py +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/src/mm_balance/rpc/__init__.py +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/tests/__init__.py +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/tests/conftest.py +0 -0
- {mm_balance-0.1.10 → mm_balance-0.1.12}/tests/test_dummy.py +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from mm_std import ConcurrentTasks, Result
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from rich.progress import TaskID
|
|
8
|
+
|
|
9
|
+
from mm_balance import output
|
|
10
|
+
from mm_balance.config import Config
|
|
11
|
+
from mm_balance.constants import Network
|
|
12
|
+
from mm_balance.rpc import btc, eth, solana
|
|
13
|
+
from mm_balance.token_decimals import TokenDecimals
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Balances:
|
|
17
|
+
class Balance(BaseModel):
|
|
18
|
+
group_index: int
|
|
19
|
+
address: str
|
|
20
|
+
token_address: str | None
|
|
21
|
+
balance: Result[Decimal] | None = None
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: Config, token_decimals: TokenDecimals):
|
|
24
|
+
self.config = config
|
|
25
|
+
self.token_decimals = token_decimals
|
|
26
|
+
self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in Network}
|
|
27
|
+
self.progress_bar = output.create_progress_bar()
|
|
28
|
+
self.progress_bar_task: dict[Network, TaskID] = {}
|
|
29
|
+
|
|
30
|
+
for idx, group in enumerate(config.groups):
|
|
31
|
+
task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses]
|
|
32
|
+
self.tasks[group.network].extend(task_list)
|
|
33
|
+
|
|
34
|
+
for network in Network:
|
|
35
|
+
if self.tasks[network]:
|
|
36
|
+
self.progress_bar_task[network] = output.create_progress_task(
|
|
37
|
+
self.progress_bar, network.value, len(self.tasks[network])
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def process(self) -> None:
|
|
41
|
+
with self.progress_bar:
|
|
42
|
+
job = ConcurrentTasks(max_workers=10)
|
|
43
|
+
for network in Network:
|
|
44
|
+
job.add_task(network.value, self._process_network, args=(network,))
|
|
45
|
+
job.execute()
|
|
46
|
+
|
|
47
|
+
def _process_network(self, network: Network) -> None:
|
|
48
|
+
job = ConcurrentTasks(max_workers=self.config.workers[network])
|
|
49
|
+
for idx, task in enumerate(self.tasks[network]):
|
|
50
|
+
job.add_task(str(idx), self._get_balance, args=(network, task.address, task.token_address))
|
|
51
|
+
job.execute()
|
|
52
|
+
for idx, _task in enumerate(self.tasks[network]):
|
|
53
|
+
self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
|
|
54
|
+
|
|
55
|
+
def _get_balance(self, network: Network, wallet_address: str, token_address: str | None) -> Result[Decimal]:
|
|
56
|
+
nodes = self.config.nodes[network]
|
|
57
|
+
round_ndigits = self.config.round_ndigits
|
|
58
|
+
proxies = self.config.proxies
|
|
59
|
+
token_decimals = self.token_decimals[network][token_address] if token_address else -1
|
|
60
|
+
match network:
|
|
61
|
+
case Network.BITCOIN:
|
|
62
|
+
res = btc.get_balance(wallet_address, proxies, round_ndigits)
|
|
63
|
+
case Network.ETHEREUM | Network.ARBITRUM_ONE | Network.OP_MAINNET:
|
|
64
|
+
if token_address is None:
|
|
65
|
+
res = eth.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
|
|
66
|
+
else:
|
|
67
|
+
res = eth.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
68
|
+
case Network.SOLANA:
|
|
69
|
+
if token_address is None:
|
|
70
|
+
res = solana.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
|
|
71
|
+
else:
|
|
72
|
+
res = solana.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
73
|
+
|
|
74
|
+
case _:
|
|
75
|
+
raise ValueError
|
|
76
|
+
|
|
77
|
+
self.progress_bar.update(self.progress_bar_task[network], advance=1)
|
|
78
|
+
return res
|
|
79
|
+
|
|
80
|
+
def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
|
|
81
|
+
# TODO: can we get network by group_index?
|
|
82
|
+
return [b for b in self.tasks[network] if b.group_index == group_index]
|
|
@@ -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
|
-
|
|
2
|
-
-
|
|
1
|
+
coins:
|
|
2
|
+
- ticker: SOL
|
|
3
|
+
network: solana
|
|
3
4
|
addresses:
|
|
4
5
|
- 2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S # binance
|
|
5
6
|
|
|
6
|
-
-
|
|
7
|
-
network:
|
|
7
|
+
- ticker: USDT
|
|
8
|
+
network: solana
|
|
8
9
|
addresses:
|
|
9
10
|
- 2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S # binance
|
|
10
11
|
|
|
11
|
-
-
|
|
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
|
-
-
|
|
21
|
+
- ticker: ETH
|
|
22
|
+
network: ethereum
|
|
20
23
|
comment: okx
|
|
21
24
|
addresses: okx_eth
|
|
22
25
|
|
|
23
|
-
-
|
|
26
|
+
- ticker: USDT
|
|
27
|
+
network: ethereum
|
|
24
28
|
comment: okx
|
|
25
29
|
addresses: okx_eth
|
|
26
30
|
|
|
27
|
-
-
|
|
31
|
+
- ticker: ETH
|
|
32
|
+
network: ethereum
|
|
28
33
|
comment: binance
|
|
29
34
|
addresses: binance_eth
|
|
30
35
|
|
|
31
|
-
-
|
|
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:
|
|
62
|
+
#price: yes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import 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 (
|
|
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)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Config(BaseConfig):
|
|
79
|
+
groups: list[Group] = Field(alias="coins")
|
|
80
|
+
addresses: list[AddressGroup] = Field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
proxies_url: str | None = None
|
|
83
|
+
proxies: list[str] = Field(default_factory=list)
|
|
84
|
+
round_ndigits: int = 4
|
|
85
|
+
nodes: dict[Network, list[str]] = Field(default_factory=dict)
|
|
86
|
+
print_format: PrintFormat = PrintFormat.TABLE
|
|
87
|
+
price: bool = True
|
|
88
|
+
|
|
89
|
+
workers: dict[Network, int] = {network: 5 for network in Network}
|
|
90
|
+
|
|
91
|
+
def has_share(self) -> bool:
|
|
92
|
+
return any(g.share != Decimal(1) for g in self.groups)
|
|
93
|
+
|
|
94
|
+
@model_validator(mode="after")
|
|
95
|
+
def final_validator(self) -> Self:
|
|
96
|
+
# load from proxies_url
|
|
97
|
+
if self.proxies_url is not None:
|
|
98
|
+
self.proxies = get_proxies(self.proxies_url)
|
|
99
|
+
|
|
100
|
+
# load addresses from address_group
|
|
101
|
+
for group in self.groups:
|
|
102
|
+
group.process_addresses(self.addresses)
|
|
103
|
+
|
|
104
|
+
# load default rpc nodes
|
|
105
|
+
if Network.BITCOIN not in self.nodes:
|
|
106
|
+
self.nodes[Network.BITCOIN] = []
|
|
107
|
+
if Network.ETHEREUM not in self.nodes:
|
|
108
|
+
self.nodes[Network.ETHEREUM] = DEFAULT_ETHEREUM_NODES
|
|
109
|
+
if Network.ARBITRUM_ONE not in self.nodes:
|
|
110
|
+
self.nodes[Network.ARBITRUM_ONE] = DEFAULT_ARBITRUM_ONE_NODES
|
|
111
|
+
if Network.OP_MAINNET not in self.nodes:
|
|
112
|
+
self.nodes[Network.OP_MAINNET] = DEFAULT_ARBITRUM_ONE_NODES
|
|
113
|
+
if Network.SOLANA not in self.nodes:
|
|
114
|
+
self.nodes[Network.SOLANA] = DEFAULT_SOLANA_NODES
|
|
115
|
+
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
|
|
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}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def detect_token_address(coin: str, network: Network) -> str | None:
|
|
133
|
+
if network in TOKEN_ADDRESS:
|
|
134
|
+
return TOKEN_ADDRESS[network].get(coin)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_proxies(proxies_url: str) -> list[str]:
|
|
138
|
+
try:
|
|
139
|
+
res = hr(proxies_url)
|
|
140
|
+
if res.is_error():
|
|
141
|
+
fatal(f"Can't get proxies: {res.error}")
|
|
142
|
+
proxies = [p.strip() for p in res.body.splitlines() if p.strip()]
|
|
143
|
+
return pydash.uniq(proxies)
|
|
144
|
+
except Exception as err:
|
|
145
|
+
fatal(f"Can't get proxies: {err}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
|
|
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"]
|
|
@@ -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:
|
|
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.
|
|
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)
|
|
@@ -0,0 +1,66 @@
|
|
|
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, 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.ticker 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
|
+
TICKER_TO_COINGECKO_ID = {
|
|
42
|
+
"BTC": "bitcoin",
|
|
43
|
+
"ETH": "ethereum",
|
|
44
|
+
"USDT": "tether",
|
|
45
|
+
"USDC": "usd-coin",
|
|
46
|
+
"SOL": "solana",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_coingecko_id(group: Group) -> str:
|
|
51
|
+
if group.coingecko_id:
|
|
52
|
+
return group.coingecko_id
|
|
53
|
+
elif group.network is Network.BITCOIN:
|
|
54
|
+
return "bitcoin"
|
|
55
|
+
elif group.network is Network.ETHEREUM and group.token_address is None:
|
|
56
|
+
return "ethereum"
|
|
57
|
+
elif group.ticker == "ETH":
|
|
58
|
+
return "ethereum"
|
|
59
|
+
elif group.ticker == "USDT":
|
|
60
|
+
return "tether"
|
|
61
|
+
elif group.ticker == "USDC":
|
|
62
|
+
return "usd-coin"
|
|
63
|
+
elif group.ticker == "SOL":
|
|
64
|
+
return "solana"
|
|
65
|
+
|
|
66
|
+
raise ValueError(f"can't get coingecko_id for {group.ticker}")
|
|
@@ -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
|
+
)
|
|
@@ -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=
|
|
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=
|
|
22
|
-
timeout=
|
|
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=
|
|
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)
|
|
@@ -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
|
+
)
|
|
@@ -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.
|
|
26
|
+
case Network.ETHEREUM:
|
|
27
27
|
decimals_res = eth.get_token_decimals(nodes, group.token_address, proxies)
|
|
28
|
-
case Network.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
50
|
-
coins_share[group.
|
|
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
|
|
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
|
|
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,169 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from decimal import Decimal
|
|
4
|
-
|
|
5
|
-
from mm_std import ConcurrentTasks, Result
|
|
6
|
-
from pydantic import BaseModel
|
|
7
|
-
from rich.progress import TaskID
|
|
8
|
-
|
|
9
|
-
from mm_balance import output
|
|
10
|
-
from mm_balance.config import Config
|
|
11
|
-
from mm_balance.rpc import btc, eth, solana
|
|
12
|
-
from mm_balance.token_decimals import TokenDecimals
|
|
13
|
-
from mm_balance.types import Network
|
|
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, self.config)
|
|
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
|
|
@@ -1,153 +0,0 @@
|
|
|
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.types import DEFAULT_ETH_NODES, DEFAULT_SOL_NODES, EthTokenAddress, Network, SolTokenAddress
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
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]
|
|
71
|
-
addresses: list[AddressGroup] = Field(default_factory=list)
|
|
72
|
-
|
|
73
|
-
proxies_url: str | None = None
|
|
74
|
-
proxies: list[str] = Field(default_factory=list)
|
|
75
|
-
round_ndigits: int = 4
|
|
76
|
-
nodes: dict[Network, list[str]] = Field(default_factory=dict)
|
|
77
|
-
print_format: PrintFormat = PrintFormat.TABLE
|
|
78
|
-
price: bool = True
|
|
79
|
-
|
|
80
|
-
workers: dict[Network, int] = {network: 5 for network in Network}
|
|
81
|
-
|
|
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
|
-
def has_share(self) -> bool:
|
|
92
|
-
return any(g.share != Decimal(1) for g in self.groups)
|
|
93
|
-
|
|
94
|
-
@model_validator(mode="after")
|
|
95
|
-
def final_validator(self) -> Self:
|
|
96
|
-
# load from proxies_url
|
|
97
|
-
if self.proxies_url is not None:
|
|
98
|
-
self.proxies = get_proxies(self.proxies_url)
|
|
99
|
-
|
|
100
|
-
# load addresses from address_group
|
|
101
|
-
for group in self.groups:
|
|
102
|
-
group.process_addresses(self.addresses)
|
|
103
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
return self
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
134
|
-
if network == Network.SOL.lower():
|
|
135
|
-
if coin.lower() == "usdt":
|
|
136
|
-
return SolTokenAddress.USDT
|
|
137
|
-
if coin.lower() == "usdc":
|
|
138
|
-
return SolTokenAddress.USDC
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def get_proxies(proxies_url: str) -> list[str]:
|
|
142
|
-
try:
|
|
143
|
-
res = hr(proxies_url)
|
|
144
|
-
if res.is_error():
|
|
145
|
-
fatal(f"Can't get proxies: {res.error}")
|
|
146
|
-
proxies = [p.strip() for p in res.body.splitlines() if p.strip()]
|
|
147
|
-
return pydash.uniq(proxies)
|
|
148
|
-
except Exception as err:
|
|
149
|
-
fatal(f"Can't get proxies: {err}")
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def get_address_group_by_name(address_groups: list[Config.AddressGroup], name: str) -> Config.AddressGroup | None:
|
|
153
|
-
return pydash.find(address_groups, lambda g: g.name == name)
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from decimal import Decimal
|
|
3
|
-
|
|
4
|
-
import pydash
|
|
5
|
-
from mm_std import Err, Ok, Result, fatal, hr
|
|
6
|
-
from mm_std.random_ import random_str_choice
|
|
7
|
-
|
|
8
|
-
from mm_balance import output
|
|
9
|
-
from mm_balance.config import Config
|
|
10
|
-
from mm_balance.types import EthTokenAddress, Network
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Prices(dict[str, Decimal]):
|
|
14
|
-
"""
|
|
15
|
-
A Prices class representing a mapping from coin names to their prices.
|
|
16
|
-
|
|
17
|
-
Inherits from:
|
|
18
|
-
Dict[str, Decimal]: A dictionary with coin names as keys and their prices as Decimal values.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_prices(config: Config) -> Prices:
|
|
23
|
-
result = Prices()
|
|
24
|
-
coins_total = len(pydash.uniq([group.coin for group in config.groups]))
|
|
25
|
-
|
|
26
|
-
progress = output.create_progress_bar()
|
|
27
|
-
|
|
28
|
-
with progress:
|
|
29
|
-
task_id = output.create_progress_task(progress, "prices", total=coins_total)
|
|
30
|
-
|
|
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)
|
|
40
|
-
else:
|
|
41
|
-
fatal(res.err)
|
|
42
|
-
# raise ValueError(res.err)
|
|
43
|
-
|
|
44
|
-
return result
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
70
|
-
if group.coingecko_id:
|
|
71
|
-
return group.coingecko_id
|
|
72
|
-
elif group.network is Network.BTC:
|
|
73
|
-
return "bitcoin"
|
|
74
|
-
elif group.network is Network.ETH and group.token_address is None:
|
|
75
|
-
return "ethereum"
|
|
76
|
-
elif group.coin.lower() == "usdt" or (group.token_address is not None and group.token_address == EthTokenAddress.USDT):
|
|
77
|
-
return "tether"
|
|
78
|
-
elif group.coin.lower() == "usdc" or (group.token_address is not None and group.token_address == EthTokenAddress.USDC):
|
|
79
|
-
return "usd-coin"
|
|
80
|
-
elif group.coin.lower() == "sol":
|
|
81
|
-
return "solana"
|
|
82
|
-
|
|
83
|
-
raise ValueError(f"can't get coingecko_id for {group.coin}")
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from decimal import Decimal
|
|
2
|
-
|
|
3
|
-
from mm_btc.blockstream import BlockstreamClient
|
|
4
|
-
from mm_std import Ok, Result
|
|
5
|
-
from rich.progress import Progress, TaskID
|
|
6
|
-
|
|
7
|
-
from mm_balance.config import Config
|
|
8
|
-
|
|
9
|
-
|
|
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)
|
|
13
|
-
.get_confirmed_balance(address)
|
|
14
|
-
.and_then(
|
|
15
|
-
lambda b: Ok(round(Decimal(b / 100_000_000), config.round_ndigits)),
|
|
16
|
-
)
|
|
17
|
-
)
|
|
18
|
-
if task_id is not None and progress is not None:
|
|
19
|
-
progress.update(task_id, advance=1)
|
|
20
|
-
return res
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from decimal import Decimal
|
|
2
|
-
|
|
3
|
-
from mm_solana import balance, token
|
|
4
|
-
from mm_std import Ok, Result
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
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
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_token_balance(
|
|
14
|
-
nodes: list[str], wallet_address: str, token_address: str, decimals: int, proxies: list[str], round_ndigits: int
|
|
15
|
-
) -> 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
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
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)
|
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|