mm-eth 0.2.0__py3-none-any.whl → 0.2.2__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_eth/abi.py +3 -3
- mm_eth/account.py +1 -1
- mm_eth/anvil.py +2 -2
- mm_eth/cli/__init__.py +0 -0
- mm_eth/cli/calcs.py +112 -0
- mm_eth/cli/cli.py +237 -0
- mm_eth/cli/cli_utils.py +105 -0
- mm_eth/cli/cmd/__init__.py +0 -0
- mm_eth/cli/cmd/balance_cmd.py +51 -0
- mm_eth/cli/cmd/balances_cmd.py +124 -0
- mm_eth/cli/cmd/call_contract_cmd.py +43 -0
- mm_eth/cli/cmd/config_example_cmd.py +9 -0
- mm_eth/cli/cmd/deploy_cmd.py +43 -0
- mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
- mm_eth/cli/cmd/mnemonic_cmd.py +26 -0
- mm_eth/cli/cmd/node_cmd.py +47 -0
- mm_eth/cli/cmd/private_key_cmd.py +10 -0
- mm_eth/cli/cmd/rpc_cmd.py +78 -0
- mm_eth/cli/cmd/send_contract_cmd.py +251 -0
- mm_eth/cli/cmd/solc_cmd.py +24 -0
- mm_eth/cli/cmd/token_cmd.py +29 -0
- mm_eth/cli/cmd/transfer_erc20_cmd.py +277 -0
- mm_eth/cli/cmd/transfer_eth_cmd.py +254 -0
- mm_eth/cli/cmd/vault_cmd.py +16 -0
- mm_eth/cli/config_examples/balances.yml +15 -0
- mm_eth/cli/config_examples/call_contract.yml +5 -0
- mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
- mm_eth/cli/config_examples/transfer_eth.yml +24 -0
- mm_eth/cli/print_helpers.py +37 -0
- mm_eth/cli/rpc_helpers.py +140 -0
- mm_eth/cli/validators.py +85 -0
- mm_eth/erc20.py +8 -7
- mm_eth/rpc.py +8 -8
- mm_eth/solc.py +2 -3
- mm_eth/tx.py +3 -5
- mm_eth/utils.py +11 -16
- mm_eth/vault.py +5 -5
- mm_eth-0.2.2.dist-info/METADATA +9 -0
- mm_eth-0.2.2.dist-info/RECORD +47 -0
- mm_eth-0.2.2.dist-info/entry_points.txt +2 -0
- mm_eth-0.2.0.dist-info/METADATA +0 -7
- mm_eth-0.2.0.dist-info/RECORD +0 -18
- {mm_eth-0.2.0.dist-info → mm_eth-0.2.2.dist-info}/WHEEL +0 -0
mm_eth/abi.py
CHANGED
|
@@ -64,7 +64,7 @@ def decode_function_input(contract_abi: ABI, tx_input: str) -> FunctionInput:
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def get_function_abi(contr_abi: ABI, fn_name: str) -> ABIFunction:
|
|
67
|
-
abi = pydash.find(contr_abi, lambda x: x.get("name", None) == fn_name and x.get("type", None) == "function") # type:ignore
|
|
67
|
+
abi = pydash.find(contr_abi, lambda x: x.get("name", None) == fn_name and x.get("type", None) == "function") # type: ignore[call-overload, attr-defined]
|
|
68
68
|
if not abi:
|
|
69
69
|
raise ValueError("can't find abi for function: " + fn_name)
|
|
70
70
|
return cast(ABIFunction, abi)
|
|
@@ -122,8 +122,8 @@ def parse_function_signatures(contract_abi: ABI) -> dict[str, str]:
|
|
|
122
122
|
result: dict[str, str] = {}
|
|
123
123
|
for item in contract_abi:
|
|
124
124
|
if item.get("type", None) == "function":
|
|
125
|
-
function_name = item["name"] # type: ignore
|
|
126
|
-
types = ",".join([i["type"] for i in item["inputs"]]) # type: ignore
|
|
125
|
+
function_name = item["name"] # type: ignore[typeddict-item]
|
|
126
|
+
types = ",".join([i["type"] for i in item["inputs"]]) # type: ignore[typeddict-item]
|
|
127
127
|
function_name_and_types = f"{function_name}({types})"
|
|
128
128
|
result[function_name_and_types] = encode_function_signature(function_name_and_types)
|
|
129
129
|
return result
|
mm_eth/account.py
CHANGED
|
@@ -67,6 +67,6 @@ def create_private_keys_dict(private_keys: list[str]) -> dict[str, str]: # addr
|
|
|
67
67
|
def is_private_key(private_key: str) -> bool:
|
|
68
68
|
try:
|
|
69
69
|
key_api.PrivateKey(decode_hex(private_key)).public_key.to_address()
|
|
70
|
-
return True
|
|
70
|
+
return True # noqa: TRY300
|
|
71
71
|
except Exception:
|
|
72
72
|
return False
|
mm_eth/anvil.py
CHANGED
|
@@ -10,7 +10,7 @@ from mm_eth import account, rpc
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Anvil:
|
|
13
|
-
def __init__(self, *, chain_id: int, port: int, mnemonic: str):
|
|
13
|
+
def __init__(self, *, chain_id: int, port: int, mnemonic: str) -> None:
|
|
14
14
|
self.chain_id = chain_id
|
|
15
15
|
self.port = port
|
|
16
16
|
self.mnemonic = mnemonic
|
|
@@ -18,7 +18,7 @@ class Anvil:
|
|
|
18
18
|
|
|
19
19
|
def start_process(self) -> None:
|
|
20
20
|
cmd = f"anvil -m '{self.mnemonic}' -p {self.port} --chain-id {self.chain_id}"
|
|
21
|
-
self.process = Popen(cmd, shell=True) # nosec
|
|
21
|
+
self.process = Popen(cmd, shell=True) # noqa: S602 # nosec
|
|
22
22
|
time.sleep(3)
|
|
23
23
|
|
|
24
24
|
def stop(self) -> None:
|
mm_eth/cli/__init__.py
ADDED
|
File without changes
|
mm_eth/cli/calcs.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
from mm_std.random_ import random_decimal
|
|
6
|
+
from mm_std.str import split_on_plus_minus_tokens
|
|
7
|
+
|
|
8
|
+
from mm_eth.utils import from_wei_str, to_wei
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def calc_var_wei_value(value: str, *, var_name: str = "var", var_value: int | None = None, decimals: int | None = None) -> int:
|
|
12
|
+
if not isinstance(value, str):
|
|
13
|
+
raise TypeError(f"value is not str: {value}")
|
|
14
|
+
try:
|
|
15
|
+
var_name = var_name.lower()
|
|
16
|
+
result = 0
|
|
17
|
+
for token in split_on_plus_minus_tokens(value.lower()):
|
|
18
|
+
operator = token[0]
|
|
19
|
+
item = token[1:]
|
|
20
|
+
if item.isdigit():
|
|
21
|
+
item_value = int(item)
|
|
22
|
+
elif item.endswith("eth"):
|
|
23
|
+
item = item.removesuffix("eth")
|
|
24
|
+
item_value = int(Decimal(item) * 10**18)
|
|
25
|
+
elif item.endswith("ether"):
|
|
26
|
+
item = item.removesuffix("ether")
|
|
27
|
+
item_value = int(Decimal(item) * 10**18)
|
|
28
|
+
elif item.endswith("gwei"):
|
|
29
|
+
item = item.removesuffix("gwei")
|
|
30
|
+
item_value = int(Decimal(item) * 10**9)
|
|
31
|
+
elif item.endswith("t"):
|
|
32
|
+
if decimals is None:
|
|
33
|
+
raise ValueError("t without decimals") # noqa: TRY301
|
|
34
|
+
item = item.removesuffix("t")
|
|
35
|
+
item_value = int(Decimal(item) * 10**decimals)
|
|
36
|
+
elif item.endswith(var_name):
|
|
37
|
+
if var_value is None:
|
|
38
|
+
raise ValueError("base value is not set") # noqa: TRY301
|
|
39
|
+
item = item.removesuffix(var_name)
|
|
40
|
+
k = Decimal(item) if item else Decimal(1)
|
|
41
|
+
item_value = int(k * var_value)
|
|
42
|
+
elif item.startswith("random(") and item.endswith(")"):
|
|
43
|
+
item = item.lstrip("random(").rstrip(")")
|
|
44
|
+
arr = item.split(",")
|
|
45
|
+
if len(arr) != 2:
|
|
46
|
+
raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
|
|
47
|
+
from_value = to_wei(arr[0], decimals=decimals)
|
|
48
|
+
to_value = to_wei(arr[1], decimals=decimals)
|
|
49
|
+
if from_value > to_value:
|
|
50
|
+
raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
|
|
51
|
+
item_value = random.randint(from_value, to_value)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"wrong value: {value}") # noqa: TRY301
|
|
54
|
+
|
|
55
|
+
if operator == "+":
|
|
56
|
+
result += item_value
|
|
57
|
+
if operator == "-":
|
|
58
|
+
result -= item_value
|
|
59
|
+
|
|
60
|
+
return result # noqa: TRY300
|
|
61
|
+
except Exception as err:
|
|
62
|
+
raise ValueError(f"wrong value: {value}, error={err}") from err
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def calc_decimal_value(value: str) -> Decimal:
|
|
66
|
+
value = value.lower().strip()
|
|
67
|
+
if value.startswith("random(") and value.endswith(")"):
|
|
68
|
+
arr = value.lstrip("random(").rstrip(")").split(",")
|
|
69
|
+
if len(arr) != 2:
|
|
70
|
+
raise ValueError(f"wrong value, random part: {value}")
|
|
71
|
+
from_value = Decimal(arr[0])
|
|
72
|
+
to_value = Decimal(arr[1])
|
|
73
|
+
if from_value > to_value:
|
|
74
|
+
raise ValueError(f"wrong value, random part: {value}")
|
|
75
|
+
return random_decimal(from_value, to_value)
|
|
76
|
+
return Decimal(value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def calc_function_args(value: str) -> str:
|
|
80
|
+
while True:
|
|
81
|
+
if "random(" not in value:
|
|
82
|
+
return value
|
|
83
|
+
start_index = value.index("random(")
|
|
84
|
+
stop_index = value.index(")", start_index)
|
|
85
|
+
random_range = [int(v.strip()) for v in value[start_index + 7 : stop_index].split(",")]
|
|
86
|
+
if len(random_range) != 2:
|
|
87
|
+
raise ValueError("wrong random(from,to) template")
|
|
88
|
+
rand_value = str(random.randint(random_range[0], random_range[1]))
|
|
89
|
+
value = value[0:start_index] + rand_value + value[stop_index + 1 :]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_value_less_min_limit(
|
|
93
|
+
value_min_limit: str | None,
|
|
94
|
+
value: int,
|
|
95
|
+
value_unit: str,
|
|
96
|
+
decimals: int | None = None,
|
|
97
|
+
log_prefix: str | None = None,
|
|
98
|
+
) -> bool:
|
|
99
|
+
if value_min_limit is None:
|
|
100
|
+
return False
|
|
101
|
+
if value < calc_var_wei_value(value_min_limit, decimals=decimals):
|
|
102
|
+
prefix = _get_prefix(log_prefix)
|
|
103
|
+
logger.info("{}value is less min limit, value={}", prefix, from_wei_str(value, value_unit, decimals=decimals))
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _get_prefix(log_prefix: str | None) -> str:
|
|
109
|
+
prefix = log_prefix or ""
|
|
110
|
+
if prefix:
|
|
111
|
+
prefix += ": "
|
|
112
|
+
return prefix
|
mm_eth/cli/cli.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from mm_std import PrintFormat, print_plain
|
|
6
|
+
|
|
7
|
+
from . import cli_utils
|
|
8
|
+
from .cmd import (
|
|
9
|
+
balance_cmd,
|
|
10
|
+
balances_cmd,
|
|
11
|
+
call_contract_cmd,
|
|
12
|
+
config_example_cmd,
|
|
13
|
+
deploy_cmd,
|
|
14
|
+
encode_input_data_cmd,
|
|
15
|
+
mnemonic_cmd,
|
|
16
|
+
node_cmd,
|
|
17
|
+
private_key_cmd,
|
|
18
|
+
rpc_cmd,
|
|
19
|
+
send_contract_cmd,
|
|
20
|
+
solc_cmd,
|
|
21
|
+
token_cmd,
|
|
22
|
+
transfer_erc20_cmd,
|
|
23
|
+
transfer_eth_cmd,
|
|
24
|
+
vault_cmd,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
28
|
+
|
|
29
|
+
wallet_app = typer.Typer(no_args_is_help=True, help="Wallet commands: generate mnemonic, private to address")
|
|
30
|
+
app.add_typer(wallet_app, name="wallet")
|
|
31
|
+
app.add_typer(wallet_app, name="w", hidden=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConfigExample(str, Enum):
|
|
35
|
+
TRANSFER_ETH = "transfer-eth"
|
|
36
|
+
TRANSFER_ERC20 = "transfer-erc20"
|
|
37
|
+
BALANCES = "balances"
|
|
38
|
+
CALL_CONTRACT = "call-contract"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command(name="balance", help="Gen account balance")
|
|
42
|
+
def balance_command(
|
|
43
|
+
wallet_address: Annotated[str, typer.Argument()],
|
|
44
|
+
token_address: Annotated[str | None, typer.Option("--token", "-t")] = None,
|
|
45
|
+
rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL")] = "", # nosec
|
|
46
|
+
wei: bool = typer.Option(False, "--wei", "-w", help="Print balances in wei units"),
|
|
47
|
+
print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.PLAIN,
|
|
48
|
+
) -> None:
|
|
49
|
+
balance_cmd.run(rpc_url, wallet_address, token_address, wei, print_format)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command(name="token", help="Get token info")
|
|
53
|
+
def token_command(
|
|
54
|
+
token_address: Annotated[str, typer.Argument()],
|
|
55
|
+
rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL")] = "",
|
|
56
|
+
) -> None:
|
|
57
|
+
token_cmd.run(rpc_url, token_address)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command(name="node", help="Check RPC url")
|
|
61
|
+
def node_command(
|
|
62
|
+
urls: Annotated[list[str], typer.Argument()],
|
|
63
|
+
print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.TABLE,
|
|
64
|
+
proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
node_cmd.run(urls, print_format, proxy)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@wallet_app.command(name="mnemonic", help="Generate eth accounts based on a mnemonic")
|
|
70
|
+
def mnemonic_command( # nosec
|
|
71
|
+
mnemonic: Annotated[str, typer.Option("--mnemonic", "-m")] = "",
|
|
72
|
+
passphrase: Annotated[str, typer.Option("--passphrase", "-pass")] = "",
|
|
73
|
+
print_path: bool = typer.Option(False, "--print_path"),
|
|
74
|
+
path_prefix: Annotated[str, typer.Option("--path")] = "m/44'/60'/0'/0",
|
|
75
|
+
limit: int = typer.Option(10, "--limit", "-l"),
|
|
76
|
+
save_file: str = typer.Option("", "--save", "-s", help="Save private keys to a file"),
|
|
77
|
+
) -> None:
|
|
78
|
+
mnemonic_cmd.run(
|
|
79
|
+
mnemonic,
|
|
80
|
+
passphrase=passphrase,
|
|
81
|
+
print_path=print_path,
|
|
82
|
+
limit=limit,
|
|
83
|
+
path_prefix=path_prefix,
|
|
84
|
+
save_file=save_file,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@wallet_app.command(name="private-key", help="Print an address for a private key")
|
|
89
|
+
def private_key_command(private_key: str) -> None:
|
|
90
|
+
private_key_cmd.run(private_key)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command(name="solc", help="Compile a solidity file")
|
|
94
|
+
def solc_command(
|
|
95
|
+
contract_path: str,
|
|
96
|
+
tmp_dir: str = "/tmp", # noqa: S108 # nosec
|
|
97
|
+
print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.PLAIN,
|
|
98
|
+
) -> None:
|
|
99
|
+
solc_cmd.run(contract_path, tmp_dir, print_format)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command(name="vault", help="Save private keys to vault")
|
|
103
|
+
def vault_command(
|
|
104
|
+
keys_url: Annotated[
|
|
105
|
+
str,
|
|
106
|
+
typer.Option(..., "--url", "-u", help="Url to keys, for example https://vault.site.com:8200/v1/kv/keys1"),
|
|
107
|
+
],
|
|
108
|
+
vault_token: Annotated[
|
|
109
|
+
str,
|
|
110
|
+
typer.Option(..., "--token", "-t", prompt=True, hide_input=True, prompt_required=False, help="A vault token"),
|
|
111
|
+
],
|
|
112
|
+
keys_file: Annotated[str, typer.Option(..., "--file", "-f", help="Path to a file with private keys")],
|
|
113
|
+
) -> None:
|
|
114
|
+
vault_cmd.run(keys_url, vault_token, keys_file)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command(name="rpc", help="Call a JSON-RPC method")
|
|
118
|
+
def rpc_command(
|
|
119
|
+
method: Annotated[str, typer.Argument()] = "",
|
|
120
|
+
params: Annotated[str, typer.Argument()] = "[]",
|
|
121
|
+
rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL", help="RPC node url")] = "",
|
|
122
|
+
hex2dec: Annotated[bool, typer.Option("--hex2dec", "-d", help="Print result in decimal value")] = False,
|
|
123
|
+
) -> None:
|
|
124
|
+
rpc_cmd.run(rpc_url, method, params, hex2dec)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command(name="transfer-eth", help="Transfer eth / base token from one or many accounts")
|
|
128
|
+
def transfer_eth_command(
|
|
129
|
+
config_path: str,
|
|
130
|
+
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
131
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
132
|
+
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
133
|
+
no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
134
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
135
|
+
) -> None:
|
|
136
|
+
transfer_eth_cmd.run(
|
|
137
|
+
config_path,
|
|
138
|
+
print_balances=print_balances,
|
|
139
|
+
print_config=print_config,
|
|
140
|
+
debug=debug,
|
|
141
|
+
no_receipt=no_receipt,
|
|
142
|
+
emulate=emulate,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command(name="transfer-erc20", help="Transfer ERC20 token from one or many accounts")
|
|
147
|
+
def transfer_erc20_command(
|
|
148
|
+
config_path: str,
|
|
149
|
+
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
150
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
151
|
+
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
152
|
+
no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
153
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
154
|
+
) -> None:
|
|
155
|
+
transfer_erc20_cmd.run(
|
|
156
|
+
config_path,
|
|
157
|
+
print_balances=print_balances,
|
|
158
|
+
print_config=print_config,
|
|
159
|
+
debug=debug,
|
|
160
|
+
no_receipt=no_receipt,
|
|
161
|
+
emulate=emulate,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command(name="send-contract", help="Send transactions to a contract")
|
|
166
|
+
def send_contract_command(
|
|
167
|
+
config_path: str,
|
|
168
|
+
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
169
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
170
|
+
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
171
|
+
no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
172
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
173
|
+
) -> None:
|
|
174
|
+
send_contract_cmd.run(
|
|
175
|
+
config_path,
|
|
176
|
+
print_balances=print_balances,
|
|
177
|
+
print_config=print_config,
|
|
178
|
+
debug=debug,
|
|
179
|
+
no_receipt=no_receipt,
|
|
180
|
+
emulate=emulate,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.command(name="balances", help="Print base and ERC20 token balances")
|
|
185
|
+
def balances_command(
|
|
186
|
+
config_path: str,
|
|
187
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
|
|
188
|
+
nonce: bool = typer.Option(False, "--nonce", "-n", help="Print nonce also"),
|
|
189
|
+
wei: bool = typer.Option(False, "--wei", "-w", help="Show balances in WEI"),
|
|
190
|
+
) -> None:
|
|
191
|
+
balances_cmd.run(config_path, print_config, wei, nonce)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.command(name="call-contract", help="Call a method on a contract")
|
|
195
|
+
def call_contract_command(
|
|
196
|
+
config_path: str,
|
|
197
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
|
|
198
|
+
) -> None:
|
|
199
|
+
call_contract_cmd.run(config_path, print_config)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.command(name="deploy", help="Deploy a smart contract onchain")
|
|
203
|
+
def deploy_command(
|
|
204
|
+
config_path: str,
|
|
205
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
|
|
206
|
+
) -> None:
|
|
207
|
+
deploy_cmd.run(config_path, print_config=print_config)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.command(name="config-example", help="Print an example of config for a command")
|
|
211
|
+
def config_example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
|
|
212
|
+
config_example_cmd.run(command)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command(name="encode-input-data", help="Encode input data by a function signature")
|
|
216
|
+
def encode_input_data(
|
|
217
|
+
funtion_signature: str = typer.Argument(help="Function signature, for example: transfer(address, uint256)"),
|
|
218
|
+
args_str: str = typer.Argument(
|
|
219
|
+
help="""Function arguments, as an array string. For example: '["0xA659FB44eB5d4bFaC1074Cb426b1b11D58D28308", 123]' """,
|
|
220
|
+
),
|
|
221
|
+
) -> None:
|
|
222
|
+
encode_input_data_cmd.run(funtion_signature, args_str)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def version_callback(value: bool) -> None:
|
|
226
|
+
if value:
|
|
227
|
+
print_plain(f"mm-eth-cli version: {cli_utils.get_version()}")
|
|
228
|
+
raise typer.Exit
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@app.callback()
|
|
232
|
+
def main(_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True)) -> None:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main_":
|
|
237
|
+
app()
|
mm_eth/cli/cli_utils.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import eth_utils
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from mm_std import BaseConfig, fatal, print_json, str_to_list
|
|
8
|
+
|
|
9
|
+
from mm_eth import account
|
|
10
|
+
from mm_eth.account import is_private_key
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_version() -> str:
|
|
14
|
+
return importlib.metadata.version("mm-eth-cli")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def public_rpc_url(url: str | None) -> str:
|
|
18
|
+
if not url or url == "1":
|
|
19
|
+
return "https://ethereum.publicnode.com"
|
|
20
|
+
if url.startswith(("http://", "https://", "ws://", "wss://")):
|
|
21
|
+
return url
|
|
22
|
+
|
|
23
|
+
match url.lower():
|
|
24
|
+
case "opbnb" | "204":
|
|
25
|
+
return "https://opbnb-mainnet-rpc.bnbchain.org"
|
|
26
|
+
case _:
|
|
27
|
+
return url
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init_logger(debug: bool, log_debug_file: str | None, log_info_file: str | None) -> None:
|
|
31
|
+
if debug:
|
|
32
|
+
level = "DEBUG"
|
|
33
|
+
format_ = "<green>{time:YYYY-MM-DD HH:mm:ss}</green> <level>{level}</level> {message}"
|
|
34
|
+
else:
|
|
35
|
+
level = "INFO"
|
|
36
|
+
format_ = "{message}"
|
|
37
|
+
|
|
38
|
+
logger.remove()
|
|
39
|
+
logger.add(sys.stderr, format=format_, colorize=True, level=level)
|
|
40
|
+
if log_debug_file:
|
|
41
|
+
logger.add(Path(log_debug_file).expanduser(), format="{time:YYYY-MM-DD HH:mm:ss} {level} {message}")
|
|
42
|
+
if log_info_file:
|
|
43
|
+
logger.add(Path(log_info_file).expanduser(), format="{message}", level="INFO")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_private_keys(addresses: list[str], private_keys: dict[str, str]) -> None:
|
|
47
|
+
for address in addresses:
|
|
48
|
+
address = address.lower() # noqa: PLW2901
|
|
49
|
+
if address not in private_keys:
|
|
50
|
+
fatal(f"no private key for {address}")
|
|
51
|
+
if account.private_to_address(private_keys[address]) != address:
|
|
52
|
+
fatal(f"no private key for {address}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_tx_addresses_from_str(v: str | None) -> list[tuple[str, str]]:
|
|
56
|
+
result: list[tuple[str, str]] = []
|
|
57
|
+
if v is None:
|
|
58
|
+
return result
|
|
59
|
+
for line in str_to_list(v, remove_comments=True):
|
|
60
|
+
arr = line.split()
|
|
61
|
+
if len(arr) == 2 and eth_utils.is_address(arr[0]) and eth_utils.is_address(arr[1]):
|
|
62
|
+
result.append((arr[0].lower(), arr[1].lower()))
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_tx_addresses_from_files(addresses_from_file: str, addresses_to_file: str) -> list[tuple[str, str]]:
|
|
67
|
+
from_file = Path(addresses_from_file).expanduser()
|
|
68
|
+
to_file = Path(addresses_to_file).expanduser()
|
|
69
|
+
if not from_file.is_file():
|
|
70
|
+
raise ValueError(f"can't read addresses from 'addresses_from_file={addresses_from_file}")
|
|
71
|
+
if not to_file.is_file():
|
|
72
|
+
raise ValueError(f"can't read addresses from 'addresses_to_file={addresses_to_file}")
|
|
73
|
+
|
|
74
|
+
# get addresses_from
|
|
75
|
+
addresses_from = []
|
|
76
|
+
for line in from_file.read_text().strip().split("\n"):
|
|
77
|
+
if not eth_utils.is_address(line):
|
|
78
|
+
raise ValueError(f"illigal address in addresses_from_file: {line}")
|
|
79
|
+
addresses_from.append(line.lower())
|
|
80
|
+
|
|
81
|
+
# get addresses_to
|
|
82
|
+
addresses_to = []
|
|
83
|
+
for line in to_file.read_text().strip().split("\n"):
|
|
84
|
+
if not eth_utils.is_address(line):
|
|
85
|
+
raise ValueError(f"illigal address in addresses_to_file: {line}")
|
|
86
|
+
addresses_to.append(line.lower())
|
|
87
|
+
|
|
88
|
+
if len(addresses_from) != len(addresses_to):
|
|
89
|
+
raise ValueError("len(addresses_from) != len(addresses_to)")
|
|
90
|
+
|
|
91
|
+
return list(zip(addresses_from, addresses_to, strict=True))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_private_keys_from_file(private_keys_file: str) -> list[str]:
|
|
95
|
+
lines = Path(private_keys_file).expanduser().read_text().split()
|
|
96
|
+
return [line for line in lines if is_private_key(line)]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read_config[T: BaseConfig](config_type: type[T], config_path: Path) -> T:
|
|
100
|
+
res = config_type.read_config(config_path)
|
|
101
|
+
if res.is_ok():
|
|
102
|
+
return res.unwrap()
|
|
103
|
+
|
|
104
|
+
print_json(res.err)
|
|
105
|
+
sys.exit(1)
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from mm_std import Err, Ok, PrintFormat, fatal, print_json, print_plain
|
|
2
|
+
|
|
3
|
+
from mm_eth import erc20, rpc
|
|
4
|
+
from mm_eth.cli.cli_utils import public_rpc_url
|
|
5
|
+
from mm_eth.utils import from_wei_str
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(rpc_url: str, wallet_address: str, token_address: str | None, wei: bool, print_format: PrintFormat) -> None:
|
|
9
|
+
rpc_url = public_rpc_url(rpc_url)
|
|
10
|
+
json_result: dict[str, object] = {}
|
|
11
|
+
|
|
12
|
+
# nonce
|
|
13
|
+
nonce = rpc.eth_get_transaction_count(rpc_url, wallet_address).ok_or_err()
|
|
14
|
+
print_plain(f"nonce: {nonce}", print_format)
|
|
15
|
+
json_result["nonce"] = nonce
|
|
16
|
+
|
|
17
|
+
# balance
|
|
18
|
+
balance_res = rpc.eth_get_balance(rpc_url, wallet_address)
|
|
19
|
+
if isinstance(balance_res, Ok):
|
|
20
|
+
balance = str(balance_res.ok) if wei else from_wei_str(balance_res.ok, "eth")
|
|
21
|
+
else:
|
|
22
|
+
balance = balance_res.err
|
|
23
|
+
print_plain(f"eth_balance: {balance}", print_format)
|
|
24
|
+
json_result["eth_balance"] = balance
|
|
25
|
+
|
|
26
|
+
if token_address is not None:
|
|
27
|
+
# token decimal
|
|
28
|
+
decimals_res = erc20.get_decimals(rpc_url, token_address)
|
|
29
|
+
if isinstance(decimals_res, Err):
|
|
30
|
+
fatal(f"error: can't get token decimals: {decimals_res.err}")
|
|
31
|
+
decimals = decimals_res.ok
|
|
32
|
+
print_plain(f"token_decimal: {decimals}", print_format)
|
|
33
|
+
json_result["token_decimal"] = decimals
|
|
34
|
+
|
|
35
|
+
# token symbol
|
|
36
|
+
symbol_res = erc20.get_symbol(rpc_url, token_address)
|
|
37
|
+
if isinstance(symbol_res, Err):
|
|
38
|
+
fatal(f"error: can't get token symbol: {symbol_res.err}")
|
|
39
|
+
symbol = symbol_res.ok
|
|
40
|
+
print_plain(f"token_symbol: {symbol}", print_format)
|
|
41
|
+
json_result["token_symbol"] = symbol
|
|
42
|
+
|
|
43
|
+
# token balance
|
|
44
|
+
balance_res = erc20.get_balance(rpc_url, token_address, wallet_address)
|
|
45
|
+
if isinstance(balance_res, Err):
|
|
46
|
+
fatal(f"error: can't get token balance: {balance_res.err}")
|
|
47
|
+
balance = str(balance_res.ok) if wei else from_wei_str(balance_res.ok, "t", decimals=decimals)
|
|
48
|
+
print_plain(f"token_balance: {balance}", print_format)
|
|
49
|
+
json_result["token_balance"] = balance
|
|
50
|
+
|
|
51
|
+
print_json(json_result, print_format)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mm_std import BaseConfig, Err, Ok, fatal, print_json
|
|
6
|
+
from pydantic import Field, field_validator
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from mm_eth import erc20, rpc
|
|
11
|
+
from mm_eth.cli import cli_utils, validators
|
|
12
|
+
from mm_eth.utils import from_token_wei_str, from_wei_str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Config(BaseConfig):
|
|
16
|
+
addresses: list[str]
|
|
17
|
+
tokens: list[str] = Field(default_factory=list)
|
|
18
|
+
nodes: list[str]
|
|
19
|
+
round_ndigits: int = 5
|
|
20
|
+
|
|
21
|
+
@field_validator("nodes", mode="before")
|
|
22
|
+
@classmethod
|
|
23
|
+
def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
24
|
+
return validators.nodes_validator(v)
|
|
25
|
+
|
|
26
|
+
@field_validator("tokens", "addresses", mode="before")
|
|
27
|
+
@classmethod
|
|
28
|
+
def addresses_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
29
|
+
return validators.addresses_validator(v)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Token:
|
|
34
|
+
address: str
|
|
35
|
+
decimals: int
|
|
36
|
+
symbol: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> None:
|
|
40
|
+
config = cli_utils.read_config(Config, Path(config_path))
|
|
41
|
+
if print_config:
|
|
42
|
+
print_json(config.model_dump())
|
|
43
|
+
sys.exit(0)
|
|
44
|
+
|
|
45
|
+
tokens = _get_tokens_info(config)
|
|
46
|
+
|
|
47
|
+
table = Table(title="balances")
|
|
48
|
+
table.add_column("address")
|
|
49
|
+
if show_nonce:
|
|
50
|
+
table.add_column("nonce")
|
|
51
|
+
table.add_column("wei" if wei else "eth")
|
|
52
|
+
for t in tokens:
|
|
53
|
+
table.add_column(t.symbol)
|
|
54
|
+
|
|
55
|
+
base_sum = 0
|
|
56
|
+
token_sum: dict[str, int] = {t.address: 0 for t in tokens}
|
|
57
|
+
with Live(table, refresh_per_second=0.5):
|
|
58
|
+
for address in config.addresses:
|
|
59
|
+
row = [address]
|
|
60
|
+
if show_nonce:
|
|
61
|
+
row.append(str(rpc.eth_get_transaction_count(config.nodes, address, attempts=5).ok_or_err()))
|
|
62
|
+
|
|
63
|
+
base_balance_res = rpc.eth_get_balance(config.nodes, address, attempts=5)
|
|
64
|
+
if isinstance(base_balance_res, Ok):
|
|
65
|
+
base_sum += base_balance_res.ok
|
|
66
|
+
if wei:
|
|
67
|
+
row.append(str(base_balance_res.ok))
|
|
68
|
+
else:
|
|
69
|
+
row.append(
|
|
70
|
+
from_wei_str(base_balance_res.ok, "eth", round_ndigits=config.round_ndigits, print_unit_name=False),
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
row.append(base_balance_res.err)
|
|
74
|
+
|
|
75
|
+
for t in tokens:
|
|
76
|
+
token_balance_res = erc20.get_balance(config.nodes, t.address, address, attempts=5)
|
|
77
|
+
if isinstance(token_balance_res, Ok):
|
|
78
|
+
token_sum[t.address] += token_balance_res.ok
|
|
79
|
+
if wei:
|
|
80
|
+
row.append(str(token_balance_res.ok))
|
|
81
|
+
else:
|
|
82
|
+
row.append(
|
|
83
|
+
from_token_wei_str(
|
|
84
|
+
token_balance_res.ok,
|
|
85
|
+
decimals=t.decimals,
|
|
86
|
+
round_ndigits=config.round_ndigits,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
row.append(token_balance_res.err)
|
|
91
|
+
|
|
92
|
+
table.add_row(*row)
|
|
93
|
+
|
|
94
|
+
sum_row = ["sum"]
|
|
95
|
+
if show_nonce:
|
|
96
|
+
sum_row.append("")
|
|
97
|
+
if wei:
|
|
98
|
+
sum_row.append(str(base_sum))
|
|
99
|
+
sum_row.extend([str(token_sum[t.address]) for t in tokens])
|
|
100
|
+
else:
|
|
101
|
+
sum_row.append(from_wei_str(base_sum, "eth", round_ndigits=config.round_ndigits, print_unit_name=False))
|
|
102
|
+
sum_row.extend(
|
|
103
|
+
[from_token_wei_str(token_sum[t.address], t.decimals, round_ndigits=config.round_ndigits) for t in tokens]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
table.add_row(*sum_row)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_tokens_info(config: Config) -> list[Token]:
|
|
110
|
+
result: list[Token] = []
|
|
111
|
+
for address in config.tokens:
|
|
112
|
+
decimals_res = erc20.get_decimals(config.nodes, address, attempts=5)
|
|
113
|
+
if isinstance(decimals_res, Err):
|
|
114
|
+
fatal(f"can't get token {address} decimals: {decimals_res.err}")
|
|
115
|
+
decimal = decimals_res.ok
|
|
116
|
+
|
|
117
|
+
symbols_res = erc20.get_symbol(config.nodes, address, attempts=5)
|
|
118
|
+
if isinstance(symbols_res, Err):
|
|
119
|
+
fatal(f"can't get token {address} symbol: {symbols_res.err}")
|
|
120
|
+
symbol = symbols_res.ok
|
|
121
|
+
|
|
122
|
+
result.append(Token(address=address, decimals=decimal, symbol=symbol))
|
|
123
|
+
|
|
124
|
+
return result
|