mm-sol 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mm_sol/account.py +9 -7
- mm_sol/balance.py +83 -6
- mm_sol/cli/calcs.py +101 -0
- mm_sol/cli/cli.py +37 -7
- mm_sol/cli/cli_utils.py +28 -16
- mm_sol/cli/cmd/balance_cmd.py +5 -3
- mm_sol/cli/cmd/balances_cmd.py +36 -26
- mm_sol/cli/cmd/node_cmd.py +2 -2
- mm_sol/cli/cmd/transfer_sol_cmd.py +185 -30
- mm_sol/cli/cmd/transfer_token_cmd.py +135 -0
- mm_sol/cli/cmd/wallet/keypair_cmd.py +2 -2
- mm_sol/cli/cmd/wallet/new_cmd.py +2 -2
- mm_sol/cli/examples/transfer-sol.yml +9 -6
- mm_sol/cli/validators.py +17 -0
- mm_sol/converters.py +33 -0
- mm_sol/solana_cli.py +0 -2
- mm_sol/token.py +12 -112
- mm_sol/transfer.py +140 -41
- mm_sol/utils.py +7 -26
- {mm_sol-0.2.5.dist-info → mm_sol-0.2.7.dist-info}/METADATA +4 -4
- mm_sol-0.2.7.dist-info/RECORD +32 -0
- mm_sol/types.py +0 -4
- mm_sol-0.2.5.dist-info/RECORD +0 -29
- {mm_sol-0.2.5.dist-info → mm_sol-0.2.7.dist-info}/WHEEL +0 -0
- {mm_sol-0.2.5.dist-info → mm_sol-0.2.7.dist-info}/entry_points.txt +0 -0
|
@@ -1,41 +1,196 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Self
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
from
|
|
7
|
+
import mm_crypto_utils
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from mm_crypto_utils import AddressToPrivate, TxRoute
|
|
10
|
+
from mm_std import BaseConfig, Err, utc_now
|
|
11
|
+
from pydantic import BeforeValidator, Field, model_validator
|
|
12
|
+
from solders.signature import Signature
|
|
6
13
|
|
|
7
|
-
from mm_sol
|
|
8
|
-
from mm_sol.
|
|
14
|
+
from mm_sol import transfer
|
|
15
|
+
from mm_sol.account import get_public_key, is_address
|
|
16
|
+
from mm_sol.cli import calcs, cli_utils, validators
|
|
17
|
+
from mm_sol.cli.validators import Validators
|
|
18
|
+
from mm_sol.converters import lamports_to_sol
|
|
19
|
+
from mm_sol.utils import get_client
|
|
9
20
|
|
|
10
21
|
|
|
11
22
|
class Config(BaseConfig):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
24
|
+
routes: Annotated[list[TxRoute], BeforeValidator(Validators.routes(is_address))]
|
|
25
|
+
routes_from_file: Path | None = None
|
|
26
|
+
routes_to_file: Path | None = None
|
|
27
|
+
private_keys: Annotated[
|
|
28
|
+
AddressToPrivate, Field(default_factory=AddressToPrivate), BeforeValidator(Validators.private_keys(get_public_key))
|
|
29
|
+
]
|
|
30
|
+
private_keys_file: Path | None = None
|
|
31
|
+
proxies_url: str | None = None
|
|
32
|
+
proxies: list[str] = Field(default_factory=list)
|
|
33
|
+
value: str
|
|
34
|
+
value_min_limit: str | None = None
|
|
35
|
+
delay: str | None = None # in seconds
|
|
36
|
+
round_ndigits: int = 5
|
|
37
|
+
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
|
+
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
21
39
|
|
|
22
40
|
@property
|
|
23
|
-
def
|
|
24
|
-
return
|
|
41
|
+
def from_addresses(self) -> list[str]:
|
|
42
|
+
return [r.from_address for r in self.routes]
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def final_validator(self) -> Self:
|
|
46
|
+
# routes_files
|
|
47
|
+
if self.routes_from_file and self.routes_to_file:
|
|
48
|
+
self.routes += TxRoute.from_files(self.routes_from_file, self.routes_to_file, is_address)
|
|
49
|
+
if not self.routes:
|
|
50
|
+
raise ValueError("routes is empty")
|
|
51
|
+
|
|
52
|
+
# load private keys from file
|
|
53
|
+
if self.private_keys_file:
|
|
54
|
+
self.private_keys.update(AddressToPrivate.from_file(self.private_keys_file, get_public_key))
|
|
55
|
+
|
|
56
|
+
# check all private keys exist
|
|
57
|
+
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
58
|
+
raise ValueError("private keys are not set for all addresses")
|
|
59
|
+
|
|
60
|
+
# fetch proxies from proxies_url
|
|
61
|
+
proxies_url = self.proxies_url or os.getenv("MM_PROXIES_URL", "")
|
|
62
|
+
if proxies_url:
|
|
63
|
+
self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
|
|
64
|
+
|
|
65
|
+
# value
|
|
66
|
+
if not validators.is_valid_var_lamports(self.value, "balance"):
|
|
67
|
+
raise ValueError(f"wrong value: {self.value}")
|
|
68
|
+
|
|
69
|
+
# value_min_limit
|
|
70
|
+
if not validators.is_valid_var_lamports(self.value_min_limit):
|
|
71
|
+
raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
|
|
25
72
|
|
|
73
|
+
# delay
|
|
74
|
+
if not validators.is_valid_var_lamports(self.delay):
|
|
75
|
+
raise ValueError(f"wrong delay: {self.delay}")
|
|
26
76
|
|
|
27
|
-
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run(
|
|
81
|
+
config_path: str,
|
|
82
|
+
*,
|
|
83
|
+
print_balances: bool,
|
|
84
|
+
print_config: bool,
|
|
85
|
+
debug: bool,
|
|
86
|
+
no_confirmation: bool,
|
|
87
|
+
emulate: bool,
|
|
88
|
+
) -> None:
|
|
28
89
|
config = Config.read_config_or_exit(config_path)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
90
|
+
|
|
91
|
+
if print_config:
|
|
92
|
+
config.print_and_exit({"private_keys", "proxies"})
|
|
93
|
+
|
|
94
|
+
mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
95
|
+
|
|
96
|
+
if print_balances:
|
|
97
|
+
cli_utils.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits, proxies=config.proxies)
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
_run_transfers(config, no_confirmation=no_confirmation, emulate=emulate)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _run_transfers(config: Config, *, no_confirmation: bool, emulate: bool) -> None:
|
|
104
|
+
logger.info(f"started at {utc_now()} UTC")
|
|
105
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
106
|
+
for i, route in enumerate(config.routes):
|
|
107
|
+
_transfer(
|
|
108
|
+
from_address=route.from_address,
|
|
109
|
+
to_address=route.to_address,
|
|
110
|
+
config=config,
|
|
111
|
+
no_confirmation=no_confirmation,
|
|
112
|
+
emulate=emulate,
|
|
39
113
|
)
|
|
40
|
-
|
|
41
|
-
|
|
114
|
+
if not emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
115
|
+
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
116
|
+
logger.debug(f"delay {delay_value} seconds")
|
|
117
|
+
time.sleep(float(delay_value))
|
|
118
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _transfer(*, from_address: str, to_address: str, config: Config, no_confirmation: bool, emulate: bool) -> None:
|
|
122
|
+
log_prefix = f"{from_address}->{to_address}"
|
|
123
|
+
fee = 5000
|
|
124
|
+
# get value
|
|
125
|
+
value_res = calcs.calc_sol_value(
|
|
126
|
+
nodes=config.nodes, value_str=config.value, address=from_address, proxies=config.proxies, fee=fee
|
|
127
|
+
)
|
|
128
|
+
logger.debug(f"{log_prefix}value={value_res.ok_or_err()}")
|
|
129
|
+
if isinstance(value_res, Err):
|
|
130
|
+
logger.info(f"{log_prefix}calc value error, {value_res.err}")
|
|
131
|
+
return
|
|
132
|
+
value = value_res.ok
|
|
133
|
+
|
|
134
|
+
# value_min_limit
|
|
135
|
+
if calcs.is_sol_value_less_min_limit(config.value_min_limit, value, log_prefix=log_prefix):
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
tx_params = {
|
|
139
|
+
"fee": fee,
|
|
140
|
+
"value": value,
|
|
141
|
+
"to": to_address,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# emulate?
|
|
145
|
+
if emulate:
|
|
146
|
+
msg = f"{log_prefix}: emulate, value={lamports_to_sol(value, config.round_ndigits)}SOL,"
|
|
147
|
+
msg += f" fee={fee}"
|
|
148
|
+
logger.info(msg)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
logger.debug(f"{log_prefix}: tx_params={tx_params}")
|
|
152
|
+
|
|
153
|
+
res = transfer.transfer_sol_with_retries(
|
|
154
|
+
nodes=config.nodes,
|
|
155
|
+
from_address=from_address,
|
|
156
|
+
private_key=config.private_keys[from_address],
|
|
157
|
+
to_address=to_address,
|
|
158
|
+
lamports=value,
|
|
159
|
+
proxies=config.proxies,
|
|
160
|
+
retries=3,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if isinstance(res, Err):
|
|
164
|
+
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
165
|
+
return
|
|
166
|
+
signature = res.ok
|
|
167
|
+
|
|
168
|
+
if no_confirmation:
|
|
169
|
+
msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}"
|
|
170
|
+
logger.info(msg)
|
|
171
|
+
else:
|
|
172
|
+
logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
|
|
173
|
+
status = "UNKNOWN"
|
|
174
|
+
if _wait_confirmation(config, signature, log_prefix):
|
|
175
|
+
status = "OK"
|
|
176
|
+
msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
|
|
177
|
+
logger.info(msg)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _wait_confirmation(config: Config, signature: Signature, log_prefix: str) -> bool:
|
|
181
|
+
count = 0
|
|
182
|
+
while True:
|
|
183
|
+
try:
|
|
184
|
+
node = mm_crypto_utils.random_node(config.nodes)
|
|
185
|
+
proxy = mm_crypto_utils.random_proxy(config.proxies)
|
|
186
|
+
client = get_client(node, proxy=proxy)
|
|
187
|
+
res = client.get_transaction(signature)
|
|
188
|
+
if res.value and res.value.slot: # check for tx error
|
|
189
|
+
return True
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"{log_prefix}: can't get confirmation, error={e}")
|
|
192
|
+
time.sleep(1)
|
|
193
|
+
count += 1
|
|
194
|
+
if count > 30:
|
|
195
|
+
logger.error(f"{log_prefix}: can't get confirmation, timeout")
|
|
196
|
+
return False
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Self
|
|
6
|
+
|
|
7
|
+
import mm_crypto_utils
|
|
8
|
+
import typer
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from mm_crypto_utils import AddressToPrivate, TxRoute
|
|
11
|
+
from mm_std import BaseConfig, Err, fatal, utc_now
|
|
12
|
+
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
13
|
+
|
|
14
|
+
from mm_sol.account import get_public_key, is_address
|
|
15
|
+
from mm_sol.cli import calcs, cli_utils
|
|
16
|
+
from mm_sol.cli.validators import Validators
|
|
17
|
+
from mm_sol.token import get_decimals_with_retries
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Config(BaseConfig):
|
|
21
|
+
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
22
|
+
routes: Annotated[list[TxRoute], BeforeValidator(Validators.routes(is_address))]
|
|
23
|
+
routes_from_file: Path | None = None
|
|
24
|
+
routes_to_file: Path | None = None
|
|
25
|
+
private_keys: Annotated[
|
|
26
|
+
AddressToPrivate, Field(default_factory=AddressToPrivate), BeforeValidator(Validators.private_keys(get_public_key))
|
|
27
|
+
]
|
|
28
|
+
private_keys_file: Path | None = None
|
|
29
|
+
proxies_url: str | None = None
|
|
30
|
+
proxies: list[str] = Field(default_factory=list)
|
|
31
|
+
token: Annotated[str, AfterValidator(Validators.address(is_address))]
|
|
32
|
+
value: str
|
|
33
|
+
value_min_limit: str | None = None
|
|
34
|
+
delay: str | None = None # in seconds
|
|
35
|
+
round_ndigits: int = 5
|
|
36
|
+
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
37
|
+
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def from_addresses(self) -> list[str]:
|
|
41
|
+
return [r.from_address for r in self.routes]
|
|
42
|
+
|
|
43
|
+
@model_validator(mode="after")
|
|
44
|
+
def final_validator(self) -> Self:
|
|
45
|
+
# routes_files
|
|
46
|
+
if self.routes_from_file and self.routes_to_file:
|
|
47
|
+
self.routes += TxRoute.from_files(self.routes_from_file, self.routes_to_file, is_address)
|
|
48
|
+
if not self.routes:
|
|
49
|
+
raise ValueError("routes is empty")
|
|
50
|
+
|
|
51
|
+
# load private keys from file
|
|
52
|
+
if self.private_keys_file:
|
|
53
|
+
self.private_keys.update(AddressToPrivate.from_file(self.private_keys_file, get_public_key))
|
|
54
|
+
|
|
55
|
+
# check all private keys exist
|
|
56
|
+
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
57
|
+
raise ValueError("private keys are not set for all addresses")
|
|
58
|
+
|
|
59
|
+
# fetch proxies from proxies_url
|
|
60
|
+
proxies_url = self.proxies_url or os.getenv("MM_PROXIES_URL", "")
|
|
61
|
+
if proxies_url:
|
|
62
|
+
self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
|
|
63
|
+
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run(
|
|
68
|
+
config_path: str,
|
|
69
|
+
*,
|
|
70
|
+
print_balances: bool,
|
|
71
|
+
print_config: bool,
|
|
72
|
+
debug: bool,
|
|
73
|
+
no_confirmation: bool,
|
|
74
|
+
emulate: bool,
|
|
75
|
+
) -> None:
|
|
76
|
+
config = Config.read_config_or_exit(config_path)
|
|
77
|
+
|
|
78
|
+
if print_config:
|
|
79
|
+
config.print_and_exit({"private_keys", "proxies"})
|
|
80
|
+
|
|
81
|
+
mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
82
|
+
|
|
83
|
+
decimals_res = get_decimals_with_retries(config.nodes, config.token, retries=3, proxies=config.proxies)
|
|
84
|
+
if isinstance(decimals_res, Err):
|
|
85
|
+
fatal(f"can't get decimals for token={config.token}, error={decimals_res.err}")
|
|
86
|
+
|
|
87
|
+
token_decimals = decimals_res.ok
|
|
88
|
+
logger.debug(f"token decimals={token_decimals}")
|
|
89
|
+
|
|
90
|
+
if print_balances:
|
|
91
|
+
# cli_utils.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits, proxies=config.proxies) # noqa: E501
|
|
92
|
+
typer.echo("Not implemented yet")
|
|
93
|
+
sys.exit(0)
|
|
94
|
+
|
|
95
|
+
_run_transfers(config, token_decimals, no_confirmation=no_confirmation, emulate=emulate)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run_transfers(config: Config, token_decimals: int, *, no_confirmation: bool, emulate: bool) -> None:
|
|
99
|
+
logger.info(f"started at {utc_now()} UTC")
|
|
100
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
101
|
+
for i, route in enumerate(config.routes):
|
|
102
|
+
_transfer(
|
|
103
|
+
route=route,
|
|
104
|
+
token_decimals=token_decimals,
|
|
105
|
+
config=config,
|
|
106
|
+
no_confirmation=no_confirmation,
|
|
107
|
+
emulate=emulate,
|
|
108
|
+
)
|
|
109
|
+
if not emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
110
|
+
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
111
|
+
logger.debug(f"delay {delay_value} seconds")
|
|
112
|
+
time.sleep(float(delay_value))
|
|
113
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirmation: bool, emulate: bool) -> None:
|
|
117
|
+
log_prefix = f"{route.from_address}->{route.to_address}"
|
|
118
|
+
fee = 5000
|
|
119
|
+
|
|
120
|
+
# get value
|
|
121
|
+
value_res = calcs.calc_token_value(
|
|
122
|
+
nodes=config.nodes,
|
|
123
|
+
value_str=config.value,
|
|
124
|
+
wallet_address=route.from_address,
|
|
125
|
+
proxies=config.proxies,
|
|
126
|
+
token_mint_address=config.token,
|
|
127
|
+
token_decimals=token_decimals,
|
|
128
|
+
)
|
|
129
|
+
logger.debug(f"{log_prefix}: value={value_res.ok_or_err()}")
|
|
130
|
+
if isinstance(value_res, Err):
|
|
131
|
+
logger.info(f"{log_prefix}: calc value error, {value_res.err}")
|
|
132
|
+
return
|
|
133
|
+
value = value_res.ok
|
|
134
|
+
|
|
135
|
+
logger.debug(f"{log_prefix}: value={value}, fee={fee}, no_confirmation={no_confirmation}, emulate={emulate}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from mm_std import
|
|
3
|
+
from mm_std import print_json
|
|
4
4
|
|
|
5
5
|
from mm_sol.account import (
|
|
6
6
|
get_private_key_arr_str,
|
|
@@ -16,4 +16,4 @@ def run(private_key: str) -> None:
|
|
|
16
16
|
public = get_public_key(private_key)
|
|
17
17
|
private_base58 = get_private_key_base58(private_key)
|
|
18
18
|
private_arr = get_private_key_arr_str(private_key)
|
|
19
|
-
|
|
19
|
+
print_json({"public": public, "private_base58": private_base58, "private_arr": private_arr})
|
mm_sol/cli/cmd/wallet/new_cmd.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from mm_std import
|
|
1
|
+
from mm_std import print_json
|
|
2
2
|
|
|
3
3
|
from mm_sol.account import generate_account, get_private_key_arr_str
|
|
4
4
|
|
|
@@ -11,4 +11,4 @@ def run(limit: int, array: bool) -> None:
|
|
|
11
11
|
if array:
|
|
12
12
|
private_key = get_private_key_arr_str(acc.private_key_base58)
|
|
13
13
|
result[acc.public_key] = private_key
|
|
14
|
-
|
|
14
|
+
print_json(result)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
recipients:
|
|
5
|
-
- Rjg3K9PPDm1B5bmUP7eXZNfmApgvDPF9SrTh12dLRH9
|
|
1
|
+
tx_routes: |
|
|
2
|
+
Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
|
|
3
|
+
Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
|
|
6
4
|
|
|
5
|
+
|
|
6
|
+
private_keys_file: ./path/to/privates.txt
|
|
7
|
+
value: 0.012 sol
|
|
8
|
+
|
|
9
|
+
proxies_url: https://site.com/api/get-proxies
|
|
7
10
|
nodes: |
|
|
8
|
-
https://api.
|
|
11
|
+
https://api.devnet.solana.com
|
mm_sol/cli/validators.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from mm_crypto_utils import ConfigValidators
|
|
2
|
+
|
|
3
|
+
from mm_sol.cli import calcs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_valid_var_lamports(value: str | None, base_name: str = "var", decimals: int | None = None) -> bool:
|
|
7
|
+
if value is None:
|
|
8
|
+
return True # check for None on BaseModel.field type level
|
|
9
|
+
try:
|
|
10
|
+
calcs.calc_var_value(value, var_value=123, var_name=base_name, decimals=decimals)
|
|
11
|
+
return True # noqa: TRY300
|
|
12
|
+
except ValueError:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Validators(ConfigValidators):
|
|
17
|
+
pass
|
mm_sol/converters.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
|
|
5
|
+
return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sol_to_lamports(sol: Decimal) -> int:
|
|
9
|
+
return int(sol * 10**9)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def to_lamports(value: str | int | Decimal, decimals: int | None = None) -> int:
|
|
13
|
+
if isinstance(value, int):
|
|
14
|
+
return value
|
|
15
|
+
if isinstance(value, Decimal):
|
|
16
|
+
if value != value.to_integral_value():
|
|
17
|
+
raise ValueError(f"value must be integral number: {value}")
|
|
18
|
+
return int(value)
|
|
19
|
+
if isinstance(value, str):
|
|
20
|
+
value = value.lower().replace(" ", "").strip()
|
|
21
|
+
if value.endswith("sol"):
|
|
22
|
+
value = value.replace("sol", "")
|
|
23
|
+
return sol_to_lamports(Decimal(value))
|
|
24
|
+
if value.endswith("t"):
|
|
25
|
+
if decimals is None:
|
|
26
|
+
raise ValueError("t without decimals")
|
|
27
|
+
value = value.removesuffix("t")
|
|
28
|
+
return int(Decimal(value) * 10**decimals)
|
|
29
|
+
if value.isdigit():
|
|
30
|
+
return int(value)
|
|
31
|
+
raise ValueError("wrong value " + value)
|
|
32
|
+
|
|
33
|
+
raise ValueError(f"value has a wrong type: {type(value)}")
|
mm_sol/solana_cli.py
CHANGED
|
@@ -26,7 +26,6 @@ class StakeAccount(BaseModel):
|
|
|
26
26
|
vote: str | None = Field(None, alias="delegatedVoteAccountAddress")
|
|
27
27
|
|
|
28
28
|
@field_validator("balance")
|
|
29
|
-
@classmethod
|
|
30
29
|
def from_lamports_to_sol(cls, v: int | None) -> float | None:
|
|
31
30
|
if v:
|
|
32
31
|
return v / 1_000_000_000
|
|
@@ -44,7 +43,6 @@ class Stake(BaseModel):
|
|
|
44
43
|
lock_time: int | None = Field(None, alias="unixTimestamp")
|
|
45
44
|
|
|
46
45
|
@field_validator("balance", "delegated", "active")
|
|
47
|
-
@classmethod
|
|
48
46
|
def from_lamports_to_sol(cls, v: int | None) -> float | None:
|
|
49
47
|
if v:
|
|
50
48
|
return v / 1_000_000_000
|
mm_sol/token.py
CHANGED
|
@@ -1,86 +1,20 @@
|
|
|
1
|
+
import mm_crypto_utils
|
|
2
|
+
from mm_crypto_utils import Nodes, Proxies
|
|
1
3
|
from mm_std import Err, Ok, Result
|
|
2
|
-
from solana.exceptions import SolanaRpcException
|
|
3
|
-
from solana.rpc.types import TokenAccountOpts
|
|
4
4
|
from solders.pubkey import Pubkey
|
|
5
5
|
|
|
6
|
-
from mm_sol.
|
|
7
|
-
from mm_sol.utils import get_client, get_node, get_proxy
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def get_balance(
|
|
11
|
-
node: str,
|
|
12
|
-
owner_address: str,
|
|
13
|
-
token_mint_address: str,
|
|
14
|
-
token_account: str | None = None,
|
|
15
|
-
timeout: float = 10,
|
|
16
|
-
proxy: str | None = None,
|
|
17
|
-
no_token_accounts_return_zero: bool = True,
|
|
18
|
-
) -> Result[int]:
|
|
19
|
-
try:
|
|
20
|
-
client = get_client(node, proxy=proxy, timeout=timeout)
|
|
21
|
-
if token_account:
|
|
22
|
-
res_balance = client.get_token_account_balance(Pubkey.from_string(token_account))
|
|
23
|
-
return Ok(int(res_balance.value.amount))
|
|
24
|
-
|
|
25
|
-
res_accounts = client.get_token_accounts_by_owner(
|
|
26
|
-
Pubkey.from_string(owner_address),
|
|
27
|
-
TokenAccountOpts(mint=Pubkey.from_string(token_mint_address)),
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if no_token_accounts_return_zero and not res_accounts.value:
|
|
31
|
-
return Ok(0)
|
|
32
|
-
if not res_accounts.value:
|
|
33
|
-
return Err("no_token_accounts")
|
|
34
|
-
|
|
35
|
-
token_accounts = [a.pubkey for a in res_accounts.value]
|
|
36
|
-
balances = []
|
|
37
|
-
for token_account_ in token_accounts:
|
|
38
|
-
res = client.get_token_account_balance(token_account_)
|
|
39
|
-
if res.value: # type:ignore[truthy-bool]
|
|
40
|
-
balances.append(int(res.value.amount))
|
|
41
|
-
|
|
42
|
-
if len(balances) > 1:
|
|
43
|
-
return Err("there are many non empty token accounts, set token_account explicitly")
|
|
44
|
-
return Ok(balances[0])
|
|
45
|
-
except SolanaRpcException as e:
|
|
46
|
-
return Err(e.error_msg)
|
|
47
|
-
except Exception as e:
|
|
48
|
-
return Err(e)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def get_balance_with_retries(
|
|
52
|
-
nodes: Nodes,
|
|
53
|
-
owner_address: str,
|
|
54
|
-
token_mint_address: str,
|
|
55
|
-
retries: int,
|
|
56
|
-
token_account: str | None = None,
|
|
57
|
-
timeout: float = 10,
|
|
58
|
-
proxies: Proxies = None,
|
|
59
|
-
no_token_accounts_return_zero: bool = True,
|
|
60
|
-
) -> Result[int]:
|
|
61
|
-
res: Result[int] = Err("not started yet")
|
|
62
|
-
for _ in range(retries):
|
|
63
|
-
res = get_balance(
|
|
64
|
-
get_node(nodes),
|
|
65
|
-
owner_address,
|
|
66
|
-
token_mint_address,
|
|
67
|
-
token_account,
|
|
68
|
-
timeout=timeout,
|
|
69
|
-
proxy=get_proxy(proxies),
|
|
70
|
-
no_token_accounts_return_zero=no_token_accounts_return_zero,
|
|
71
|
-
)
|
|
72
|
-
if res.is_ok():
|
|
73
|
-
return res
|
|
74
|
-
return res
|
|
6
|
+
from mm_sol.utils import get_client
|
|
75
7
|
|
|
76
8
|
|
|
77
9
|
def get_decimals(node: str, token_mint_address: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
|
|
10
|
+
data = None
|
|
78
11
|
try:
|
|
79
12
|
client = get_client(node, proxy=proxy, timeout=timeout)
|
|
80
13
|
res = client.get_token_supply(Pubkey.from_string(token_mint_address))
|
|
14
|
+
data = res
|
|
81
15
|
return Ok(res.value.decimals)
|
|
82
16
|
except Exception as e:
|
|
83
|
-
return Err(e)
|
|
17
|
+
return Err(e, data=data)
|
|
84
18
|
|
|
85
19
|
|
|
86
20
|
def get_decimals_with_retries(
|
|
@@ -88,46 +22,12 @@ def get_decimals_with_retries(
|
|
|
88
22
|
) -> Result[int]:
|
|
89
23
|
res: Result[int] = Err("not started yet")
|
|
90
24
|
for _ in range(retries):
|
|
91
|
-
res = get_decimals(
|
|
25
|
+
res = get_decimals(
|
|
26
|
+
node=mm_crypto_utils.random_node(nodes),
|
|
27
|
+
token_mint_address=token_mint_address,
|
|
28
|
+
timeout=timeout,
|
|
29
|
+
proxy=mm_crypto_utils.random_proxy(proxies),
|
|
30
|
+
)
|
|
92
31
|
if res.is_ok():
|
|
93
32
|
return res
|
|
94
33
|
return res
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# def transfer_to_wallet_address(
|
|
98
|
-
# *,
|
|
99
|
-
# node: str,
|
|
100
|
-
# private_key: str,
|
|
101
|
-
# recipient_wallet_address: str,
|
|
102
|
-
# token_mint_address: str,
|
|
103
|
-
# amount: int,
|
|
104
|
-
# ) -> Result[str]:
|
|
105
|
-
# try:
|
|
106
|
-
# keypair = account.get_keypair(private_key)
|
|
107
|
-
# token_client = Token(Client(node), Pubkey.from_string(token_mint_address), program_id=TOKEN_PROGRAM_ID, payer=keypair)
|
|
108
|
-
#
|
|
109
|
-
# # get from_token_account
|
|
110
|
-
# res = token_client.get_accounts(keypair.public_key)
|
|
111
|
-
# token_accounts = res["result"]["value"]
|
|
112
|
-
# if len(token_accounts) > 1:
|
|
113
|
-
# return Result(error="many_from_token_accounts", data=res)
|
|
114
|
-
# from_token_account = Pubkey.from_string(token_accounts[0]["pubkey"])
|
|
115
|
-
#
|
|
116
|
-
# # get to_token_account
|
|
117
|
-
# res = token_client.get_accounts(Pubkey.from_string(recipient_wallet_address))
|
|
118
|
-
# token_accounts = res["result"]["value"]
|
|
119
|
-
# if len(token_accounts) > 1:
|
|
120
|
-
# return Result(error="many_to_token_accounts", data=res)
|
|
121
|
-
# elif len(token_accounts) == 1:
|
|
122
|
-
# to_token_account = Pubkey.from_string(token_accounts[0]["pubkey"])
|
|
123
|
-
# else: # create a new to_token_account
|
|
124
|
-
# to_token_account = token_client.create_account(owner=Pubkey.from_string(recipient_wallet_address))
|
|
125
|
-
#
|
|
126
|
-
# res = token_client.transfer(source=from_token_account, dest=to_token_account, owner=keypair, amount=amount)
|
|
127
|
-
# if res.get("result"):
|
|
128
|
-
# return Result(ok=res.get("result"), data=res)
|
|
129
|
-
# return Result(error="unknown_response", data=res)
|
|
130
|
-
# except RPCException as e:
|
|
131
|
-
# return Result(error="rcp_exception", data=str(e))
|
|
132
|
-
# except Exception as e:
|
|
133
|
-
# return Result(error="exception", data=str(e))
|