mm-eth 0.2.1__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/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-0.2.1.dist-info → mm_eth-0.2.2.dist-info}/METADATA +4 -2
- 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.1.dist-info/RECORD +0 -18
- {mm_eth-0.2.1.dist-info → mm_eth-0.2.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from mm_std import BaseConfig, fatal, print_json
|
|
6
|
+
from pydantic import StrictStr
|
|
7
|
+
|
|
8
|
+
from mm_eth import account, deploy
|
|
9
|
+
from mm_eth.cli import cli_utils, rpc_helpers
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config(BaseConfig):
|
|
13
|
+
private_key: StrictStr
|
|
14
|
+
nonce: int | None = None
|
|
15
|
+
gas: StrictStr
|
|
16
|
+
max_fee_per_gas: str
|
|
17
|
+
max_priority_fee_per_gas: str
|
|
18
|
+
value: str | None = None
|
|
19
|
+
contract_bin: StrictStr
|
|
20
|
+
constructor_types: StrictStr = "[]"
|
|
21
|
+
constructor_values: StrictStr = "[]"
|
|
22
|
+
chain_id: int
|
|
23
|
+
node: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(config_path: str, *, print_config: bool) -> None:
|
|
27
|
+
config = cli_utils.read_config(Config, Path(config_path))
|
|
28
|
+
if print_config:
|
|
29
|
+
print_json(config.model_dump(exclude={"private_key"}))
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
constructor_types = yaml.full_load(config.constructor_types)
|
|
33
|
+
constructor_values = yaml.full_load(config.constructor_values)
|
|
34
|
+
|
|
35
|
+
sender_address = account.private_to_address(config.private_key)
|
|
36
|
+
if sender_address is None:
|
|
37
|
+
fatal("private address is wrong")
|
|
38
|
+
|
|
39
|
+
nonce = rpc_helpers.get_nonce(config.node, sender_address)
|
|
40
|
+
if nonce is None:
|
|
41
|
+
fatal("can't get nonce")
|
|
42
|
+
|
|
43
|
+
deploy.get_deploy_contract_data(config.contract_bin, constructor_types, constructor_values)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from mm_std import print_plain
|
|
4
|
+
|
|
5
|
+
from mm_eth import abi
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(function_signature: str, args_str: str) -> None:
|
|
9
|
+
args_str = args_str.replace("'", '"')
|
|
10
|
+
print_plain(abi.encode_function_input_by_signature(function_signature, json.loads(args_str)))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from mm_std import print_json
|
|
5
|
+
|
|
6
|
+
from mm_eth.account import generate_accounts, generate_mnemonic
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(mnemonic: str, passphrase: str, limit: int, print_path: bool, path_prefix: str, save_file: str) -> None: # nosec
|
|
10
|
+
result: dict[str, Any] = {}
|
|
11
|
+
if not mnemonic:
|
|
12
|
+
mnemonic = generate_mnemonic()
|
|
13
|
+
result["mnemonic"] = mnemonic
|
|
14
|
+
if passphrase:
|
|
15
|
+
result["passphrase"] = passphrase
|
|
16
|
+
result["accounts"] = []
|
|
17
|
+
for acc in generate_accounts(mnemonic=mnemonic, passphrase=passphrase, limit=limit, path_prefix=path_prefix):
|
|
18
|
+
new_account = {"address": acc.address, "private": acc.private_key}
|
|
19
|
+
if print_path:
|
|
20
|
+
new_account["path"] = acc.path
|
|
21
|
+
result["accounts"].append(new_account)
|
|
22
|
+
print_json(result)
|
|
23
|
+
|
|
24
|
+
if save_file:
|
|
25
|
+
data = [acc["address"] + "\t" + acc["private"] for acc in result["accounts"]]
|
|
26
|
+
Path(save_file).write_text("\n".join(data) + "\n")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from mm_std import Ok, PrintFormat, print_json, print_plain
|
|
2
|
+
from rich.live import Live
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
from mm_eth import rpc
|
|
6
|
+
from mm_eth.utils import from_wei_str, name_network
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(urls: list[str], print_format: PrintFormat, proxy: str | None) -> None:
|
|
10
|
+
json_result: dict[str, object] = {}
|
|
11
|
+
table = Table(title="nodes")
|
|
12
|
+
if print_format == PrintFormat.TABLE:
|
|
13
|
+
table.add_column("url")
|
|
14
|
+
table.add_column("chain_id")
|
|
15
|
+
table.add_column("chain_name")
|
|
16
|
+
table.add_column("block_number")
|
|
17
|
+
table.add_column("base_fee")
|
|
18
|
+
|
|
19
|
+
with Live(table, refresh_per_second=0.5):
|
|
20
|
+
for url in urls:
|
|
21
|
+
chain_id_res = rpc.eth_chain_id(url, timeout=10, proxies=proxy)
|
|
22
|
+
chain_id = chain_id_res.ok_or_err()
|
|
23
|
+
chain_name = ""
|
|
24
|
+
if isinstance(chain_id_res, Ok):
|
|
25
|
+
chain_name = name_network(chain_id_res.ok)
|
|
26
|
+
block_number = rpc.eth_block_number(url, timeout=10, proxies=proxy).ok_or_err()
|
|
27
|
+
base_fee = rpc.get_base_fee_per_gas(url, timeout=10, proxies=proxy).map_or_else(
|
|
28
|
+
lambda err: err,
|
|
29
|
+
lambda ok: from_wei_str(ok, "gwei"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
json_result[url] = {
|
|
33
|
+
"chain_id": chain_id,
|
|
34
|
+
"chain_name": chain_name,
|
|
35
|
+
"block_number": block_number,
|
|
36
|
+
"base_fee": base_fee,
|
|
37
|
+
}
|
|
38
|
+
if print_format == PrintFormat.TABLE:
|
|
39
|
+
table.add_row(url, str(chain_id), chain_name, str(block_number), base_fee)
|
|
40
|
+
print_plain(f"url: {url}", print_format)
|
|
41
|
+
print_plain(f"chain_id: {chain_id}", print_format)
|
|
42
|
+
print_plain(f"chain_name: {chain_name}", print_format)
|
|
43
|
+
print_plain(f"block_number: {block_number}", print_format)
|
|
44
|
+
print_plain(f"base_fee: {base_fee}", print_format)
|
|
45
|
+
print_plain("", print_format)
|
|
46
|
+
|
|
47
|
+
print_json(json_result, print_format)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from mm_std import fatal, hr, print_console, str_starts_with_any
|
|
5
|
+
from rich import print_json
|
|
6
|
+
|
|
7
|
+
from mm_eth.cli.cli_utils import public_rpc_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run(rpc_url: str, method: str, params: str, hex2dec: bool) -> None:
|
|
11
|
+
rpc_url = public_rpc_url(rpc_url)
|
|
12
|
+
if not method:
|
|
13
|
+
return list_all_methods()
|
|
14
|
+
if not str_starts_with_any(rpc_url, ["http://", "https://"]):
|
|
15
|
+
fatal(f"invalid rpc_url: {rpc_url}")
|
|
16
|
+
params = params.replace("'", '"')
|
|
17
|
+
data = {"jsonrpc": "2.0", "method": method, "params": parse_method_params(method, params), "id": 1}
|
|
18
|
+
res = hr(rpc_url, method="POST", params=data, json_params=True)
|
|
19
|
+
if res.json:
|
|
20
|
+
print_json(data=res.json)
|
|
21
|
+
result_value: str = res.json.get("result", "")
|
|
22
|
+
if hex2dec and result_value.startswith(("0x", "0X")):
|
|
23
|
+
print_console("hex2dec", int(result_value, 16))
|
|
24
|
+
else:
|
|
25
|
+
fatal(str(res))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_method_params(method: str, params_str: str) -> list[object]:
|
|
29
|
+
params = json.loads(params_str) if params_str.startswith("[") else params_str.split()
|
|
30
|
+
if method in ["eth_getBalance", "eth_getTransactionCount", "eth_getCode"] and len(params) == 1:
|
|
31
|
+
params.append("latest")
|
|
32
|
+
return cast(list[object], params)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def list_all_methods() -> None:
|
|
36
|
+
all_methods = """
|
|
37
|
+
web3_clientVersion
|
|
38
|
+
web3_sha3
|
|
39
|
+
net_version
|
|
40
|
+
net_listening
|
|
41
|
+
net_peerCount
|
|
42
|
+
eth_protocolVersion
|
|
43
|
+
eth_syncing
|
|
44
|
+
eth_chainId
|
|
45
|
+
eth_gasPrice
|
|
46
|
+
eth_accounts
|
|
47
|
+
eth_blockNumber
|
|
48
|
+
eth_getBalance
|
|
49
|
+
eth_getStorageAt
|
|
50
|
+
eth_getTransactionCount
|
|
51
|
+
eth_getBlockTransactionCountByHash
|
|
52
|
+
eth_getBlockTransactionCountByNumber
|
|
53
|
+
eth_getUncleCountByBlockHash
|
|
54
|
+
eth_getUncleCountByBlockNumber
|
|
55
|
+
eth_getCode
|
|
56
|
+
eth_sign
|
|
57
|
+
eth_signTransaction
|
|
58
|
+
eth_sendTransaction
|
|
59
|
+
eth_sendRawTransaction
|
|
60
|
+
eth_call
|
|
61
|
+
eth_estimateGas
|
|
62
|
+
eth_getBlockByHash
|
|
63
|
+
eth_getBlockByNumber
|
|
64
|
+
eth_getTransactionByHash
|
|
65
|
+
eth_getTransactionByBlockHashAndIndex
|
|
66
|
+
eth_getTransactionByBlockNumberAndIndex
|
|
67
|
+
eth_getTransactionReceipt
|
|
68
|
+
eth_getUncleByBlockHashAndIndex
|
|
69
|
+
eth_getUncleByBlockNumberAndIndex
|
|
70
|
+
eth_newFilter
|
|
71
|
+
eth_newBlockFilter
|
|
72
|
+
eth_newPendingTransactionFilter
|
|
73
|
+
eth_uninstallFilter
|
|
74
|
+
eth_getFilterChanges
|
|
75
|
+
eth_getFilterLogs
|
|
76
|
+
eth_getLogs
|
|
77
|
+
""".strip()
|
|
78
|
+
print_console(all_methods)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Self
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from mm_std import BaseConfig, Err, Ok, print_json, str_to_list, utc_now
|
|
9
|
+
from pydantic import Field, StrictStr, field_validator, model_validator
|
|
10
|
+
|
|
11
|
+
from mm_eth import abi, rpc
|
|
12
|
+
from mm_eth.account import create_private_keys_dict, private_to_address
|
|
13
|
+
from mm_eth.cli import calcs, cli_utils, print_helpers, rpc_helpers, validators
|
|
14
|
+
from mm_eth.tx import sign_tx
|
|
15
|
+
from mm_eth.utils import from_wei_str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Config(BaseConfig):
|
|
19
|
+
contract_address: str
|
|
20
|
+
function_signature: str
|
|
21
|
+
function_args: StrictStr = "[]"
|
|
22
|
+
nodes: list[StrictStr]
|
|
23
|
+
chain_id: int
|
|
24
|
+
private_keys: dict[str, str] = Field(default_factory=dict)
|
|
25
|
+
private_keys_file: str | None = None
|
|
26
|
+
max_fee_per_gas: str
|
|
27
|
+
max_fee_per_gas_limit: str | None = None
|
|
28
|
+
max_priority_fee_per_gas: str
|
|
29
|
+
value: str | None = None
|
|
30
|
+
gas: str
|
|
31
|
+
from_addresses: list[str]
|
|
32
|
+
delay: str | None = None # in seconds
|
|
33
|
+
round_ndigits: int = 5
|
|
34
|
+
log_debug: str | None = None
|
|
35
|
+
log_info: str | None = None
|
|
36
|
+
|
|
37
|
+
@field_validator("log_debug", "log_info", mode="before")
|
|
38
|
+
@classmethod
|
|
39
|
+
def log_validator(cls, v: str | None) -> str | None:
|
|
40
|
+
return validators.log_validator(v)
|
|
41
|
+
|
|
42
|
+
@field_validator("nodes", "from_addresses", mode="before")
|
|
43
|
+
@classmethod
|
|
44
|
+
def list_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
45
|
+
return validators.nodes_validator(v)
|
|
46
|
+
|
|
47
|
+
@field_validator("from_addresses", mode="before")
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_addresses_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
50
|
+
return str_to_list(v, remove_comments=True, lower=True)
|
|
51
|
+
|
|
52
|
+
@field_validator("private_keys", mode="before")
|
|
53
|
+
@classmethod
|
|
54
|
+
def private_keys_validator(cls, v: str | list[str] | None) -> dict[str, str]:
|
|
55
|
+
if v is None:
|
|
56
|
+
return {}
|
|
57
|
+
if isinstance(v, str):
|
|
58
|
+
return create_private_keys_dict(str_to_list(v, unique=True, remove_comments=True))
|
|
59
|
+
return create_private_keys_dict(v)
|
|
60
|
+
|
|
61
|
+
# noinspection DuplicatedCode
|
|
62
|
+
@model_validator(mode="after")
|
|
63
|
+
def final_validator(self) -> Self:
|
|
64
|
+
# load private keys from file
|
|
65
|
+
if self.private_keys_file:
|
|
66
|
+
file = Path(self.private_keys_file).expanduser()
|
|
67
|
+
if not file.is_file():
|
|
68
|
+
raise ValueError("can't read private_keys_file")
|
|
69
|
+
for line in file.read_text().strip().split("\n"):
|
|
70
|
+
line = line.strip() # noqa: PLW2901
|
|
71
|
+
address = private_to_address(line)
|
|
72
|
+
if address is None:
|
|
73
|
+
raise ValueError("there is not a private key in private_keys_file")
|
|
74
|
+
self.private_keys[address.lower()] = line
|
|
75
|
+
|
|
76
|
+
# check that from_addresses is not empty
|
|
77
|
+
if not self.from_addresses:
|
|
78
|
+
raise ValueError("from_addresses is empty")
|
|
79
|
+
|
|
80
|
+
# max_fee_per_gas
|
|
81
|
+
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas, "base"):
|
|
82
|
+
raise ValueError(f"wrong max_fee_per_gas: {self.max_fee_per_gas}")
|
|
83
|
+
|
|
84
|
+
# max_fee_per_gas_limit
|
|
85
|
+
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas_limit, "base"):
|
|
86
|
+
raise ValueError(f"wrong max_fee_per_gas_limit: {self.max_fee_per_gas_limit}")
|
|
87
|
+
|
|
88
|
+
# max_priority_fee_per_gas
|
|
89
|
+
if not validators.is_valid_calc_var_wei_value(self.max_priority_fee_per_gas):
|
|
90
|
+
raise ValueError(f"wrong max_priority_fee_per_gas: {self.max_priority_fee_per_gas}")
|
|
91
|
+
|
|
92
|
+
# value
|
|
93
|
+
if self.value is not None and not validators.is_valid_calc_var_wei_value(self.value, "balance"):
|
|
94
|
+
raise ValueError(f"wrong value: {self.value}")
|
|
95
|
+
|
|
96
|
+
# gas
|
|
97
|
+
if not validators.is_valid_calc_var_wei_value(self.gas, "estimate"):
|
|
98
|
+
raise ValueError(f"wrong gas: {self.gas}")
|
|
99
|
+
|
|
100
|
+
# delay
|
|
101
|
+
if not validators.is_valid_calc_decimal_value(self.delay):
|
|
102
|
+
raise ValueError(f"wrong delay: {self.delay}")
|
|
103
|
+
|
|
104
|
+
# function_args
|
|
105
|
+
if not validators.is_valid_calc_function_args(self.function_args):
|
|
106
|
+
raise ValueError(f"wrong function_args: {self.function_args}")
|
|
107
|
+
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# noinspection DuplicatedCode
|
|
112
|
+
def run(
|
|
113
|
+
config_path: str,
|
|
114
|
+
*,
|
|
115
|
+
print_balances: bool,
|
|
116
|
+
print_config: bool,
|
|
117
|
+
debug: bool,
|
|
118
|
+
no_receipt: bool,
|
|
119
|
+
emulate: bool,
|
|
120
|
+
) -> None:
|
|
121
|
+
config = cli_utils.read_config(Config, Path(config_path))
|
|
122
|
+
if print_config:
|
|
123
|
+
print_json(config.model_dump(exclude={"private_key"}))
|
|
124
|
+
sys.exit(0)
|
|
125
|
+
|
|
126
|
+
cli_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
127
|
+
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
128
|
+
|
|
129
|
+
if print_balances:
|
|
130
|
+
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
_run_transfers(config, no_receipt=no_receipt, emulate=emulate)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# noinspection DuplicatedCode
|
|
137
|
+
def _run_transfers(config: Config, *, no_receipt: bool, emulate: bool) -> None:
|
|
138
|
+
logger.info(f"started at {utc_now()} UTC")
|
|
139
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
140
|
+
cli_utils.check_private_keys(config.from_addresses, config.private_keys)
|
|
141
|
+
for i, from_address in enumerate(config.from_addresses):
|
|
142
|
+
_transfer(from_address=from_address, config=config, no_receipt=no_receipt, emulate=emulate)
|
|
143
|
+
if not emulate and config.delay is not None and i < len(config.from_addresses) - 1:
|
|
144
|
+
delay_value = calcs.calc_decimal_value(config.delay)
|
|
145
|
+
logger.debug(f"delay {delay_value} seconds")
|
|
146
|
+
time.sleep(float(delay_value))
|
|
147
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# noinspection DuplicatedCode
|
|
151
|
+
def _transfer(*, from_address: str, config: Config, no_receipt: bool, emulate: bool) -> None:
|
|
152
|
+
log_prefix = f"{from_address}"
|
|
153
|
+
# get nonce
|
|
154
|
+
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
155
|
+
if nonce is None:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# get max_fee_per_gas
|
|
159
|
+
max_fee_per_gas = rpc_helpers.calc_max_fee_per_gas(config.nodes, config.max_fee_per_gas, log_prefix)
|
|
160
|
+
if max_fee_per_gas is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# check max_fee_per_gas_limit
|
|
164
|
+
if rpc_helpers.is_max_fee_per_gas_limit_exceeded(max_fee_per_gas, config.max_fee_per_gas_limit, log_prefix):
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
max_priority_fee_per_gas = calcs.calc_var_wei_value(config.max_priority_fee_per_gas)
|
|
168
|
+
|
|
169
|
+
# data
|
|
170
|
+
function_args = calcs.calc_function_args(config.function_args).replace("'", '"')
|
|
171
|
+
data = abi.encode_function_input_by_signature(config.function_signature, json.loads(function_args))
|
|
172
|
+
|
|
173
|
+
# get gas
|
|
174
|
+
gas = rpc_helpers.calc_gas(
|
|
175
|
+
nodes=config.nodes,
|
|
176
|
+
gas=config.gas,
|
|
177
|
+
from_address=from_address,
|
|
178
|
+
to_address=config.contract_address,
|
|
179
|
+
value=None,
|
|
180
|
+
data=data,
|
|
181
|
+
log_prefix=log_prefix,
|
|
182
|
+
)
|
|
183
|
+
if gas is None:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# get value
|
|
187
|
+
value = None
|
|
188
|
+
if config.value is not None:
|
|
189
|
+
value = rpc_helpers.calc_eth_value(
|
|
190
|
+
nodes=config.nodes,
|
|
191
|
+
value_str=config.value,
|
|
192
|
+
address=from_address,
|
|
193
|
+
gas=gas,
|
|
194
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
195
|
+
log_prefix=log_prefix,
|
|
196
|
+
)
|
|
197
|
+
if value is None:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
tx_params = {
|
|
201
|
+
"nonce": nonce,
|
|
202
|
+
"max_fee_per_gas": max_fee_per_gas,
|
|
203
|
+
"max_priority_fee_per_gas": max_priority_fee_per_gas,
|
|
204
|
+
"gas": gas,
|
|
205
|
+
"value": value,
|
|
206
|
+
"to": config.contract_address,
|
|
207
|
+
"chain_id": config.chain_id,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# emulate?
|
|
211
|
+
if emulate:
|
|
212
|
+
msg = f"{log_prefix}: emulate,"
|
|
213
|
+
if value is not None:
|
|
214
|
+
msg += f" value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
215
|
+
msg += f" max_fee_per_gas={from_wei_str(max_fee_per_gas, 'gwei', config.round_ndigits)},"
|
|
216
|
+
msg += f" max_priority_fee_per_gas={from_wei_str(max_priority_fee_per_gas, 'gwei', config.round_ndigits)},"
|
|
217
|
+
msg += f" gas={gas}"
|
|
218
|
+
logger.info(msg)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
logger.debug(f"{log_prefix}: tx_params={tx_params}")
|
|
222
|
+
signed_tx = sign_tx(
|
|
223
|
+
nonce=nonce,
|
|
224
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
225
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
226
|
+
gas=gas,
|
|
227
|
+
private_key=config.private_keys[from_address],
|
|
228
|
+
chain_id=config.chain_id,
|
|
229
|
+
value=value,
|
|
230
|
+
data=data,
|
|
231
|
+
to=config.contract_address,
|
|
232
|
+
)
|
|
233
|
+
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
234
|
+
if isinstance(res, Err):
|
|
235
|
+
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
236
|
+
return
|
|
237
|
+
tx_hash = res.ok
|
|
238
|
+
|
|
239
|
+
if no_receipt:
|
|
240
|
+
msg = f"{log_prefix}: tx_hash={tx_hash}"
|
|
241
|
+
logger.info(msg)
|
|
242
|
+
else:
|
|
243
|
+
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
244
|
+
while True:
|
|
245
|
+
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
246
|
+
if isinstance(receipt_res, Ok):
|
|
247
|
+
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
248
|
+
msg = f"{log_prefix}: tx_hash={tx_hash}, status={status}"
|
|
249
|
+
logger.info(msg)
|
|
250
|
+
break
|
|
251
|
+
time.sleep(1)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from mm_std import Err, PrintFormat, fatal, print_json, print_plain
|
|
4
|
+
from mm_std.fs import get_filename_without_extension
|
|
5
|
+
|
|
6
|
+
from mm_eth.solc import solc
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(contract_path: str, tmp_dir: str, print_format: PrintFormat) -> None:
|
|
10
|
+
contract_name = get_filename_without_extension(contract_path)
|
|
11
|
+
res = solc(contract_name, contract_path, tmp_dir)
|
|
12
|
+
if isinstance(res, Err):
|
|
13
|
+
fatal(res.err)
|
|
14
|
+
|
|
15
|
+
bin_ = res.ok.bin
|
|
16
|
+
abi = res.ok.abi
|
|
17
|
+
|
|
18
|
+
if print_format == PrintFormat.JSON:
|
|
19
|
+
print_json({"bin": bin_, "abi": json.loads(abi)})
|
|
20
|
+
else:
|
|
21
|
+
print_plain("bin:")
|
|
22
|
+
print_plain(bin_)
|
|
23
|
+
print_plain("abi:")
|
|
24
|
+
print_plain(abi)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from mm_std import Err, Ok, print_plain
|
|
2
|
+
|
|
3
|
+
from mm_eth import erc20, rpc
|
|
4
|
+
from mm_eth.cli import cli_utils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run(rpc_url: str, token_address: str) -> None:
|
|
8
|
+
rpc_url = cli_utils.public_rpc_url(rpc_url)
|
|
9
|
+
name = erc20.get_name(rpc_url, token_address).ok_or_err()
|
|
10
|
+
symbol = erc20.get_symbol(rpc_url, token_address).ok_or_err()
|
|
11
|
+
decimals = erc20.get_decimals(rpc_url, token_address).ok_or_err()
|
|
12
|
+
transfer_count = _calc_transfer_events(rpc_url, 100, token_address)
|
|
13
|
+
|
|
14
|
+
print_plain(f"name: {name}")
|
|
15
|
+
print_plain(f"symbol: {symbol}")
|
|
16
|
+
print_plain(f"decimals: {decimals}")
|
|
17
|
+
print_plain(f"transfer_count: {transfer_count}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _calc_transfer_events(rpc_url: str, last_block_limit: int, token_address: str) -> int | str:
|
|
21
|
+
current_block_res = rpc.eth_block_number(rpc_url)
|
|
22
|
+
if isinstance(current_block_res, Err):
|
|
23
|
+
return current_block_res.err
|
|
24
|
+
current_block = current_block_res.ok
|
|
25
|
+
|
|
26
|
+
res = erc20.get_transfer_event_logs(rpc_url, token_address, current_block - last_block_limit, current_block)
|
|
27
|
+
if isinstance(res, Ok):
|
|
28
|
+
return len(res.ok)
|
|
29
|
+
return res.err
|