mm-sol 0.2.8__tar.gz → 0.2.10__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_sol-0.2.8 → mm_sol-0.2.10}/PKG-INFO +2 -2
- {mm_sol-0.2.8 → mm_sol-0.2.10}/pyproject.toml +2 -2
- mm_sol-0.2.10/src/mm_sol/cli/calcs.py +49 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cli.py +1 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cli_utils.py +25 -1
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/balances_cmd.py +6 -18
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/transfer_sol_cmd.py +21 -78
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/transfer_token_cmd.py +54 -36
- mm_sol-0.2.10/src/mm_sol/cli/examples/transfer-sol.yml +9 -0
- mm_sol-0.2.8/src/mm_sol/cli/examples/transfer-sol.yml → mm_sol-0.2.10/src/mm_sol/cli/examples/transfer-token.yml +3 -3
- mm_sol-0.2.10/src/mm_sol/cli/validators.py +32 -0
- mm_sol-0.2.10/src/mm_sol/constants.py +1 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/converters.py +4 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/transfer.py +3 -6
- {mm_sol-0.2.8 → mm_sol-0.2.10}/uv.lock +4 -4
- mm_sol-0.2.8/src/mm_sol/cli/calcs.py +0 -101
- mm_sol-0.2.8/src/mm_sol/cli/validators.py +0 -17
- mm_sol-0.2.8/tests/cli/test_calcs.py +0 -28
- {mm_sol-0.2.8 → mm_sol-0.2.10}/.env.example +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/.gitignore +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/README.txt +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/dict.dic +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/justfile +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/account.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/balance.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/block.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/balance_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/example_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/node_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/wallet/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/wallet/keypair_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/wallet/new_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/examples/balances.yml +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/py.typed +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/rpc.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/solana_cli.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/token.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/utils.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/wallet/__init__.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/wallet/test_keypair_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/wallet/test_new_cmd.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/conftest.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_account.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_balance.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_client.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_converters.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_rpc.py +0 -0
- {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_token.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-sol
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
5
|
Requires-Dist: base58~=2.1.1
|
|
6
6
|
Requires-Dist: jinja2>=3.1.5
|
|
7
|
-
Requires-Dist: mm-crypto-utils>=0.0.
|
|
7
|
+
Requires-Dist: mm-crypto-utils>=0.0.19
|
|
8
8
|
Requires-Dist: solana~=0.36.3
|
|
9
9
|
Requires-Dist: typer>=0.15.1
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import mm_crypto_utils
|
|
2
|
+
from mm_crypto_utils import Nodes, Proxies, VarInt
|
|
3
|
+
from mm_std import Err, Ok, Result
|
|
4
|
+
|
|
5
|
+
from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
|
|
6
|
+
from mm_sol.constants import SUFFIX_DECIMALS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calc_sol_expression(expression: str, var: VarInt | None = None) -> int:
|
|
10
|
+
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals=SUFFIX_DECIMALS)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def calc_token_expression(expression: str, token_decimals: int, var: VarInt | None = None) -> int:
|
|
14
|
+
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals={"t": token_decimals})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calc_sol_value_for_address(*, nodes: Nodes, value_expression: str, address: str, proxies: Proxies, fee: int) -> Result[int]:
|
|
18
|
+
value_expression = value_expression.lower()
|
|
19
|
+
var = None
|
|
20
|
+
if "balance" in value_expression:
|
|
21
|
+
res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
|
|
22
|
+
if isinstance(res, Err):
|
|
23
|
+
return res
|
|
24
|
+
var = VarInt("balance", res.ok)
|
|
25
|
+
|
|
26
|
+
value = calc_sol_expression(value_expression, var)
|
|
27
|
+
if "balance" in value_expression:
|
|
28
|
+
value = value - fee
|
|
29
|
+
return Ok(value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def calc_token_value_for_address(
|
|
33
|
+
*, nodes: Nodes, value_expression: str, wallet_address: str, token_mint_address: str, token_decimals: int, proxies: Proxies
|
|
34
|
+
) -> Result[int]:
|
|
35
|
+
var = None
|
|
36
|
+
value_expression = value_expression.lower()
|
|
37
|
+
if "balance" in value_expression:
|
|
38
|
+
res = get_token_balance_with_retries(
|
|
39
|
+
nodes=nodes,
|
|
40
|
+
owner_address=wallet_address,
|
|
41
|
+
token_mint_address=token_mint_address,
|
|
42
|
+
proxies=proxies,
|
|
43
|
+
retries=5,
|
|
44
|
+
)
|
|
45
|
+
if isinstance(res, Err):
|
|
46
|
+
return res
|
|
47
|
+
var = VarInt("balance", res.ok)
|
|
48
|
+
value = calc_token_expression(value_expression, token_decimals, var)
|
|
49
|
+
return Ok(value)
|
|
@@ -29,6 +29,7 @@ def main(_version: bool = typer.Option(None, "--version", callback=version_callb
|
|
|
29
29
|
class ConfigExample(str, Enum):
|
|
30
30
|
balances = "balances"
|
|
31
31
|
transfer_sol = "transfer-sol"
|
|
32
|
+
transfer_token = "transfer-token" # noqa: S105 # nosec
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
@app.command(name="example", help="Print an example of config for a command")
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
|
+
import time
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import mm_crypto_utils
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from mm_crypto_utils import Nodes, Proxies
|
|
4
7
|
from rich.live import Live
|
|
5
8
|
from rich.table import Table
|
|
9
|
+
from solders.signature import Signature
|
|
6
10
|
|
|
7
11
|
from mm_sol.balance import get_sol_balance_with_retries
|
|
8
12
|
from mm_sol.converters import lamports_to_sol
|
|
13
|
+
from mm_sol.utils import get_client
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
def get_version() -> str:
|
|
@@ -46,3 +51,22 @@ def print_balances(
|
|
|
46
51
|
)
|
|
47
52
|
row: list[str] = [str(count), address, balance]
|
|
48
53
|
table.add_row(*row)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def wait_confirmation(nodes: Nodes, proxies: Proxies, signature: Signature, log_prefix: str) -> bool:
|
|
57
|
+
count = 0
|
|
58
|
+
while True:
|
|
59
|
+
try:
|
|
60
|
+
node = mm_crypto_utils.random_node(nodes)
|
|
61
|
+
proxy = mm_crypto_utils.random_proxy(proxies)
|
|
62
|
+
client = get_client(node, proxy=proxy)
|
|
63
|
+
res = client.get_transaction(signature)
|
|
64
|
+
if res.value and res.value.slot: # check for tx error
|
|
65
|
+
return True
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"{log_prefix}: can't get confirmation, error={e}")
|
|
68
|
+
time.sleep(1)
|
|
69
|
+
count += 1
|
|
70
|
+
if count > 30:
|
|
71
|
+
logger.error(f"{log_prefix}: can't get confirmation, timeout")
|
|
72
|
+
return False
|
|
@@ -1,39 +1,27 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import random
|
|
3
2
|
from decimal import Decimal
|
|
4
|
-
from typing import Annotated, Any
|
|
3
|
+
from typing import Annotated, Any
|
|
5
4
|
|
|
6
|
-
import mm_crypto_utils
|
|
7
5
|
from mm_crypto_utils import ConfigValidators
|
|
8
6
|
from mm_std import BaseConfig, print_json
|
|
9
|
-
from pydantic import BeforeValidator
|
|
7
|
+
from pydantic import BeforeValidator
|
|
10
8
|
|
|
11
9
|
import mm_sol.converters
|
|
12
10
|
from mm_sol import balance
|
|
13
|
-
from mm_sol.account import is_address
|
|
14
11
|
from mm_sol.balance import get_token_balance_with_retries
|
|
12
|
+
from mm_sol.cli.validators import Validators
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class Config(BaseConfig):
|
|
18
|
-
accounts: Annotated[list[str], BeforeValidator(
|
|
16
|
+
accounts: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
|
|
17
|
+
tokens: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
|
|
19
18
|
nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
|
|
20
|
-
|
|
21
|
-
proxies_url: str | None = None
|
|
22
|
-
proxies: list[str] = Field(default_factory=list)
|
|
19
|
+
proxies: Annotated[list[str], BeforeValidator(Validators.proxies())]
|
|
23
20
|
|
|
24
21
|
@property
|
|
25
22
|
def random_node(self) -> str:
|
|
26
23
|
return random.choice(self.nodes)
|
|
27
24
|
|
|
28
|
-
@model_validator(mode="after")
|
|
29
|
-
def final_validator(self) -> Self:
|
|
30
|
-
# fetch proxies from proxies_url
|
|
31
|
-
proxies_url = self.proxies_url or os.getenv("MM_SOL_PROXIES_URL", "")
|
|
32
|
-
if proxies_url:
|
|
33
|
-
self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
|
|
34
|
-
|
|
35
|
-
return self
|
|
36
|
-
|
|
37
25
|
|
|
38
26
|
def run(config_path: str, print_config: bool) -> None:
|
|
39
27
|
config = Config.read_config_or_exit(config_path)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import sys
|
|
3
2
|
import time
|
|
4
3
|
from pathlib import Path
|
|
@@ -8,31 +7,24 @@ import mm_crypto_utils
|
|
|
8
7
|
from loguru import logger
|
|
9
8
|
from mm_crypto_utils import AddressToPrivate, TxRoute
|
|
10
9
|
from mm_std import BaseConfig, Err, utc_now
|
|
11
|
-
from pydantic import
|
|
12
|
-
from solders.signature import Signature
|
|
10
|
+
from pydantic import AfterValidator, BeforeValidator, model_validator
|
|
13
11
|
|
|
14
12
|
from mm_sol import transfer
|
|
15
|
-
from mm_sol.
|
|
16
|
-
from mm_sol.cli import
|
|
13
|
+
from mm_sol.cli import calcs, cli_utils
|
|
14
|
+
from mm_sol.cli.calcs import calc_sol_expression
|
|
17
15
|
from mm_sol.cli.validators import Validators
|
|
18
16
|
from mm_sol.converters import lamports_to_sol
|
|
19
|
-
from mm_sol.utils import get_client
|
|
20
17
|
|
|
21
18
|
|
|
19
|
+
# noinspection DuplicatedCode
|
|
22
20
|
class Config(BaseConfig):
|
|
23
21
|
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
24
|
-
routes: Annotated[list[TxRoute], BeforeValidator(Validators.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
22
|
+
routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
|
|
23
|
+
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
|
|
24
|
+
proxies: Annotated[list[str], BeforeValidator(Validators.proxies())]
|
|
25
|
+
value: Annotated[str, AfterValidator(Validators.valid_sol_expression("balance"))]
|
|
26
|
+
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_sol_expression())] = None
|
|
27
|
+
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
36
28
|
round_ndigits: int = 5
|
|
37
29
|
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
30
|
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
@@ -43,37 +35,9 @@ class Config(BaseConfig):
|
|
|
43
35
|
|
|
44
36
|
@model_validator(mode="after")
|
|
45
37
|
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
38
|
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
58
39
|
raise ValueError("private keys are not set for all addresses")
|
|
59
40
|
|
|
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}")
|
|
72
|
-
|
|
73
|
-
# delay
|
|
74
|
-
if not validators.is_valid_var_lamports(self.delay):
|
|
75
|
-
raise ValueError(f"wrong delay: {self.delay}")
|
|
76
|
-
|
|
77
41
|
return self
|
|
78
42
|
|
|
79
43
|
|
|
@@ -89,7 +53,7 @@ def run(
|
|
|
89
53
|
config = Config.read_config_or_exit(config_path)
|
|
90
54
|
|
|
91
55
|
if print_config:
|
|
92
|
-
config.print_and_exit({"private_keys"
|
|
56
|
+
config.print_and_exit({"private_keys"})
|
|
93
57
|
|
|
94
58
|
mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
95
59
|
|
|
@@ -122,8 +86,8 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
|
|
|
122
86
|
log_prefix = f"{from_address}->{to_address}"
|
|
123
87
|
fee = 5000
|
|
124
88
|
# get value
|
|
125
|
-
value_res = calcs.
|
|
126
|
-
nodes=config.nodes,
|
|
89
|
+
value_res = calcs.calc_sol_value_for_address(
|
|
90
|
+
nodes=config.nodes, value_expression=config.value, address=from_address, proxies=config.proxies, fee=fee
|
|
127
91
|
)
|
|
128
92
|
logger.debug(f"{log_prefix}value={value_res.ok_or_err()}")
|
|
129
93
|
if isinstance(value_res, Err):
|
|
@@ -132,14 +96,11 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
|
|
|
132
96
|
value = value_res.ok
|
|
133
97
|
|
|
134
98
|
# value_min_limit
|
|
135
|
-
if
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"value": value,
|
|
141
|
-
"to": to_address,
|
|
142
|
-
}
|
|
99
|
+
if config.value_min_limit:
|
|
100
|
+
value_min_limit = calc_sol_expression(config.value_min_limit)
|
|
101
|
+
if value < value_min_limit:
|
|
102
|
+
logger.info(f"{log_prefix}: value<value_min_limit, value={lamports_to_sol(value, config.round_ndigits)}sol")
|
|
103
|
+
return
|
|
143
104
|
|
|
144
105
|
# emulate?
|
|
145
106
|
if emulate:
|
|
@@ -148,7 +109,8 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
|
|
|
148
109
|
logger.info(msg)
|
|
149
110
|
return
|
|
150
111
|
|
|
151
|
-
|
|
112
|
+
debug_tx_params = {"fee": fee, "value": value, "to": to_address}
|
|
113
|
+
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
152
114
|
|
|
153
115
|
res = transfer.transfer_sol_with_retries(
|
|
154
116
|
nodes=config.nodes,
|
|
@@ -171,26 +133,7 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
|
|
|
171
133
|
else:
|
|
172
134
|
logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
|
|
173
135
|
status = "UNKNOWN"
|
|
174
|
-
if
|
|
136
|
+
if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
|
|
175
137
|
status = "OK"
|
|
176
138
|
msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
|
|
177
139
|
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
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import sys
|
|
3
2
|
import time
|
|
4
3
|
from pathlib import Path
|
|
@@ -9,29 +8,25 @@ import typer
|
|
|
9
8
|
from loguru import logger
|
|
10
9
|
from mm_crypto_utils import AddressToPrivate, TxRoute
|
|
11
10
|
from mm_std import BaseConfig, Err, fatal, utc_now
|
|
12
|
-
from pydantic import AfterValidator, BeforeValidator,
|
|
11
|
+
from pydantic import AfterValidator, BeforeValidator, model_validator
|
|
13
12
|
|
|
14
|
-
from mm_sol
|
|
13
|
+
from mm_sol import transfer
|
|
15
14
|
from mm_sol.cli import calcs, cli_utils
|
|
16
15
|
from mm_sol.cli.validators import Validators
|
|
16
|
+
from mm_sol.converters import to_token
|
|
17
17
|
from mm_sol.token import get_decimals_with_retries
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
# noinspection DuplicatedCode
|
|
20
21
|
class Config(BaseConfig):
|
|
21
22
|
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
22
|
-
routes: Annotated[list[TxRoute], BeforeValidator(Validators.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
]
|
|
28
|
-
|
|
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
|
|
23
|
+
routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
|
|
24
|
+
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
|
|
25
|
+
proxies: Annotated[list[str], BeforeValidator(Validators.proxies())]
|
|
26
|
+
token: Annotated[str, AfterValidator(Validators.sol_address())]
|
|
27
|
+
value: Annotated[str, AfterValidator(Validators.valid_token_expression("balance"))]
|
|
28
|
+
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_token_expression())] = None
|
|
29
|
+
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
35
30
|
round_ndigits: int = 5
|
|
36
31
|
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
37
32
|
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
@@ -42,25 +37,9 @@ class Config(BaseConfig):
|
|
|
42
37
|
|
|
43
38
|
@model_validator(mode="after")
|
|
44
39
|
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
40
|
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
57
41
|
raise ValueError("private keys are not set for all addresses")
|
|
58
42
|
|
|
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
43
|
return self
|
|
65
44
|
|
|
66
45
|
|
|
@@ -76,7 +55,7 @@ def run(
|
|
|
76
55
|
config = Config.read_config_or_exit(config_path)
|
|
77
56
|
|
|
78
57
|
if print_config:
|
|
79
|
-
config.print_and_exit({"private_keys"
|
|
58
|
+
config.print_and_exit({"private_keys"})
|
|
80
59
|
|
|
81
60
|
mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
82
61
|
|
|
@@ -118,9 +97,9 @@ def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirm
|
|
|
118
97
|
fee = 5000
|
|
119
98
|
|
|
120
99
|
# get value
|
|
121
|
-
value_res = calcs.
|
|
100
|
+
value_res = calcs.calc_token_value_for_address(
|
|
122
101
|
nodes=config.nodes,
|
|
123
|
-
|
|
102
|
+
value_expression=config.value,
|
|
124
103
|
wallet_address=route.from_address,
|
|
125
104
|
proxies=config.proxies,
|
|
126
105
|
token_mint_address=config.token,
|
|
@@ -131,5 +110,44 @@ def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirm
|
|
|
131
110
|
logger.info(f"{log_prefix}: calc value error, {value_res.err}")
|
|
132
111
|
return
|
|
133
112
|
value = value_res.ok
|
|
113
|
+
value_t = f"{to_token(value, decimals=token_decimals, ndigits=config.round_ndigits)}t"
|
|
114
|
+
|
|
115
|
+
# value_min_limit
|
|
116
|
+
if config.value_min_limit:
|
|
117
|
+
value_min_limit = calcs.calc_token_expression(config.value_min_limit, token_decimals)
|
|
118
|
+
if value < value_min_limit:
|
|
119
|
+
logger.info(f"{log_prefix}: value<value_min_limit, value={value_t}")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if emulate:
|
|
123
|
+
logger.info(f"{log_prefix}: emulate, value={value_t}, fee={fee}lamports")
|
|
124
|
+
return
|
|
134
125
|
|
|
135
|
-
logger.debug(f"{log_prefix}: value={value
|
|
126
|
+
logger.debug(f"{log_prefix}: value={to_token(value, decimals=token_decimals)}t, fee={fee}lamports")
|
|
127
|
+
res = transfer.transfer_token_with_retries(
|
|
128
|
+
nodes=config.nodes,
|
|
129
|
+
token_mint_address=config.token,
|
|
130
|
+
from_address=route.from_address,
|
|
131
|
+
private_key=config.private_keys[route.from_address],
|
|
132
|
+
to_address=route.to_address,
|
|
133
|
+
amount=value,
|
|
134
|
+
decimals=token_decimals,
|
|
135
|
+
proxies=config.proxies,
|
|
136
|
+
retries=3,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if isinstance(res, Err):
|
|
140
|
+
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
141
|
+
return
|
|
142
|
+
signature = res.ok
|
|
143
|
+
|
|
144
|
+
if no_confirmation:
|
|
145
|
+
msg = f"{log_prefix}: sig={signature}, value={value_t}"
|
|
146
|
+
logger.info(msg)
|
|
147
|
+
else:
|
|
148
|
+
logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
|
|
149
|
+
status = "UNKNOWN"
|
|
150
|
+
if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
|
|
151
|
+
status = "OK"
|
|
152
|
+
msg = f"{log_prefix}: sig={signature}, value={value_t}, status={status}"
|
|
153
|
+
logger.info(msg)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
routes: |
|
|
2
|
+
Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
|
|
3
|
+
Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
|
|
4
|
+
private_keys: "file: ./path/to/privates.txt"
|
|
5
|
+
value: 0.012 sol
|
|
6
|
+
|
|
7
|
+
proxies: "url: https://site.com/api/get-proxies"
|
|
8
|
+
nodes: |
|
|
9
|
+
https://api.devnet.solana.com
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
routes: |
|
|
2
2
|
Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
|
|
3
3
|
Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
|
|
4
|
-
|
|
4
|
+
token: 6VBTMLgv256c7scudNf8T5GoTJcEE8WfgcJhxbGYPQ8G
|
|
5
5
|
|
|
6
6
|
private_keys_file: ./path/to/privates.txt
|
|
7
|
-
value: 0.012
|
|
7
|
+
value: 0.012 t
|
|
8
8
|
|
|
9
9
|
proxies_url: https://site.com/api/get-proxies
|
|
10
10
|
nodes: |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from mm_crypto_utils import AddressToPrivate, ConfigValidators, TxRoute
|
|
4
|
+
|
|
5
|
+
from mm_sol.account import get_public_key, is_address
|
|
6
|
+
from mm_sol.constants import SUFFIX_DECIMALS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Validators(ConfigValidators):
|
|
10
|
+
@staticmethod
|
|
11
|
+
def sol_address() -> Callable[[str], str]:
|
|
12
|
+
return ConfigValidators.address(is_address)
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def sol_addresses(unique: bool) -> Callable[[str | list[str] | None], list[str]]:
|
|
16
|
+
return ConfigValidators.addresses(unique, is_address=is_address)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def sol_routes() -> Callable[[str | None], list[TxRoute]]:
|
|
20
|
+
return ConfigValidators.routes(is_address)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def sol_private_keys() -> Callable[[str | list[str] | None], AddressToPrivate]:
|
|
24
|
+
return ConfigValidators.private_keys(get_public_key)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def valid_sol_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
28
|
+
return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
32
|
+
return ConfigValidators.valid_calc_int_expression(var_name, {"t": 6})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SUFFIX_DECIMALS = {"sol": 9}
|
|
@@ -5,6 +5,10 @@ def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
|
|
|
5
5
|
return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def to_token(smallest_unit_value: int, decimals: int, ndigits: int = 4) -> Decimal:
|
|
9
|
+
return Decimal(str(round(smallest_unit_value / 10**decimals, ndigits=ndigits)))
|
|
10
|
+
|
|
11
|
+
|
|
8
12
|
def sol_to_lamports(sol: Decimal) -> int:
|
|
9
13
|
return int(sol * 10**9)
|
|
10
14
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from decimal import Decimal
|
|
2
|
-
|
|
3
1
|
import mm_crypto_utils
|
|
4
2
|
import pydash
|
|
5
3
|
from mm_crypto_utils import Nodes, Proxies
|
|
@@ -14,7 +12,6 @@ from spl.token.client import Token
|
|
|
14
12
|
from spl.token.constants import TOKEN_PROGRAM_ID
|
|
15
13
|
from spl.token.instructions import get_associated_token_address
|
|
16
14
|
|
|
17
|
-
import mm_sol.converters
|
|
18
15
|
from mm_sol import rpc, utils
|
|
19
16
|
from mm_sol.account import check_private_key, get_keypair
|
|
20
17
|
|
|
@@ -26,7 +23,7 @@ def transfer_token(
|
|
|
26
23
|
from_address: str | Pubkey,
|
|
27
24
|
private_key: str,
|
|
28
25
|
to_address: str | Pubkey,
|
|
29
|
-
amount:
|
|
26
|
+
amount: int, # smallest unit
|
|
30
27
|
decimals: int,
|
|
31
28
|
proxy: str | None = None,
|
|
32
29
|
timeout: float = 10,
|
|
@@ -60,7 +57,7 @@ def transfer_token(
|
|
|
60
57
|
source=from_token_account,
|
|
61
58
|
dest=recipient_token_account,
|
|
62
59
|
owner=from_address,
|
|
63
|
-
amount=
|
|
60
|
+
amount=amount,
|
|
64
61
|
decimals=decimals,
|
|
65
62
|
)
|
|
66
63
|
data.append(res)
|
|
@@ -75,7 +72,7 @@ def transfer_token_with_retries(
|
|
|
75
72
|
from_address: str | Pubkey,
|
|
76
73
|
private_key: str,
|
|
77
74
|
to_address: str | Pubkey,
|
|
78
|
-
amount:
|
|
75
|
+
amount: int, # smallest unit
|
|
79
76
|
decimals: int,
|
|
80
77
|
proxies: Proxies = None,
|
|
81
78
|
timeout: float = 10,
|
|
@@ -481,19 +481,19 @@ wheels = [
|
|
|
481
481
|
|
|
482
482
|
[[package]]
|
|
483
483
|
name = "mm-crypto-utils"
|
|
484
|
-
version = "0.0.
|
|
484
|
+
version = "0.0.19"
|
|
485
485
|
source = { registry = "https://pypi.org/simple" }
|
|
486
486
|
dependencies = [
|
|
487
487
|
{ name = "loguru" },
|
|
488
488
|
{ name = "mm-std" },
|
|
489
489
|
]
|
|
490
490
|
wheels = [
|
|
491
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
491
|
+
{ url = "https://files.pythonhosted.org/packages/49/0b/408f661aba09bfc3625652a6cf2ba516874aaaa4f6c9eaaeca450cf70004/mm_crypto_utils-0.0.19-py3-none-any.whl", hash = "sha256:b51184a53aa78cb70253c3c2e0ad56a98a214a854ffb74a4130a4401cd025538", size = 6992 },
|
|
492
492
|
]
|
|
493
493
|
|
|
494
494
|
[[package]]
|
|
495
495
|
name = "mm-sol"
|
|
496
|
-
version = "0.2.
|
|
496
|
+
version = "0.2.10"
|
|
497
497
|
source = { editable = "." }
|
|
498
498
|
dependencies = [
|
|
499
499
|
{ name = "base58" },
|
|
@@ -518,7 +518,7 @@ dev = [
|
|
|
518
518
|
requires-dist = [
|
|
519
519
|
{ name = "base58", specifier = "~=2.1.1" },
|
|
520
520
|
{ name = "jinja2", specifier = ">=3.1.5" },
|
|
521
|
-
{ name = "mm-crypto-utils", specifier = ">=0.0.
|
|
521
|
+
{ name = "mm-crypto-utils", specifier = ">=0.0.19" },
|
|
522
522
|
{ name = "solana", specifier = "~=0.36.3" },
|
|
523
523
|
{ name = "typer", specifier = ">=0.15.1" },
|
|
524
524
|
]
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
from decimal import Decimal
|
|
3
|
-
|
|
4
|
-
import mm_crypto_utils
|
|
5
|
-
from loguru import logger
|
|
6
|
-
from mm_crypto_utils import Nodes, Proxies
|
|
7
|
-
from mm_std import Ok, Result
|
|
8
|
-
from mm_std.str import split_on_plus_minus_tokens
|
|
9
|
-
|
|
10
|
-
from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
|
|
11
|
-
from mm_sol.converters import lamports_to_sol, sol_to_lamports, to_lamports
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def calc_var_value(value: str, *, var_name: str = "var", var_value: int | None = None, decimals: int | None = None) -> int:
|
|
15
|
-
if not isinstance(value, str):
|
|
16
|
-
raise TypeError(f"value is not str: {value}")
|
|
17
|
-
try:
|
|
18
|
-
var_name = var_name.lower()
|
|
19
|
-
result = 0
|
|
20
|
-
for token in split_on_plus_minus_tokens(value.lower()):
|
|
21
|
-
operator = token[0]
|
|
22
|
-
item = token[1:]
|
|
23
|
-
if item.isdigit():
|
|
24
|
-
item_value = int(item)
|
|
25
|
-
elif item.endswith("sol"):
|
|
26
|
-
item = item.removesuffix("sol")
|
|
27
|
-
item_value = sol_to_lamports(Decimal(item))
|
|
28
|
-
elif item.endswith("t"):
|
|
29
|
-
if decimals is None:
|
|
30
|
-
raise ValueError("t without decimals") # noqa: TRY301
|
|
31
|
-
item = item.removesuffix("t")
|
|
32
|
-
item_value = int(Decimal(item) * 10**decimals)
|
|
33
|
-
elif item.endswith(var_name):
|
|
34
|
-
if var_value is None:
|
|
35
|
-
raise ValueError("base value is not set") # noqa: TRY301
|
|
36
|
-
item = item.removesuffix(var_name)
|
|
37
|
-
k = Decimal(item) if item else Decimal(1)
|
|
38
|
-
item_value = int(k * var_value)
|
|
39
|
-
elif item.startswith("random(") and item.endswith(")"):
|
|
40
|
-
item = item.lstrip("random(").rstrip(")")
|
|
41
|
-
arr = item.split(",")
|
|
42
|
-
if len(arr) != 2:
|
|
43
|
-
raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
|
|
44
|
-
from_value = to_lamports(arr[0], decimals=decimals)
|
|
45
|
-
to_value = to_lamports(arr[1], decimals=decimals)
|
|
46
|
-
if from_value > to_value:
|
|
47
|
-
raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
|
|
48
|
-
item_value = random.randint(from_value, to_value)
|
|
49
|
-
else:
|
|
50
|
-
raise ValueError(f"wrong value: {value}") # noqa: TRY301
|
|
51
|
-
|
|
52
|
-
if operator == "+":
|
|
53
|
-
result += item_value
|
|
54
|
-
if operator == "-":
|
|
55
|
-
result -= item_value
|
|
56
|
-
|
|
57
|
-
return result # noqa: TRY300
|
|
58
|
-
except Exception as err:
|
|
59
|
-
raise ValueError(f"wrong value: {value}, error={err}") from err
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def is_sol_value_less_min_limit(value_min_limit: str | None, value: int, log_prefix: str | None = None) -> bool:
|
|
63
|
-
if value_min_limit is None:
|
|
64
|
-
return False
|
|
65
|
-
if value < calc_var_value(value_min_limit):
|
|
66
|
-
prefix = mm_crypto_utils.get_log_prefix(log_prefix)
|
|
67
|
-
logger.info("{}value is less min limit, value={}", prefix, lamports_to_sol(value))
|
|
68
|
-
return True
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def calc_sol_value(*, nodes: Nodes, value_str: str, address: str, proxies: Proxies, fee: int = 5000) -> Result[int]:
|
|
73
|
-
balance_value = None
|
|
74
|
-
if "balance" in value_str.lower():
|
|
75
|
-
res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
|
|
76
|
-
if res.is_err():
|
|
77
|
-
return res
|
|
78
|
-
balance_value = res.ok
|
|
79
|
-
value = calc_var_value(value_str, var_name="balance", var_value=balance_value)
|
|
80
|
-
if "balance" in value_str.lower():
|
|
81
|
-
value = value - fee
|
|
82
|
-
return Ok(value)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def calc_token_value(
|
|
86
|
-
*, nodes: Nodes, value_str: str, wallet_address: str, token_mint_address: str, token_decimals: int, proxies: Proxies
|
|
87
|
-
) -> Result[int]:
|
|
88
|
-
balance_value = None
|
|
89
|
-
if "balance" in value_str.lower():
|
|
90
|
-
res = get_token_balance_with_retries(
|
|
91
|
-
nodes=nodes,
|
|
92
|
-
owner_address=wallet_address,
|
|
93
|
-
token_mint_address=token_mint_address,
|
|
94
|
-
proxies=proxies,
|
|
95
|
-
retries=5,
|
|
96
|
-
)
|
|
97
|
-
if res.is_err():
|
|
98
|
-
return res
|
|
99
|
-
balance_value = res.ok
|
|
100
|
-
value = calc_var_value(value_str, var_name="balance", var_value=balance_value, decimals=token_decimals)
|
|
101
|
-
return Ok(value)
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from mm_sol.cli import calcs
|
|
4
|
-
from mm_sol.converters import to_lamports
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_calc_var_value():
|
|
8
|
-
assert calcs.calc_var_value("100") == 100
|
|
9
|
-
assert calcs.calc_var_value("10 + 2 - 5") == 7
|
|
10
|
-
assert calcs.calc_var_value("10 - random(2,2)") == 8
|
|
11
|
-
assert calcs.calc_var_value("1.5base + 1", var_value=10, var_name="base") == 16
|
|
12
|
-
assert calcs.calc_var_value("1.5estimate + 1", var_value=10, var_name="estimate") == 16
|
|
13
|
-
assert calcs.calc_var_value("12.2 sol") == to_lamports("12.2sol")
|
|
14
|
-
assert calcs.calc_var_value("12.2 t", decimals=6) == 12.2 * 10**6
|
|
15
|
-
|
|
16
|
-
with pytest.raises(ValueError):
|
|
17
|
-
calcs.calc_var_value("fff")
|
|
18
|
-
with pytest.raises(ValueError):
|
|
19
|
-
calcs.calc_var_value("12.3 sol + base", var_name="base")
|
|
20
|
-
with pytest.raises(ValueError):
|
|
21
|
-
calcs.calc_var_value("1.5estimate + 1", var_value=10)
|
|
22
|
-
with pytest.raises(ValueError):
|
|
23
|
-
calcs.calc_var_value("1.1t")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# def test_calc_function_args():
|
|
27
|
-
# res = calcs.calc_function_args('["xxx", random(100,200), 100, "aaa", random(1,3)]')
|
|
28
|
-
# assert json.loads(res)[1] >= 100
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|