mm-balance 0.1.11__tar.gz → 0.1.13__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.11 → mm_balance-0.1.13}/PKG-INFO +2 -2
- {mm_balance-0.1.11 → mm_balance-0.1.13}/pyproject.toml +2 -2
- mm_balance-0.1.13/src/mm_balance/balances.py +90 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/cli.py +12 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/config/example.yml +15 -8
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/config.py +37 -44
- mm_balance-0.1.13/src/mm_balance/constants.py +56 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/output.py +14 -1
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/price.py +19 -8
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/token_decimals.py +3 -3
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/total.py +7 -7
- {mm_balance-0.1.11 → mm_balance-0.1.13}/uv.lock +5 -5
- mm_balance-0.1.11/src/mm_balance/balances.py +0 -169
- mm_balance-0.1.11/src/mm_balance/constants.py +0 -45
- {mm_balance-0.1.11 → mm_balance-0.1.13}/.gitignore +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/README.md +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/justfile +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/__init__.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/rpc/__init__.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/rpc/btc.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/rpc/eth.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/src/mm_balance/rpc/solana.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/tests/__init__.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/tests/conftest.py +0 -0
- {mm_balance-0.1.11 → mm_balance-0.1.13}/tests/test_dummy.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mm-balance
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
5
|
Requires-Dist: mm-btc==0.1.0
|
|
6
6
|
Requires-Dist: mm-eth==0.1.3
|
|
7
|
-
Requires-Dist: mm-solana==0.1.
|
|
7
|
+
Requires-Dist: mm-solana==0.1.5
|
|
8
8
|
Requires-Dist: typer>=0.12.5
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-balance"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.13"
|
|
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
10
|
"typer>=0.12.5",
|
|
11
11
|
]
|
|
12
12
|
[project.scripts]
|
|
@@ -0,0 +1,90 @@
|
|
|
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]
|
|
83
|
+
|
|
84
|
+
def get_errors(self) -> list[Balance]:
|
|
85
|
+
result = []
|
|
86
|
+
for network in self.tasks:
|
|
87
|
+
for task in self.tasks[network]:
|
|
88
|
+
if task.balance is not None and task.balance.is_err():
|
|
89
|
+
result.append(task)
|
|
90
|
+
return result
|
|
@@ -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"):
|
|
@@ -40,6 +51,7 @@ def cli(
|
|
|
40
51
|
|
|
41
52
|
output.print_groups(balances, config, prices)
|
|
42
53
|
output.print_total(config, balances, prices)
|
|
54
|
+
output.print_errors(config, balances)
|
|
43
55
|
|
|
44
56
|
|
|
45
57
|
if __name__ == "__main__":
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
coins:
|
|
2
|
-
-
|
|
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 @@ coins:
|
|
|
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
|
|
|
@@ -54,3 +60,4 @@ addresses:
|
|
|
54
60
|
#- http://123.123.123.124
|
|
55
61
|
#round_ndigits: 4
|
|
56
62
|
#price: yes
|
|
63
|
+
#skip_empty: no
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
|
-
from
|
|
4
|
+
from pathlib import Path
|
|
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
|
+
from mm_balance.constants import DEFAULT_NODES, TOKEN_ADDRESS, Network
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class Group(BaseConfig):
|
|
14
15
|
comment: str = ""
|
|
15
|
-
|
|
16
|
+
ticker: str
|
|
16
17
|
network: Network
|
|
17
18
|
token_address: str | None = None
|
|
18
19
|
coingecko_id: str | None = None
|
|
@@ -21,30 +22,31 @@ class Group(BaseConfig):
|
|
|
21
22
|
|
|
22
23
|
@property
|
|
23
24
|
def name(self) -> str:
|
|
24
|
-
result = self.
|
|
25
|
+
result = self.ticker
|
|
25
26
|
if self.comment:
|
|
26
27
|
result += " / " + self.comment
|
|
28
|
+
result += " / " + self.network.value
|
|
27
29
|
return result
|
|
28
30
|
|
|
29
|
-
@field_validator("
|
|
30
|
-
def
|
|
31
|
+
@field_validator("ticker", mode="after")
|
|
32
|
+
def ticker_validator(cls, v: str) -> str:
|
|
31
33
|
return v.upper()
|
|
32
34
|
|
|
33
35
|
@field_validator("addresses", mode="before")
|
|
34
36
|
def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
35
37
|
return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
|
|
36
38
|
|
|
37
|
-
@model_validator(mode="before")
|
|
38
|
-
def before_all(cls, data: Any) -> Any:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
# @model_validator(mode="before")
|
|
40
|
+
# def before_all(cls, data: Any) -> Any:
|
|
41
|
+
# if "network" not in data:
|
|
42
|
+
# data["network"] = detect_network(data["coin"])
|
|
43
|
+
# return data
|
|
42
44
|
|
|
43
45
|
@model_validator(mode="after")
|
|
44
46
|
def final_validator(self) -> Self:
|
|
45
47
|
if self.token_address is None:
|
|
46
|
-
self.token_address = detect_token_address(self.
|
|
47
|
-
if self.token_address is not None and self.network is Network.
|
|
48
|
+
self.token_address = detect_token_address(self.ticker, self.network)
|
|
49
|
+
if self.token_address is not None and self.network is Network.ETHEREUM:
|
|
48
50
|
self.token_address = self.token_address.lower()
|
|
49
51
|
return self
|
|
50
52
|
|
|
@@ -56,7 +58,7 @@ class Group(BaseConfig):
|
|
|
56
58
|
else:
|
|
57
59
|
# TODO: check address is valid
|
|
58
60
|
addresses.append(address)
|
|
59
|
-
self.addresses = addresses
|
|
61
|
+
self.addresses = pydash.uniq(process_file_addresses(addresses))
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
class AddressGroup(BaseConfig):
|
|
@@ -78,6 +80,7 @@ class Config(BaseConfig):
|
|
|
78
80
|
nodes: dict[Network, list[str]] = Field(default_factory=dict)
|
|
79
81
|
print_format: PrintFormat = PrintFormat.TABLE
|
|
80
82
|
price: bool = True
|
|
83
|
+
skip_empty: bool = False # don't print the address with an empty balance
|
|
81
84
|
|
|
82
85
|
workers: dict[Network, int] = {network: 5 for network in Network}
|
|
83
86
|
|
|
@@ -95,40 +98,16 @@ class Config(BaseConfig):
|
|
|
95
98
|
group.process_addresses(self.addresses)
|
|
96
99
|
|
|
97
100
|
# load default rpc nodes
|
|
98
|
-
|
|
99
|
-
self.nodes
|
|
100
|
-
|
|
101
|
-
self.nodes[Network.ETH] = DEFAULT_ETH_NODES
|
|
102
|
-
if Network.SOL not in self.nodes:
|
|
103
|
-
self.nodes[Network.SOL] = DEFAULT_SOL_NODES
|
|
101
|
+
for network in Network:
|
|
102
|
+
if network not in self.nodes:
|
|
103
|
+
self.nodes[network] = DEFAULT_NODES[network]
|
|
104
104
|
|
|
105
105
|
return self
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return Network.BTC
|
|
112
|
-
if coin == "eth":
|
|
113
|
-
return Network.ETH
|
|
114
|
-
if coin == "sol":
|
|
115
|
-
return Network.SOL
|
|
116
|
-
return Network.ETH
|
|
117
|
-
# TODO: raise ValueError(f"can't get network for the coin: {coin}")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def detect_token_address(coin: str, network: str) -> str | None:
|
|
121
|
-
if network == Network.ETH.lower():
|
|
122
|
-
if coin.lower() == "usdt":
|
|
123
|
-
return EthTokenAddress.USDT
|
|
124
|
-
if coin.lower() == "usdc":
|
|
125
|
-
return EthTokenAddress.USDC
|
|
126
|
-
|
|
127
|
-
if network == Network.SOL.lower():
|
|
128
|
-
if coin.lower() == "usdt":
|
|
129
|
-
return SolTokenAddress.USDT
|
|
130
|
-
if coin.lower() == "usdc":
|
|
131
|
-
return SolTokenAddress.USDC
|
|
108
|
+
def detect_token_address(ticker: str, network: Network) -> str | None:
|
|
109
|
+
if network in TOKEN_ADDRESS:
|
|
110
|
+
return TOKEN_ADDRESS[network].get(ticker)
|
|
132
111
|
|
|
133
112
|
|
|
134
113
|
def get_proxies(proxies_url: str) -> list[str]:
|
|
@@ -144,3 +123,17 @@ def get_proxies(proxies_url: str) -> list[str]:
|
|
|
144
123
|
|
|
145
124
|
def get_address_group_by_name(address_groups: list[AddressGroup], name: str) -> AddressGroup | None:
|
|
146
125
|
return pydash.find(address_groups, lambda g: g.name == name)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def process_file_addresses(addresses: list[str]) -> list[str]:
|
|
129
|
+
result = []
|
|
130
|
+
for address in addresses:
|
|
131
|
+
if address.startswith("file://"):
|
|
132
|
+
path = Path(address.removeprefix("file://"))
|
|
133
|
+
if path.is_file():
|
|
134
|
+
result.extend(path.read_text().strip().splitlines())
|
|
135
|
+
else:
|
|
136
|
+
fatal(f"File with addresses not found: {path}")
|
|
137
|
+
else:
|
|
138
|
+
result.append(address)
|
|
139
|
+
return result
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
Network.ARBITRUM_ONE: {
|
|
31
|
+
"USDT": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
|
|
32
|
+
"USDC": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
|
|
33
|
+
},
|
|
34
|
+
Network.OP_MAINNET: {
|
|
35
|
+
"USDT": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
|
|
36
|
+
"USDC": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
TICKER_TO_COINGECKO_ID = {
|
|
41
|
+
"BTC": "bitcoin",
|
|
42
|
+
"ETH": "ethereum",
|
|
43
|
+
"USDT": "tether",
|
|
44
|
+
"USDC": "usd-coin",
|
|
45
|
+
"SOL": "solana",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
USD_STABLECOINS = ["USDT", "USDC"]
|
|
49
|
+
|
|
50
|
+
DEFAULT_NODES: dict[Network, list[str]] = {
|
|
51
|
+
Network.ARBITRUM_ONE: ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com"],
|
|
52
|
+
Network.BITCOIN: [],
|
|
53
|
+
Network.ETHEREUM: ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"],
|
|
54
|
+
Network.SOLANA: ["https://api.mainnet-beta.solana.com"],
|
|
55
|
+
Network.OP_MAINNET: ["https://mainnet.optimism.io", "https://optimism.llamarpc.com"],
|
|
56
|
+
}
|
|
@@ -20,11 +20,13 @@ def _print_group(group: Group, group_balances: list[Balances.Balance], config: C
|
|
|
20
20
|
balance_sum = Decimal(0)
|
|
21
21
|
usd_sum = Decimal(0)
|
|
22
22
|
for address_task in group_balances:
|
|
23
|
+
if config.skip_empty and isinstance(address_task.balance, Ok) and address_task.balance.ok == Decimal(0):
|
|
24
|
+
continue
|
|
23
25
|
row = [address_task.address, address_task.balance.ok_or_err()] # type: ignore[union-attr]
|
|
24
26
|
if isinstance(address_task.balance, Ok):
|
|
25
27
|
balance_sum += address_task.balance.ok
|
|
26
28
|
if config.price:
|
|
27
|
-
balance_usd = round(address_task.balance.ok * prices[group.
|
|
29
|
+
balance_usd = round(address_task.balance.ok * prices[group.ticker], config.round_ndigits)
|
|
28
30
|
usd_sum += balance_usd
|
|
29
31
|
row.append(f"${balance_usd}")
|
|
30
32
|
rows.append(row)
|
|
@@ -57,6 +59,17 @@ def print_total(config: Config, balances: Balances, prices: Prices) -> None:
|
|
|
57
59
|
total.print()
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
def print_errors(config: Config, balances: Balances) -> None:
|
|
63
|
+
error_balances = balances.get_errors()
|
|
64
|
+
if not error_balances:
|
|
65
|
+
return
|
|
66
|
+
rows = []
|
|
67
|
+
for balance in error_balances:
|
|
68
|
+
group = config.groups[balance.group_index]
|
|
69
|
+
rows.append([group.ticker + " / " + group.network, balance.address, balance.balance.err]) # type: ignore[union-attr]
|
|
70
|
+
print_table("Errors", ["coin", "address", "error"], rows)
|
|
71
|
+
|
|
72
|
+
|
|
60
73
|
def create_progress_bar() -> Progress:
|
|
61
74
|
return Progress(
|
|
62
75
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -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, Network
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Prices(dict[str, Decimal]):
|
|
@@ -20,7 +20,7 @@ class Prices(dict[str, Decimal]):
|
|
|
20
20
|
def get_prices(config: Config) -> Prices:
|
|
21
21
|
result = Prices()
|
|
22
22
|
|
|
23
|
-
coins = pydash.uniq([group.
|
|
23
|
+
coins = pydash.uniq([group.ticker for group in config.groups])
|
|
24
24
|
coingecko_ids = pydash.uniq([get_coingecko_id(group) for group in config.groups])
|
|
25
25
|
|
|
26
26
|
url = f"https://api.coingecko.com/api/v3/simple/price?ids={",".join(coingecko_ids)}&vs_currencies=usd"
|
|
@@ -38,18 +38,29 @@ 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
|
+
|
|
41
50
|
def get_coingecko_id(group: Group) -> str:
|
|
42
51
|
if group.coingecko_id:
|
|
43
52
|
return group.coingecko_id
|
|
44
|
-
elif group.network is Network.
|
|
53
|
+
elif group.network is Network.BITCOIN:
|
|
45
54
|
return "bitcoin"
|
|
46
|
-
elif group.network is Network.
|
|
55
|
+
elif group.network is Network.ETHEREUM and group.token_address is None:
|
|
56
|
+
return "ethereum"
|
|
57
|
+
elif group.ticker == "ETH":
|
|
47
58
|
return "ethereum"
|
|
48
|
-
elif group.
|
|
59
|
+
elif group.ticker == "USDT":
|
|
49
60
|
return "tether"
|
|
50
|
-
elif group.
|
|
61
|
+
elif group.ticker == "USDC":
|
|
51
62
|
return "usd-coin"
|
|
52
|
-
elif group.
|
|
63
|
+
elif group.ticker == "SOL":
|
|
53
64
|
return "solana"
|
|
54
65
|
|
|
55
|
-
raise ValueError(f"can't get coingecko_id for {group.
|
|
66
|
+
raise ValueError(f"can't get coingecko_id for {group.ticker}")
|
|
@@ -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,7 +7,7 @@ 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
|
|
10
|
+
from mm_balance.constants import USD_STABLECOINS
|
|
11
11
|
from mm_balance.price import Prices
|
|
12
12
|
|
|
13
13
|
|
|
@@ -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)
|
|
@@ -925,7 +925,7 @@ wheels = [
|
|
|
925
925
|
|
|
926
926
|
[[package]]
|
|
927
927
|
name = "mm-balance"
|
|
928
|
-
version = "0.1.
|
|
928
|
+
version = "0.1.13"
|
|
929
929
|
source = { editable = "." }
|
|
930
930
|
dependencies = [
|
|
931
931
|
{ name = "mm-btc" },
|
|
@@ -952,7 +952,7 @@ dev = [
|
|
|
952
952
|
requires-dist = [
|
|
953
953
|
{ name = "mm-btc", specifier = "==0.1.0" },
|
|
954
954
|
{ name = "mm-eth", specifier = "==0.1.3" },
|
|
955
|
-
{ name = "mm-solana", specifier = "==0.1.
|
|
955
|
+
{ name = "mm-solana", specifier = "==0.1.5" },
|
|
956
956
|
{ name = "typer", specifier = ">=0.12.5" },
|
|
957
957
|
]
|
|
958
958
|
|
|
@@ -1004,7 +1004,7 @@ wheels = [
|
|
|
1004
1004
|
|
|
1005
1005
|
[[package]]
|
|
1006
1006
|
name = "mm-solana"
|
|
1007
|
-
version = "0.1.
|
|
1007
|
+
version = "0.1.5"
|
|
1008
1008
|
source = { registry = "https://pypi.org/simple" }
|
|
1009
1009
|
dependencies = [
|
|
1010
1010
|
{ name = "base58" },
|
|
@@ -1015,9 +1015,9 @@ dependencies = [
|
|
|
1015
1015
|
{ name = "solana" },
|
|
1016
1016
|
{ name = "typer" },
|
|
1017
1017
|
]
|
|
1018
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1018
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d9/ad/2cd6a536f8d314e2fc37d85d00b1a3a67e1731fded3b4fa82a9ca18c4eec/mm_solana-0.1.5.tar.gz", hash = "sha256:00be9fe5486484b6f31240b76ea7c7a33d364f818252bd3bb314b9b6926aed8c", size = 51391 }
|
|
1019
1019
|
wheels = [
|
|
1020
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1020
|
+
{ url = "https://files.pythonhosted.org/packages/29/53/75850218681cfe1a5b3bff02703193cfa89d7c41d776718df6a2c183774f/mm_solana-0.1.5-py3-none-any.whl", hash = "sha256:4d441791b8c14134651d9b27a3bee617b565b6390ad47a012efe4ad42eca8b92", size = 18927 },
|
|
1021
1021
|
]
|
|
1022
1022
|
|
|
1023
1023
|
[[package]]
|
|
@@ -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.constants import Network
|
|
12
|
-
from mm_balance.rpc import btc, eth, solana
|
|
13
|
-
from mm_balance.token_decimals import TokenDecimals
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Balances:
|
|
17
|
-
class Balance(BaseModel):
|
|
18
|
-
group_index: int
|
|
19
|
-
address: str
|
|
20
|
-
token_address: str | None
|
|
21
|
-
balance: Result[Decimal] | None = None
|
|
22
|
-
|
|
23
|
-
def __init__(self, config: Config, token_decimals: TokenDecimals):
|
|
24
|
-
self.config = config
|
|
25
|
-
self.token_decimals = token_decimals
|
|
26
|
-
self.tasks: dict[Network, list[Balances.Balance]] = {network: [] for network in Network}
|
|
27
|
-
self.progress_bar = output.create_progress_bar()
|
|
28
|
-
self.progress_bar_task: dict[Network, TaskID] = {}
|
|
29
|
-
|
|
30
|
-
for idx, group in enumerate(config.groups):
|
|
31
|
-
task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses]
|
|
32
|
-
self.tasks[group.network].extend(task_list)
|
|
33
|
-
|
|
34
|
-
for network in Network:
|
|
35
|
-
self.progress_bar_task[network] = output.create_progress_task(
|
|
36
|
-
self.progress_bar, network.value, len(self.tasks[network])
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
def process(self) -> None:
|
|
40
|
-
with self.progress_bar:
|
|
41
|
-
job = ConcurrentTasks(max_workers=10)
|
|
42
|
-
for network in Network:
|
|
43
|
-
job.add_task(network.value, self._process_network, args=(network,))
|
|
44
|
-
job.execute()
|
|
45
|
-
|
|
46
|
-
def _process_network(self, network: Network) -> None:
|
|
47
|
-
job = ConcurrentTasks(max_workers=self.config.workers[network])
|
|
48
|
-
for idx, task in enumerate(self.tasks[network]):
|
|
49
|
-
job.add_task(str(idx), self._get_balance, args=(network, task.address, task.token_address))
|
|
50
|
-
job.execute()
|
|
51
|
-
for idx, _task in enumerate(self.tasks[network]):
|
|
52
|
-
self.tasks[network][idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
|
|
53
|
-
|
|
54
|
-
def _get_balance(self, network: Network, wallet_address: str, token_address: str | None) -> Result[Decimal]:
|
|
55
|
-
nodes = self.config.nodes[network]
|
|
56
|
-
round_ndigits = self.config.round_ndigits
|
|
57
|
-
proxies = self.config.proxies
|
|
58
|
-
token_decimals = self.token_decimals[network][token_address] if token_address else -1
|
|
59
|
-
match network:
|
|
60
|
-
case Network.BTC:
|
|
61
|
-
res = btc.get_balance(wallet_address, proxies, round_ndigits)
|
|
62
|
-
case Network.ETH:
|
|
63
|
-
if token_address is None:
|
|
64
|
-
res = eth.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
|
|
65
|
-
else:
|
|
66
|
-
res = eth.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
67
|
-
case Network.SOL:
|
|
68
|
-
if token_address is None:
|
|
69
|
-
res = solana.get_native_balance(nodes, wallet_address, proxies, round_ndigits)
|
|
70
|
-
else:
|
|
71
|
-
res = solana.get_token_balance(nodes, wallet_address, token_address, token_decimals, proxies, round_ndigits)
|
|
72
|
-
|
|
73
|
-
case _:
|
|
74
|
-
raise ValueError
|
|
75
|
-
|
|
76
|
-
self.progress_bar.update(self.progress_bar_task[network], advance=1)
|
|
77
|
-
return res
|
|
78
|
-
|
|
79
|
-
def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
|
|
80
|
-
# TODO: can we get network by group_index?
|
|
81
|
-
return [b for b in self.tasks[network] if b.group_index == group_index]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# class Balances2(BaseModel):
|
|
85
|
-
# class Balance(BaseModel):
|
|
86
|
-
# group_index: int
|
|
87
|
-
# address: str
|
|
88
|
-
# token_address: str | None
|
|
89
|
-
# balance: Result[Decimal] | None = None
|
|
90
|
-
#
|
|
91
|
-
# config: Config
|
|
92
|
-
# token_decimals: TokenDecimals
|
|
93
|
-
# # separate balance tasks on networks
|
|
94
|
-
# btc: list[Balance]
|
|
95
|
-
# eth: list[Balance]
|
|
96
|
-
# sol: list[Balance]
|
|
97
|
-
#
|
|
98
|
-
# def network_tasks(self, network: Network) -> list[Balance]:
|
|
99
|
-
# if network == Network.BTC:
|
|
100
|
-
# return self.btc
|
|
101
|
-
# elif network == Network.ETH:
|
|
102
|
-
# return self.eth
|
|
103
|
-
# elif network == Network.SOL:
|
|
104
|
-
# return self.sol
|
|
105
|
-
# else:
|
|
106
|
-
# raise ValueError
|
|
107
|
-
#
|
|
108
|
-
# def get_group_balances(self, group_index: int, network: Network) -> list[Balance]:
|
|
109
|
-
# # TODO: can we get network by group_index?
|
|
110
|
-
# if network == Network.BTC:
|
|
111
|
-
# network_balances = self.btc
|
|
112
|
-
# elif network == Network.ETH:
|
|
113
|
-
# network_balances = self.eth
|
|
114
|
-
# elif network == Network.SOL:
|
|
115
|
-
# network_balances = self.sol
|
|
116
|
-
# else:
|
|
117
|
-
# raise ValueError
|
|
118
|
-
#
|
|
119
|
-
# return [b for b in network_balances if b.group_index == group_index]
|
|
120
|
-
#
|
|
121
|
-
# def process(self) -> None:
|
|
122
|
-
# progress = output.create_progress_bar()
|
|
123
|
-
# task_btc = output.create_progress_task(progress, "btc", len(self.btc))
|
|
124
|
-
# task_eth = output.create_progress_task(progress, "eth", len(self.eth))
|
|
125
|
-
# task_sol = output.create_progress_task(progress, "sol", len(self.sol))
|
|
126
|
-
# with progress:
|
|
127
|
-
# job = ConcurrentTasks()
|
|
128
|
-
# job.add_task("btc", self._process_btc, args=(progress, task_btc))
|
|
129
|
-
# job.add_task("eth", self._process_eth, args=(progress, task_eth))
|
|
130
|
-
# job.add_task("sol", self._process_sol, args=(progress, task_sol))
|
|
131
|
-
# job.execute()
|
|
132
|
-
#
|
|
133
|
-
# def _process_btc(self, progress: Progress, task_id: TaskID) -> None:
|
|
134
|
-
# job = ConcurrentTasks(max_workers=self.config.workers.btc)
|
|
135
|
-
# for idx, task in enumerate(self.btc):
|
|
136
|
-
# job.add_task(str(idx), btc.get_balance, args=(task.address, self.config, progress, task_id))
|
|
137
|
-
# job.execute()
|
|
138
|
-
# for idx, _task in enumerate(self.btc):
|
|
139
|
-
# self.btc[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
|
|
140
|
-
#
|
|
141
|
-
# def _process_eth(self, progress: Progress, task_id: TaskID) -> None:
|
|
142
|
-
# job = ConcurrentTasks(max_workers=self.config.workers.eth)
|
|
143
|
-
# for idx, task in enumerate(self.eth):
|
|
144
|
-
# job.add_task(str(idx), self._get_balance,
|
|
145
|
-
# args=(Network.ETH, task.address, task.token_address, self.config, progress, task_id))
|
|
146
|
-
# job.execute()
|
|
147
|
-
# for idx, _task in enumerate(self.eth):
|
|
148
|
-
# self.eth[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
|
|
149
|
-
#
|
|
150
|
-
# def _process_sol(self, progress: Progress, task_id: TaskID) -> None:
|
|
151
|
-
# job = ConcurrentTasks(max_workers=self.config.workers.sol)
|
|
152
|
-
# for idx, task in enumerate(self.sol):
|
|
153
|
-
# job.add_task(str(idx), solana.get_balance, args=(task.address, self.config, progress, task_id))
|
|
154
|
-
# job.execute()
|
|
155
|
-
# for idx, _task in enumerate(self.sol):
|
|
156
|
-
# self.sol[idx].balance = job.result.get(str(idx)) # type: ignore[assignment]
|
|
157
|
-
#
|
|
158
|
-
# @staticmethod
|
|
159
|
-
# def from_config(config: Config, token_decimals: TokenDecimals) -> Balances:
|
|
160
|
-
# tasks = Balances(config=config, btc=[], eth=[], sol=[], token_decimals=token_decimals)
|
|
161
|
-
# for idx, group in enumerate(config.groups):
|
|
162
|
-
# task_list = [Balances.Balance(group_index=idx, address=a, token_address=group.token_address) for a in group.addresses] # noqa
|
|
163
|
-
# if group.network == Network.BTC:
|
|
164
|
-
# tasks.btc.extend(task_list)
|
|
165
|
-
# elif group.network == Network.ETH:
|
|
166
|
-
# tasks.eth.extend(task_list)
|
|
167
|
-
# elif group.network == Network.SOL:
|
|
168
|
-
# tasks.sol.extend(task_list)
|
|
169
|
-
# return tasks
|
|
@@ -1,45 +0,0 @@
|
|
|
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 EthTokenAddress(str, Enum):
|
|
14
|
-
USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7"
|
|
15
|
-
USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@unique
|
|
19
|
-
class SolTokenAddress(str, Enum):
|
|
20
|
-
USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
21
|
-
USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
DEFAULT_ETH_NODES = ["https://ethereum.publicnode.com", "https://rpc.ankr.com/eth"]
|
|
25
|
-
DEFAULT_SOL_NODES = ["https://api.mainnet-beta.solana.com"]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@unique
|
|
29
|
-
class Coin(str, Enum):
|
|
30
|
-
BTC = "BTC"
|
|
31
|
-
ETH = "ETH"
|
|
32
|
-
SOL = "SOL"
|
|
33
|
-
USDT = "USDT"
|
|
34
|
-
USDC = "USDC"
|
|
35
|
-
|
|
36
|
-
@classmethod
|
|
37
|
-
def usd_coins(cls) -> list[Coin]:
|
|
38
|
-
return [Coin.USDT, Coin.USDC]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@unique
|
|
42
|
-
class Network(str, Enum):
|
|
43
|
-
BTC = "btc"
|
|
44
|
-
ETH = "eth"
|
|
45
|
-
SOL = "sol"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|