mm-balance 0.1.12__tar.gz → 0.1.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mm_balance-0.1.12 → mm_balance-0.1.14}/PKG-INFO +3 -2
- {mm_balance-0.1.12 → mm_balance-0.1.14}/pyproject.toml +5 -4
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/balances.py +27 -23
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/cli.py +10 -3
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/config/example.yml +11 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/config.py +39 -39
- mm_balance-0.1.14/src/mm_balance/constants.py +69 -0
- mm_balance-0.1.14/src/mm_balance/output.py +109 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/price.py +5 -23
- mm_balance-0.1.14/src/mm_balance/rpc/aptos.py +23 -0
- mm_balance-0.1.14/src/mm_balance/rpc/evm.py +27 -0
- mm_balance-0.1.14/src/mm_balance/rpc/solana.py +26 -0
- mm_balance-0.1.14/src/mm_balance/token_decimals.py +55 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/total.py +18 -8
- mm_balance-0.1.14/src/mm_balance/utils.py +10 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/uv.lock +78 -64
- mm_balance-0.1.12/src/mm_balance/constants.py +0 -46
- mm_balance-0.1.12/src/mm_balance/output.py +0 -69
- mm_balance-0.1.12/src/mm_balance/rpc/eth.py +0 -31
- mm_balance-0.1.12/src/mm_balance/rpc/solana.py +0 -26
- mm_balance-0.1.12/src/mm_balance/token_decimals.py +0 -37
- {mm_balance-0.1.12 → mm_balance-0.1.14}/.gitignore +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/README.md +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/justfile +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/__init__.py +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/rpc/__init__.py +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/src/mm_balance/rpc/btc.py +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/tests/__init__.py +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/tests/conftest.py +0 -0
- {mm_balance-0.1.12 → mm_balance-0.1.14}/tests/test_dummy.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mm-balance
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
|
+
Requires-Dist: mm-aptos==0.1.2
|
|
5
6
|
Requires-Dist: mm-btc==0.1.0
|
|
6
7
|
Requires-Dist: mm-eth==0.1.3
|
|
7
|
-
Requires-Dist: mm-solana==0.1.
|
|
8
|
+
Requires-Dist: mm-solana==0.1.5
|
|
8
9
|
Requires-Dist: typer>=0.12.5
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-balance"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.14"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"mm-btc==0.1.0",
|
|
8
8
|
"mm-eth==0.1.3",
|
|
9
|
-
"mm-solana==0.1.
|
|
9
|
+
"mm-solana==0.1.5",
|
|
10
|
+
"mm-aptos==0.1.2",
|
|
10
11
|
"typer>=0.12.5",
|
|
11
12
|
]
|
|
12
13
|
[project.scripts]
|
|
@@ -22,10 +23,10 @@ dev-dependencies = [
|
|
|
22
23
|
"pytest-xdist~=3.6.1",
|
|
23
24
|
"pytest-httpserver~=1.1.0",
|
|
24
25
|
"coverage~=7.6.3",
|
|
25
|
-
"ruff~=0.
|
|
26
|
+
"ruff~=0.7.0",
|
|
26
27
|
"pip-audit~=2.7.0",
|
|
27
28
|
"bandit~=1.7.10",
|
|
28
|
-
"mypy~=1.12.
|
|
29
|
+
"mypy~=1.12.1",
|
|
29
30
|
"types-python-dateutil~=2.9.0.20241003",
|
|
30
31
|
"types-PyYAML~=6.0.12.20240917",
|
|
31
32
|
]
|
|
@@ -8,8 +8,8 @@ from rich.progress import TaskID
|
|
|
8
8
|
|
|
9
9
|
from mm_balance import output
|
|
10
10
|
from mm_balance.config import Config
|
|
11
|
-
from mm_balance.constants import Network
|
|
12
|
-
from mm_balance.rpc import btc,
|
|
11
|
+
from mm_balance.constants import NETWORK_APTOS, NETWORK_BITCOIN, NETWORK_SOLANA, Network
|
|
12
|
+
from mm_balance.rpc import aptos, btc, evm, solana
|
|
13
13
|
from mm_balance.token_decimals import TokenDecimals
|
|
14
14
|
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ class Balances:
|
|
|
23
23
|
def __init__(self, config: Config, token_decimals: TokenDecimals):
|
|
24
24
|
self.config = config
|
|
25
25
|
self.token_decimals = token_decimals
|
|
26
|
-
self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in
|
|
26
|
+
self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in config.networks()}
|
|
27
27
|
self.progress_bar = output.create_progress_bar()
|
|
28
28
|
self.progress_bar_task: dict[Network, TaskID] = {}
|
|
29
29
|
|
|
@@ -31,17 +31,17 @@ class Balances:
|
|
|
31
31
|
task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses]
|
|
32
32
|
self.tasks[group.network].extend(task_list)
|
|
33
33
|
|
|
34
|
-
for network in
|
|
34
|
+
for network in config.networks():
|
|
35
35
|
if self.tasks[network]:
|
|
36
36
|
self.progress_bar_task[network] = output.create_progress_task(
|
|
37
|
-
self.progress_bar, network
|
|
37
|
+
self.progress_bar, network, len(self.tasks[network])
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
def process(self) -> None:
|
|
41
41
|
with self.progress_bar:
|
|
42
42
|
job = ConcurrentTasks(max_workers=10)
|
|
43
|
-
for network in
|
|
44
|
-
job.add_task(network
|
|
43
|
+
for network in self.config.networks():
|
|
44
|
+
job.add_task(network, self._process_network, args=(network,))
|
|
45
45
|
job.execute()
|
|
46
46
|
|
|
47
47
|
def _process_network(self, network: Network) -> None:
|
|
@@ -49,6 +49,7 @@ class Balances:
|
|
|
49
49
|
for idx, task in enumerate(self.tasks[network]):
|
|
50
50
|
job.add_task(str(idx), self._get_balance, args=(network, task.address, task.token_address))
|
|
51
51
|
job.execute()
|
|
52
|
+
# TODO: print job.exceptions if present
|
|
52
53
|
for idx, _task in enumerate(self.tasks[network]):
|
|
53
54
|
self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
|
|
54
55
|
|
|
@@ -56,23 +57,18 @@ class Balances:
|
|
|
56
57
|
nodes = self.config.nodes[network]
|
|
57
58
|
round_ndigits = self.config.round_ndigits
|
|
58
59
|
proxies = self.config.proxies
|
|
59
|
-
token_decimals = self.token_decimals[network][token_address]
|
|
60
|
-
match network:
|
|
61
|
-
case Network.BITCOIN:
|
|
62
|
-
res = btc.get_balance(wallet_address, proxies, round_ndigits)
|
|
63
|
-
case Network.ETHEREUM | Network.ARBITRUM_ONE | Network.OP_MAINNET:
|
|
64
|
-
if token_address is None:
|
|
65
|
-
res = eth.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
|
|
66
|
-
else:
|
|
67
|
-
res = eth.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
68
|
-
case Network.SOLANA:
|
|
69
|
-
if token_address is None:
|
|
70
|
-
res = solana.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
|
|
71
|
-
else:
|
|
72
|
-
res = solana.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
60
|
+
token_decimals = self.token_decimals[network][token_address]
|
|
73
61
|
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
if network.is_evm_network():
|
|
63
|
+
res = evm.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
64
|
+
elif network == NETWORK_BITCOIN:
|
|
65
|
+
res = btc.get_balance(wallet_address, proxies, round_ndigits)
|
|
66
|
+
elif network == NETWORK_APTOS:
|
|
67
|
+
res = aptos.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
68
|
+
elif network == NETWORK_SOLANA:
|
|
69
|
+
res = solana.get_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f"Unsupported network: {network}")
|
|
76
72
|
|
|
77
73
|
self.progress_bar.update(self.progress_bar_task[network], advance=1)
|
|
78
74
|
return res
|
|
@@ -80,3 +76,11 @@ class Balances:
|
|
|
80
76
|
def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
|
|
81
77
|
# TODO: can we get network by group_index?
|
|
82
78
|
return [b for b in self.tasks[network] if b.group_index == group_index]
|
|
79
|
+
|
|
80
|
+
def get_errors(self) -> list[Balance]:
|
|
81
|
+
result = []
|
|
82
|
+
for network in self.tasks:
|
|
83
|
+
for task in self.tasks[network]:
|
|
84
|
+
if task.balance is not None and task.balance.is_err():
|
|
85
|
+
result.append(task)
|
|
86
|
+
return result
|
|
@@ -8,7 +8,7 @@ import typer
|
|
|
8
8
|
from mm_balance import output
|
|
9
9
|
from mm_balance.balances import Balances
|
|
10
10
|
from mm_balance.config import Config
|
|
11
|
-
from mm_balance.constants import
|
|
11
|
+
from mm_balance.constants import NETWORKS
|
|
12
12
|
from mm_balance.price import Prices, get_prices
|
|
13
13
|
from mm_balance.token_decimals import get_token_decimals
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ def example_callback(value: bool) -> None:
|
|
|
24
24
|
|
|
25
25
|
def networks_callback(value: bool) -> None:
|
|
26
26
|
if value:
|
|
27
|
-
for network in
|
|
27
|
+
for network in NETWORKS:
|
|
28
28
|
typer.echo(network)
|
|
29
29
|
raise typer.Exit
|
|
30
30
|
|
|
@@ -42,15 +42,22 @@ def cli(
|
|
|
42
42
|
zip_password = getpass.getpass("zip password")
|
|
43
43
|
config = Config.read_config(config_path, zip_password=zip_password)
|
|
44
44
|
|
|
45
|
+
if config.print_debug:
|
|
46
|
+
output.print_nodes(config)
|
|
47
|
+
|
|
48
|
+
token_decimals = get_token_decimals(config)
|
|
49
|
+
if config.print_debug:
|
|
50
|
+
output.print_token_decimals(token_decimals)
|
|
51
|
+
|
|
45
52
|
prices = get_prices(config) if config.price else Prices()
|
|
46
53
|
output.print_prices(config, prices)
|
|
47
54
|
|
|
48
|
-
token_decimals = get_token_decimals(config)
|
|
49
55
|
balances = Balances(config, token_decimals)
|
|
50
56
|
balances.process()
|
|
51
57
|
|
|
52
58
|
output.print_groups(balances, config, prices)
|
|
53
59
|
output.print_total(config, balances, prices)
|
|
60
|
+
output.print_errors(config, balances)
|
|
54
61
|
|
|
55
62
|
|
|
56
63
|
if __name__ == "__main__":
|
|
@@ -38,6 +38,14 @@ coins:
|
|
|
38
38
|
comment: binance
|
|
39
39
|
addresses: binance_eth
|
|
40
40
|
|
|
41
|
+
- ticker: USDC
|
|
42
|
+
comment: okx aptos
|
|
43
|
+
network: aptos
|
|
44
|
+
token_address: 0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::USDC
|
|
45
|
+
token_decimals: 6
|
|
46
|
+
addresses: "0x834d639b10d20dcb894728aa4b9b572b2ea2d97073b10eacb111f338b20ea5d7" # for a single line it's necessary to use quotes
|
|
47
|
+
|
|
48
|
+
|
|
41
49
|
addresses:
|
|
42
50
|
- name: okx_eth
|
|
43
51
|
addresses: |
|
|
@@ -60,3 +68,6 @@ addresses:
|
|
|
60
68
|
#- http://123.123.123.124
|
|
61
69
|
#round_ndigits: 4
|
|
62
70
|
#price: yes
|
|
71
|
+
#skip_empty: no
|
|
72
|
+
#print_debug: no
|
|
73
|
+
#format_number_separator: ","
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from typing import Self
|
|
5
6
|
|
|
6
7
|
import pydash
|
|
7
8
|
from mm_std import BaseConfig, PrintFormat, fatal, hr
|
|
8
9
|
from pydantic import Field, field_validator, model_validator
|
|
9
10
|
|
|
10
|
-
from mm_balance.constants import
|
|
11
|
-
DEFAULT_ARBITRUM_ONE_NODES,
|
|
12
|
-
DEFAULT_ETHEREUM_NODES,
|
|
13
|
-
DEFAULT_SOLANA_NODES,
|
|
14
|
-
TOKEN_ADDRESS,
|
|
15
|
-
Network,
|
|
16
|
-
)
|
|
11
|
+
from mm_balance.constants import DEFAULT_NODES, TOKEN_ADDRESS, Network
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
class Group(BaseConfig):
|
|
20
15
|
comment: str = ""
|
|
21
16
|
ticker: str
|
|
22
17
|
network: Network
|
|
23
|
-
token_address: str | None = None
|
|
18
|
+
token_address: str | None = None # If None, it's a native token, for example ETH
|
|
19
|
+
token_decimals: int | None = None
|
|
24
20
|
coingecko_id: str | None = None
|
|
25
21
|
addresses: list[str] = Field(default_factory=list)
|
|
26
22
|
share: Decimal = Decimal(1)
|
|
@@ -30,11 +26,11 @@ class Group(BaseConfig):
|
|
|
30
26
|
result = self.ticker
|
|
31
27
|
if self.comment:
|
|
32
28
|
result += " / " + self.comment
|
|
33
|
-
result += " / " + self.network
|
|
29
|
+
result += " / " + self.network
|
|
34
30
|
return result
|
|
35
31
|
|
|
36
32
|
@field_validator("ticker", mode="after")
|
|
37
|
-
def
|
|
33
|
+
def ticker_validator(cls, v: str) -> str:
|
|
38
34
|
return v.upper()
|
|
39
35
|
|
|
40
36
|
@field_validator("addresses", mode="before")
|
|
@@ -51,7 +47,7 @@ class Group(BaseConfig):
|
|
|
51
47
|
def final_validator(self) -> Self:
|
|
52
48
|
if self.token_address is None:
|
|
53
49
|
self.token_address = detect_token_address(self.ticker, self.network)
|
|
54
|
-
if self.token_address is not None and self.network
|
|
50
|
+
if self.token_address is not None and self.network.is_evm_network():
|
|
55
51
|
self.token_address = self.token_address.lower()
|
|
56
52
|
return self
|
|
57
53
|
|
|
@@ -63,7 +59,7 @@ class Group(BaseConfig):
|
|
|
63
59
|
else:
|
|
64
60
|
# TODO: check address is valid
|
|
65
61
|
addresses.append(address)
|
|
66
|
-
self.addresses = addresses
|
|
62
|
+
self.addresses = pydash.uniq(process_file_addresses(addresses))
|
|
67
63
|
|
|
68
64
|
|
|
69
65
|
class AddressGroup(BaseConfig):
|
|
@@ -85,12 +81,17 @@ class Config(BaseConfig):
|
|
|
85
81
|
nodes: dict[Network, list[str]] = Field(default_factory=dict)
|
|
86
82
|
print_format: PrintFormat = PrintFormat.TABLE
|
|
87
83
|
price: bool = True
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
skip_empty: bool = False # don't print the address with an empty balance
|
|
85
|
+
print_debug: bool = False # print debug info: nodes, token_decimals
|
|
86
|
+
format_number_separator: str = "," # as thousands separators
|
|
87
|
+
workers: dict[Network, int] = Field(default_factory=dict)
|
|
90
88
|
|
|
91
89
|
def has_share(self) -> bool:
|
|
92
90
|
return any(g.share != Decimal(1) for g in self.groups)
|
|
93
91
|
|
|
92
|
+
def networks(self) -> list[Network]:
|
|
93
|
+
return pydash.uniq([group.network for group in self.groups])
|
|
94
|
+
|
|
94
95
|
@model_validator(mode="after")
|
|
95
96
|
def final_validator(self) -> Self:
|
|
96
97
|
# load from proxies_url
|
|
@@ -102,36 +103,21 @@ class Config(BaseConfig):
|
|
|
102
103
|
group.process_addresses(self.addresses)
|
|
103
104
|
|
|
104
105
|
# load default rpc nodes
|
|
105
|
-
|
|
106
|
-
self.nodes
|
|
107
|
-
|
|
108
|
-
self.nodes[Network.ETHEREUM] = DEFAULT_ETHEREUM_NODES
|
|
109
|
-
if Network.ARBITRUM_ONE not in self.nodes:
|
|
110
|
-
self.nodes[Network.ARBITRUM_ONE] = DEFAULT_ARBITRUM_ONE_NODES
|
|
111
|
-
if Network.OP_MAINNET not in self.nodes:
|
|
112
|
-
self.nodes[Network.OP_MAINNET] = DEFAULT_ARBITRUM_ONE_NODES
|
|
113
|
-
if Network.SOLANA not in self.nodes:
|
|
114
|
-
self.nodes[Network.SOLANA] = DEFAULT_SOLANA_NODES
|
|
115
|
-
|
|
116
|
-
return self
|
|
106
|
+
for network in self.networks():
|
|
107
|
+
if network not in self.nodes:
|
|
108
|
+
self.nodes[network] = DEFAULT_NODES[network]
|
|
117
109
|
|
|
110
|
+
# load default workers
|
|
111
|
+
for network in self.networks():
|
|
112
|
+
if network not in self.workers:
|
|
113
|
+
self.workers[network] = 5
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
# # coin = coin.lower()
|
|
122
|
-
# # if coin == "btc":
|
|
123
|
-
# # return Network.BTC
|
|
124
|
-
# # if coin == "eth":
|
|
125
|
-
# # return Network.ETH
|
|
126
|
-
# # if coin == "sol":
|
|
127
|
-
# # return Network.SOL
|
|
128
|
-
# # return Network.ETH
|
|
129
|
-
# # # TODO: raise ValueError(f"can't get network for the coin: {coin}")
|
|
115
|
+
return self
|
|
130
116
|
|
|
131
117
|
|
|
132
|
-
def detect_token_address(
|
|
118
|
+
def detect_token_address(ticker: str, network: Network) -> str | None:
|
|
133
119
|
if network in TOKEN_ADDRESS:
|
|
134
|
-
return TOKEN_ADDRESS[network].get(
|
|
120
|
+
return TOKEN_ADDRESS[network].get(ticker)
|
|
135
121
|
|
|
136
122
|
|
|
137
123
|
def get_proxies(proxies_url: str) -> list[str]:
|
|
@@ -147,3 +133,17 @@ def get_proxies(proxies_url: str) -> list[str]:
|
|
|
147
133
|
|
|
148
134
|
def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
|
|
149
135
|
return pydash.find(address_groups, lambda g: g.name == name)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def process_file_addresses(addresses: list[str]) -> list[str]:
|
|
139
|
+
result = []
|
|
140
|
+
for address in addresses:
|
|
141
|
+
if address.startswith("file://"):
|
|
142
|
+
path = Path(address.removeprefix("file://"))
|
|
143
|
+
if path.is_file():
|
|
144
|
+
result.extend(path.read_text().strip().splitlines())
|
|
145
|
+
else:
|
|
146
|
+
fatal(f"File with addresses not found: {path}")
|
|
147
|
+
else:
|
|
148
|
+
result.append(address)
|
|
149
|
+
return result
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
|
4
|
+
from pydantic_core import CoreSchema, core_schema
|
|
5
|
+
|
|
6
|
+
RETRIES_BALANCE = 5
|
|
7
|
+
RETRIES_DECIMALS = 5
|
|
8
|
+
RETRIES_COINGECKO_PRICES = 5
|
|
9
|
+
TIMEOUT_BALANCE = 5
|
|
10
|
+
TIMEOUT_DECIMALS = 5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Network(str):
|
|
14
|
+
def is_evm_network(self) -> bool:
|
|
15
|
+
return self in [NETWORK_ETHEREUM, NETWORK_ARBITRUM_ONE, NETWORK_OP_MAINNET] or self.startswith("evm-")
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def __get_pydantic_core_schema__(cls, _source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
19
|
+
return core_schema.no_info_after_validator_function(cls, handler(str))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
NETWORK_APTOS = Network("aptos")
|
|
23
|
+
NETWORK_ARBITRUM_ONE = Network("arbitrum-one")
|
|
24
|
+
NETWORK_BITCOIN = Network("bitcoin")
|
|
25
|
+
NETWORK_ETHEREUM = Network("ethereum")
|
|
26
|
+
NETWORK_SOLANA = Network("solana")
|
|
27
|
+
NETWORK_OP_MAINNET = Network("op-mainnet")
|
|
28
|
+
NETWORKS = [NETWORK_APTOS, NETWORK_ARBITRUM_ONE, NETWORK_BITCOIN, NETWORK_ETHEREUM, NETWORK_SOLANA, NETWORK_OP_MAINNET]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TOKEN_ADDRESS: dict[Network, dict[str, str]] = {
|
|
32
|
+
NETWORK_ETHEREUM: {
|
|
33
|
+
"USDT": "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
|
34
|
+
"USDC": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
|
35
|
+
},
|
|
36
|
+
NETWORK_SOLANA: {
|
|
37
|
+
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
|
38
|
+
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
39
|
+
},
|
|
40
|
+
NETWORK_ARBITRUM_ONE: {
|
|
41
|
+
"USDT": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
|
|
42
|
+
"USDC": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
|
|
43
|
+
},
|
|
44
|
+
NETWORK_OP_MAINNET: {
|
|
45
|
+
"USDT": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
|
|
46
|
+
"USDC": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
TICKER_TO_COINGECKO_ID = {
|
|
51
|
+
"BTC": "bitcoin",
|
|
52
|
+
"ETH": "ethereum",
|
|
53
|
+
"USDT": "tether",
|
|
54
|
+
"USDC": "usd-coin",
|
|
55
|
+
"SOL": "solana",
|
|
56
|
+
"APT": "aptos",
|
|
57
|
+
"POL": "matic-network",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
USD_STABLECOINS = ["USDT", "USDC"]
|
|
61
|
+
|
|
62
|
+
DEFAULT_NODES: dict[Network, list[str]] = {
|
|
63
|
+
NETWORK_ARBITRUM_ONE: ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com"],
|
|
64
|
+
NETWORK_BITCOIN: [],
|
|
65
|
+
NETWORK_ETHEREUM: ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"],
|
|
66
|
+
NETWORK_SOLANA: ["https://api.mainnet-beta.solana.com"],
|
|
67
|
+
NETWORK_OP_MAINNET: ["https://mainnet.optimism.io", "https://optimism.llamarpc.com"],
|
|
68
|
+
NETWORK_APTOS: ["https://fullnode.mainnet.aptoslabs.com/v1"],
|
|
69
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_std import Err, Ok, fatal, print_table
|
|
4
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskID, TextColumn
|
|
5
|
+
|
|
6
|
+
from mm_balance.balances import Balances
|
|
7
|
+
from mm_balance.config import Config, Group
|
|
8
|
+
from mm_balance.price import Prices
|
|
9
|
+
from mm_balance.token_decimals import TokenDecimals
|
|
10
|
+
from mm_balance.total import Total
|
|
11
|
+
from mm_balance.utils import fnumber
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def print_groups(balances: Balances, config: Config, prices: Prices) -> None:
|
|
15
|
+
for group_index, group in enumerate(config.groups):
|
|
16
|
+
group_balances = balances.get_group_balances(group_index, group.network)
|
|
17
|
+
_print_group(group, group_balances, config, prices)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _print_group(group: Group, group_balances: list[Balances.Balance], config: Config, prices: Prices) -> None:
|
|
21
|
+
rows = []
|
|
22
|
+
balance_sum = Decimal(0)
|
|
23
|
+
usd_sum = Decimal(0)
|
|
24
|
+
for address_task in group_balances:
|
|
25
|
+
if isinstance(address_task.balance, Err):
|
|
26
|
+
row = [address_task.address, address_task.balance.err]
|
|
27
|
+
elif isinstance(address_task.balance, Ok):
|
|
28
|
+
balance = address_task.balance.ok
|
|
29
|
+
balance_sum += balance
|
|
30
|
+
if config.skip_empty and balance == Decimal(0):
|
|
31
|
+
continue
|
|
32
|
+
row = [address_task.address, fnumber(balance, config.format_number_separator)]
|
|
33
|
+
if config.price:
|
|
34
|
+
balance_usd = round(balance * prices[group.ticker], config.round_ndigits)
|
|
35
|
+
usd_sum += balance_usd
|
|
36
|
+
row.append(fnumber(balance_usd, config.format_number_separator, "$"))
|
|
37
|
+
else:
|
|
38
|
+
fatal("address_task is None!")
|
|
39
|
+
rows.append(row)
|
|
40
|
+
|
|
41
|
+
balance_sum_str = fnumber(round(balance_sum, config.round_ndigits), config.format_number_separator)
|
|
42
|
+
sum_row = ["sum", balance_sum_str]
|
|
43
|
+
if config.price:
|
|
44
|
+
usd_sum_str = fnumber(round(usd_sum, config.round_ndigits), config.format_number_separator, "$")
|
|
45
|
+
sum_row.append(usd_sum_str)
|
|
46
|
+
rows.append(sum_row)
|
|
47
|
+
|
|
48
|
+
if group.share < Decimal(1):
|
|
49
|
+
sum_share_str = fnumber(round(balance_sum * group.share, config.round_ndigits), config.format_number_separator)
|
|
50
|
+
sum_share_row = [f"sum_share, {group.share}", sum_share_str]
|
|
51
|
+
if config.price:
|
|
52
|
+
usd_sum_share_str = fnumber(round(usd_sum * group.share, config.round_ndigits), config.format_number_separator, "$")
|
|
53
|
+
sum_share_row.append(usd_sum_share_str)
|
|
54
|
+
rows.append(sum_share_row)
|
|
55
|
+
|
|
56
|
+
table_headers = ["address", "balance"]
|
|
57
|
+
if config.price:
|
|
58
|
+
table_headers += ["usd"]
|
|
59
|
+
print_table(group.name, table_headers, rows)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_nodes(config: Config) -> None:
|
|
63
|
+
rows = []
|
|
64
|
+
for network, nodes in config.nodes.items():
|
|
65
|
+
rows.append([network, "\n".join(nodes)])
|
|
66
|
+
print_table("Nodes", ["network", "nodes"], rows)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_token_decimals(token_decimals: TokenDecimals) -> None:
|
|
70
|
+
rows = []
|
|
71
|
+
for network, decimals in token_decimals.items():
|
|
72
|
+
rows.append([network, decimals])
|
|
73
|
+
print_table("Token Decimals", ["network", "decimals"], rows)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def print_prices(config: Config, prices: Prices) -> None:
|
|
77
|
+
if config.price:
|
|
78
|
+
rows = []
|
|
79
|
+
for ticker, price in prices.items():
|
|
80
|
+
rows.append([ticker, fnumber(round(price, config.round_ndigits), config.format_number_separator, "$")])
|
|
81
|
+
print_table("Prices", ["coin", "usd"], rows)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_total(config: Config, balances: Balances, prices: Prices) -> None:
|
|
85
|
+
total = Total.calc(balances, prices, config)
|
|
86
|
+
total.print()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def print_errors(config: Config, balances: Balances) -> None:
|
|
90
|
+
error_balances = balances.get_errors()
|
|
91
|
+
if not error_balances:
|
|
92
|
+
return
|
|
93
|
+
rows = []
|
|
94
|
+
for balance in error_balances:
|
|
95
|
+
group = config.groups[balance.group_index]
|
|
96
|
+
rows.append([group.ticker + " / " + group.network, balance.address, balance.balance.err]) # type: ignore[union-attr]
|
|
97
|
+
print_table("Errors", ["coin", "address", "error"], rows)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_progress_bar() -> Progress:
|
|
101
|
+
return Progress(
|
|
102
|
+
TextColumn("[progress.description]{task.description}"),
|
|
103
|
+
BarColumn(),
|
|
104
|
+
MofNCompleteColumn(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_progress_task(progress: Progress, description: str, total: int) -> TaskID:
|
|
109
|
+
return progress.add_task("[green]" + description, total=total)
|
|
@@ -5,7 +5,7 @@ from mm_std import fatal, hr
|
|
|
5
5
|
from mm_std.random_ import random_str_choice
|
|
6
6
|
|
|
7
7
|
from mm_balance.config import Config, Group
|
|
8
|
-
from mm_balance.constants import RETRIES_COINGECKO_PRICES,
|
|
8
|
+
from mm_balance.constants import RETRIES_COINGECKO_PRICES, TICKER_TO_COINGECKO_ID
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Prices(dict[str, Decimal]):
|
|
@@ -38,29 +38,11 @@ def get_prices(config: Config) -> Prices:
|
|
|
38
38
|
return result
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
TICKER_TO_COINGECKO_ID = {
|
|
42
|
-
"BTC": "bitcoin",
|
|
43
|
-
"ETH": "ethereum",
|
|
44
|
-
"USDT": "tether",
|
|
45
|
-
"USDC": "usd-coin",
|
|
46
|
-
"SOL": "solana",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
41
|
def get_coingecko_id(group: Group) -> str:
|
|
51
42
|
if group.coingecko_id:
|
|
52
43
|
return group.coingecko_id
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return "ethereum"
|
|
57
|
-
elif group.ticker == "ETH":
|
|
58
|
-
return "ethereum"
|
|
59
|
-
elif group.ticker == "USDT":
|
|
60
|
-
return "tether"
|
|
61
|
-
elif group.ticker == "USDC":
|
|
62
|
-
return "usd-coin"
|
|
63
|
-
elif group.ticker == "SOL":
|
|
64
|
-
return "solana"
|
|
44
|
+
coingecko_id = TICKER_TO_COINGECKO_ID.get(group.ticker)
|
|
45
|
+
if coingecko_id:
|
|
46
|
+
return coingecko_id
|
|
65
47
|
|
|
66
|
-
|
|
48
|
+
fatal(f"Can't get coingecko_id for {group.ticker}. Please add coingecko_id to the config.")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_aptos import balance
|
|
4
|
+
from mm_std import Result
|
|
5
|
+
|
|
6
|
+
from mm_balance.constants import RETRIES_BALANCE, TIMEOUT_BALANCE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_balance(
|
|
10
|
+
nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
|
|
11
|
+
) -> Result[Decimal]:
|
|
12
|
+
if token is None:
|
|
13
|
+
token = "0x1::aptos_coin::AptosCoin" # nosec
|
|
14
|
+
return balance.get_decimal_balance_with_retries(
|
|
15
|
+
RETRIES_BALANCE,
|
|
16
|
+
nodes,
|
|
17
|
+
wallet,
|
|
18
|
+
token,
|
|
19
|
+
decimals=decimals,
|
|
20
|
+
timeout=TIMEOUT_BALANCE,
|
|
21
|
+
proxies=proxies,
|
|
22
|
+
round_ndigits=round_ndigits,
|
|
23
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_eth import erc20, rpc
|
|
4
|
+
from mm_std import Ok, Result
|
|
5
|
+
|
|
6
|
+
from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_balance(
|
|
10
|
+
nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
|
|
11
|
+
) -> Result[Decimal]:
|
|
12
|
+
if token is not None:
|
|
13
|
+
res = erc20.get_balance(
|
|
14
|
+
nodes,
|
|
15
|
+
token,
|
|
16
|
+
wallet,
|
|
17
|
+
proxies=proxies,
|
|
18
|
+
attempts=RETRIES_BALANCE,
|
|
19
|
+
timeout=TIMEOUT_BALANCE,
|
|
20
|
+
)
|
|
21
|
+
else:
|
|
22
|
+
res = rpc.eth_get_balance(nodes, wallet, proxies=proxies, attempts=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE)
|
|
23
|
+
return res.and_then(lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
|
|
27
|
+
return erc20.get_decimals(nodes, token_address, timeout=TIMEOUT_DECIMALS, proxies=proxies, attempts=RETRIES_DECIMALS)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_solana import balance
|
|
4
|
+
from mm_solana import token as solana_token
|
|
5
|
+
from mm_std import Ok, Result
|
|
6
|
+
|
|
7
|
+
from mm_balance.constants import RETRIES_BALANCE, RETRIES_DECIMALS, TIMEOUT_BALANCE, TIMEOUT_DECIMALS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_balance(
|
|
11
|
+
nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
|
|
12
|
+
) -> Result[Decimal]:
|
|
13
|
+
if token is None:
|
|
14
|
+
res = balance.get_balance_with_retries(nodes, wallet, retries=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE, proxies=proxies)
|
|
15
|
+
else:
|
|
16
|
+
res = solana_token.get_balance_with_retries(
|
|
17
|
+
nodes, wallet, token, retries=RETRIES_BALANCE, timeout=TIMEOUT_BALANCE, proxies=proxies
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return res.and_then(lambda b: Ok(round(Decimal(b / 10**decimals), round_ndigits)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_token_decimals(nodes: list[str], token_address: str, proxies: list[str]) -> Result[int]:
|
|
24
|
+
return solana_token.get_decimals_with_retries(
|
|
25
|
+
nodes, token_address, retries=RETRIES_DECIMALS, timeout=TIMEOUT_DECIMALS, proxies=proxies
|
|
26
|
+
)
|