mm-sol 0.2.5__py3-none-any.whl → 0.2.6__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 +4 -2
- mm_sol/balance.py +83 -6
- mm_sol/cli/calcs.py +93 -0
- mm_sol/cli/cli.py +17 -6
- 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 +183 -30
- 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 +11 -0
- mm_sol/converters.py +33 -0
- mm_sol/solana_cli.py +0 -2
- mm_sol/token.py +9 -111
- mm_sol/transfer.py +140 -41
- mm_sol/utils.py +7 -26
- {mm_sol-0.2.5.dist-info → mm_sol-0.2.6.dist-info}/METADATA +4 -4
- mm_sol-0.2.6.dist-info/RECORD +31 -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.6.dist-info}/WHEEL +0 -0
- {mm_sol-0.2.5.dist-info → mm_sol-0.2.6.dist-info}/entry_points.txt +0 -0
mm_sol/account.py
CHANGED
|
@@ -35,8 +35,10 @@ def get_keypair(private_key: str | list[int]) -> Keypair:
|
|
|
35
35
|
return Keypair.from_bytes(private_key_)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def check_private_key(
|
|
39
|
-
|
|
38
|
+
def check_private_key(public_key: str | Pubkey, private_key: str | list[int]) -> bool:
|
|
39
|
+
if isinstance(public_key, str):
|
|
40
|
+
public_key = Pubkey.from_string(public_key)
|
|
41
|
+
return get_keypair(private_key).pubkey() == public_key
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def get_public_key(private_key: str) -> str:
|
mm_sol/balance.py
CHANGED
|
@@ -1,18 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
import httpx
|
|
2
|
+
from mm_crypto_utils import Nodes, Proxies, random_node, random_proxy
|
|
3
|
+
from mm_std import Err, Ok, Result
|
|
4
|
+
from solana.exceptions import SolanaRpcException
|
|
5
|
+
from solana.rpc.types import TokenAccountOpts
|
|
6
|
+
from solders.pubkey import Pubkey
|
|
2
7
|
|
|
3
8
|
from mm_sol import rpc
|
|
4
|
-
from mm_sol.
|
|
5
|
-
from mm_sol.utils import get_node, get_proxy
|
|
9
|
+
from mm_sol.utils import get_client
|
|
6
10
|
|
|
7
11
|
|
|
8
|
-
def
|
|
12
|
+
def get_sol_balance(node: str, address: str, timeout: int = 10, proxy: str | None = None) -> Result[int]:
|
|
9
13
|
return rpc.get_balance(node, address, timeout, proxy)
|
|
10
14
|
|
|
11
15
|
|
|
12
|
-
def
|
|
16
|
+
def get_sol_balance_with_retries(
|
|
17
|
+
nodes: Nodes, address: str, retries: int, timeout: int = 10, proxies: Proxies = None
|
|
18
|
+
) -> Result[int]:
|
|
13
19
|
res: Result[int] = Err("not started yet")
|
|
14
20
|
for _ in range(retries):
|
|
15
|
-
res =
|
|
21
|
+
res = get_sol_balance(random_node(nodes), address, timeout=timeout, proxy=random_proxy(proxies))
|
|
22
|
+
if res.is_ok():
|
|
23
|
+
return res
|
|
24
|
+
return res
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_token_balance(
|
|
28
|
+
node: str,
|
|
29
|
+
owner_address: str,
|
|
30
|
+
token_mint_address: str,
|
|
31
|
+
token_account: str | None = None,
|
|
32
|
+
timeout: float = 10,
|
|
33
|
+
proxy: str | None = None,
|
|
34
|
+
no_token_accounts_return_zero: bool = True,
|
|
35
|
+
) -> Result[int]:
|
|
36
|
+
data: list[object] = []
|
|
37
|
+
try:
|
|
38
|
+
client = get_client(node, proxy=proxy, timeout=timeout)
|
|
39
|
+
if token_account:
|
|
40
|
+
res_balance = client.get_token_account_balance(Pubkey.from_string(token_account))
|
|
41
|
+
data.append(res_balance)
|
|
42
|
+
return Ok(int(res_balance.value.amount))
|
|
43
|
+
|
|
44
|
+
res_accounts = client.get_token_accounts_by_owner(
|
|
45
|
+
Pubkey.from_string(owner_address),
|
|
46
|
+
TokenAccountOpts(mint=Pubkey.from_string(token_mint_address)),
|
|
47
|
+
)
|
|
48
|
+
data.append(res_accounts)
|
|
49
|
+
|
|
50
|
+
if no_token_accounts_return_zero and not res_accounts.value:
|
|
51
|
+
return Ok(0)
|
|
52
|
+
if not res_accounts.value:
|
|
53
|
+
return Err("no_token_accounts")
|
|
54
|
+
|
|
55
|
+
token_accounts = [a.pubkey for a in res_accounts.value]
|
|
56
|
+
balances = []
|
|
57
|
+
for token_account_ in token_accounts:
|
|
58
|
+
res = client.get_token_account_balance(token_account_)
|
|
59
|
+
data.append(res)
|
|
60
|
+
if res.value: # type:ignore[truthy-bool]
|
|
61
|
+
balances.append(int(res.value.amount))
|
|
62
|
+
|
|
63
|
+
return Ok(sum(balances))
|
|
64
|
+
except httpx.HTTPStatusError as e:
|
|
65
|
+
return Err(f"http error: {e}", data=data)
|
|
66
|
+
except SolanaRpcException as e:
|
|
67
|
+
return Err(e.error_msg, data=data)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
return Err(e, data=data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_token_balance_with_retries(
|
|
73
|
+
nodes: Nodes,
|
|
74
|
+
owner_address: str,
|
|
75
|
+
token_mint_address: str,
|
|
76
|
+
retries: int,
|
|
77
|
+
token_account: str | None = None,
|
|
78
|
+
timeout: float = 10,
|
|
79
|
+
proxies: Proxies = None,
|
|
80
|
+
no_token_accounts_return_zero: bool = True,
|
|
81
|
+
) -> Result[int]:
|
|
82
|
+
res: Result[int] = Err("not started yet")
|
|
83
|
+
for _ in range(retries):
|
|
84
|
+
res = get_token_balance(
|
|
85
|
+
random_node(nodes),
|
|
86
|
+
owner_address,
|
|
87
|
+
token_mint_address,
|
|
88
|
+
token_account,
|
|
89
|
+
timeout=timeout,
|
|
90
|
+
proxy=random_proxy(proxies),
|
|
91
|
+
no_token_accounts_return_zero=no_token_accounts_return_zero,
|
|
92
|
+
)
|
|
16
93
|
if res.is_ok():
|
|
17
94
|
return res
|
|
18
95
|
return res
|
mm_sol/cli/calcs.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
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 Err
|
|
8
|
+
from mm_std.str import split_on_plus_minus_tokens
|
|
9
|
+
|
|
10
|
+
from mm_sol.balance import get_sol_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(
|
|
73
|
+
*,
|
|
74
|
+
nodes: Nodes,
|
|
75
|
+
value_str: str,
|
|
76
|
+
address: str,
|
|
77
|
+
proxies: Proxies,
|
|
78
|
+
fee: int = 5000,
|
|
79
|
+
log_prefix: str | None = None,
|
|
80
|
+
) -> int | None:
|
|
81
|
+
balance_value = None
|
|
82
|
+
if "balance" in value_str.lower():
|
|
83
|
+
prefix = mm_crypto_utils.get_log_prefix(log_prefix)
|
|
84
|
+
res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
|
|
85
|
+
logger.debug(f"{prefix}balance={res.ok_or_err()}")
|
|
86
|
+
if isinstance(res, Err):
|
|
87
|
+
logger.info(f"{prefix}balance error, {res.err}")
|
|
88
|
+
return None
|
|
89
|
+
balance_value = res.ok
|
|
90
|
+
value = calc_var_value(value_str, var_name="balance", var_value=balance_value)
|
|
91
|
+
if "balance" in value_str.lower():
|
|
92
|
+
value = value - fee
|
|
93
|
+
return value
|
mm_sol/cli/cli.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Annotated
|
|
|
4
4
|
import typer
|
|
5
5
|
from mm_std import print_plain
|
|
6
6
|
|
|
7
|
+
from . import cli_utils
|
|
7
8
|
from .cmd import balance_cmd, balances_cmd, example_cmd, node_cmd, transfer_sol_cmd
|
|
8
9
|
from .cmd.wallet import keypair_cmd, new_cmd
|
|
9
10
|
|
|
@@ -16,9 +17,7 @@ app.add_typer(wallet_app, name="w", hidden=True)
|
|
|
16
17
|
|
|
17
18
|
def version_callback(value: bool) -> None:
|
|
18
19
|
if value:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
print_plain(f"mm-sol: {importlib.metadata.version('mm-sol')}")
|
|
20
|
+
print_plain(f"mm-sol: {cli_utils.get_version()}")
|
|
22
21
|
raise typer.Exit
|
|
23
22
|
|
|
24
23
|
|
|
@@ -55,11 +54,23 @@ def balances_command(
|
|
|
55
54
|
balances_cmd.run(config_path, print_config)
|
|
56
55
|
|
|
57
56
|
|
|
58
|
-
@app.command(name="transfer-sol", help="Transfer SOL")
|
|
57
|
+
@app.command(name="transfer-sol", help="Transfer SOL from one or many accounts")
|
|
59
58
|
def transfer_sol_command(
|
|
60
|
-
config_path: str,
|
|
59
|
+
config_path: str,
|
|
60
|
+
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
61
|
+
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
62
|
+
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
63
|
+
no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
|
|
64
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
61
65
|
) -> None:
|
|
62
|
-
transfer_sol_cmd.run(
|
|
66
|
+
transfer_sol_cmd.run(
|
|
67
|
+
config_path,
|
|
68
|
+
print_balances=print_balances,
|
|
69
|
+
print_config=print_config,
|
|
70
|
+
debug=debug,
|
|
71
|
+
no_confirmation=no_confirmation,
|
|
72
|
+
emulate=emulate,
|
|
73
|
+
)
|
|
63
74
|
|
|
64
75
|
|
|
65
76
|
@app.command(name="node", help="Check RPC urls")
|
mm_sol/cli/cli_utils.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import importlib.metadata
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from
|
|
3
|
+
from mm_crypto_utils import Proxies
|
|
4
|
+
from rich.live import Live
|
|
5
|
+
from rich.table import Table
|
|
5
6
|
|
|
7
|
+
from mm_sol.balance import get_sol_balance_with_retries
|
|
8
|
+
from mm_sol.converters import lamports_to_sol
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
sys.exit(0)
|
|
10
|
+
|
|
11
|
+
def get_version() -> str:
|
|
12
|
+
return importlib.metadata.version("mm-sol")
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def public_rpc_url(url: str | None) -> str:
|
|
@@ -25,12 +27,22 @@ def public_rpc_url(url: str | None) -> str:
|
|
|
25
27
|
return url
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
def print_balances(
|
|
31
|
+
rpc_nodes: list[str],
|
|
32
|
+
addresses: list[str],
|
|
33
|
+
*,
|
|
34
|
+
proxies: Proxies = None,
|
|
35
|
+
round_ndigits: int = 5,
|
|
36
|
+
) -> None:
|
|
37
|
+
table = Table(title="balances")
|
|
38
|
+
table.add_column("n")
|
|
39
|
+
table.add_column("address")
|
|
40
|
+
table.add_column("balance, sol")
|
|
41
|
+
with Live(table, refresh_per_second=0.5):
|
|
42
|
+
for count, address in enumerate(addresses):
|
|
43
|
+
balance = get_sol_balance_with_retries(rpc_nodes, address, proxies=proxies, retries=5).map_or_else(
|
|
44
|
+
lambda err: err,
|
|
45
|
+
lambda ok: str(lamports_to_sol(ok, round_ndigits)),
|
|
46
|
+
)
|
|
47
|
+
row: list[str] = [str(count), address, balance]
|
|
48
|
+
table.add_row(*row)
|
mm_sol/cli/cmd/balance_cmd.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
+
import mm_crypto_utils
|
|
3
4
|
from mm_std import Ok, print_json
|
|
4
5
|
from pydantic import BaseModel, Field
|
|
5
6
|
|
|
7
|
+
import mm_sol.balance
|
|
6
8
|
from mm_sol import balance, token
|
|
7
9
|
from mm_sol.cli import cli_utils
|
|
8
10
|
|
|
@@ -40,10 +42,10 @@ def run(
|
|
|
40
42
|
result = BalanceResult()
|
|
41
43
|
|
|
42
44
|
rpc_url = cli_utils.public_rpc_url(rpc_url)
|
|
43
|
-
proxies =
|
|
45
|
+
proxies = mm_crypto_utils.fetch_proxies_or_fatal(proxies_url) if proxies_url else None
|
|
44
46
|
|
|
45
47
|
# sol balance
|
|
46
|
-
sol_balance_res = balance.
|
|
48
|
+
sol_balance_res = balance.get_sol_balance_with_retries(rpc_url, wallet_address, retries=3, proxies=proxies)
|
|
47
49
|
if isinstance(sol_balance_res, Ok):
|
|
48
50
|
result.sol_balance = sol_balance_res.ok
|
|
49
51
|
else:
|
|
@@ -51,7 +53,7 @@ def run(
|
|
|
51
53
|
|
|
52
54
|
# token balance
|
|
53
55
|
if token_address:
|
|
54
|
-
token_balance_res =
|
|
56
|
+
token_balance_res = mm_sol.balance.get_token_balance_with_retries(
|
|
55
57
|
nodes=rpc_url,
|
|
56
58
|
owner_address=wallet_address,
|
|
57
59
|
token_mint_address=token_address,
|
mm_sol/cli/cmd/balances_cmd.py
CHANGED
|
@@ -1,62 +1,72 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import random
|
|
2
3
|
from decimal import Decimal
|
|
3
|
-
from typing import Any
|
|
4
|
+
from typing import Annotated, Any, Self
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
from
|
|
6
|
+
import mm_crypto_utils
|
|
7
|
+
from mm_crypto_utils import ConfigValidators
|
|
8
|
+
from mm_std import BaseConfig, print_json
|
|
9
|
+
from pydantic import BeforeValidator, Field, model_validator
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
from mm_sol
|
|
10
|
-
from mm_sol.
|
|
11
|
+
import mm_sol.converters
|
|
12
|
+
from mm_sol import balance
|
|
13
|
+
from mm_sol.account import is_valid_pubkey
|
|
14
|
+
from mm_sol.balance import get_token_balance_with_retries
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class Config(BaseConfig):
|
|
14
|
-
accounts: list[
|
|
15
|
-
nodes: list[
|
|
16
|
-
tokens: list[
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def to_list_validator(cls, v: list[str] | str | None) -> list[str]:
|
|
20
|
-
return str_to_list(v)
|
|
18
|
+
accounts: Annotated[list[str], BeforeValidator(ConfigValidators.addresses(unique=True, is_address=is_valid_pubkey))]
|
|
19
|
+
nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
|
|
20
|
+
tokens: Annotated[list[str], BeforeValidator(ConfigValidators.addresses(unique=True, is_address=is_valid_pubkey))]
|
|
21
|
+
proxies_url: str | None = None
|
|
22
|
+
proxies: list[str] = Field(default_factory=list)
|
|
21
23
|
|
|
22
24
|
@property
|
|
23
25
|
def random_node(self) -> str:
|
|
24
26
|
return random.choice(self.nodes)
|
|
25
27
|
|
|
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
|
+
|
|
26
37
|
|
|
27
38
|
def run(config_path: str, print_config: bool) -> None:
|
|
28
39
|
config = Config.read_config_or_exit(config_path)
|
|
29
|
-
|
|
40
|
+
if print_config:
|
|
41
|
+
config.print_and_exit()
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
# print_config_and_exit(ctx, config)
|
|
33
|
-
result: dict[str, Any] = {"sol": _get_sol_balances(config.accounts, config.nodes)}
|
|
43
|
+
result: dict[str, Any] = {"sol": _get_sol_balances(config.accounts, config)}
|
|
34
44
|
result["sol_sum"] = sum([v for v in result["sol"].values() if v is not None])
|
|
35
45
|
|
|
36
46
|
if config.tokens:
|
|
37
47
|
for token in config.tokens:
|
|
38
|
-
result[token] = _get_token_balances(token, config.accounts, config
|
|
48
|
+
result[token] = _get_token_balances(token, config.accounts, config)
|
|
39
49
|
result[token + "_sum"] = sum([v for v in result[token].values() if v is not None])
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
print_json(result)
|
|
42
52
|
|
|
43
53
|
|
|
44
|
-
def _get_token_balances(token: str, accounts: list[str],
|
|
54
|
+
def _get_token_balances(token: str, accounts: list[str], config: Config) -> dict[str, int | None]:
|
|
45
55
|
result = {}
|
|
46
56
|
for account in accounts:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
nodes=nodes,
|
|
57
|
+
result[account] = get_token_balance_with_retries(
|
|
58
|
+
nodes=config.nodes,
|
|
50
59
|
owner_address=account,
|
|
51
60
|
token_mint_address=token,
|
|
52
61
|
retries=3,
|
|
62
|
+
proxies=config.proxies,
|
|
53
63
|
).ok_or_none()
|
|
54
64
|
return result
|
|
55
65
|
|
|
56
66
|
|
|
57
|
-
def _get_sol_balances(accounts: list[str],
|
|
67
|
+
def _get_sol_balances(accounts: list[str], config: Config) -> dict[str, Decimal | None]:
|
|
58
68
|
result = {}
|
|
59
69
|
for account in accounts:
|
|
60
|
-
res = balance.
|
|
61
|
-
result[account] =
|
|
70
|
+
res = balance.get_sol_balance_with_retries(nodes=config.nodes, address=account, retries=3, proxies=config.proxies)
|
|
71
|
+
result[account] = mm_sol.converters.lamports_to_sol(res.unwrap(), ndigits=2) if res.is_ok() else None
|
|
62
72
|
return result
|
mm_sol/cli/cmd/node_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 import rpc
|
|
4
4
|
|
|
@@ -6,4 +6,4 @@ from mm_sol import rpc
|
|
|
6
6
|
def run(urls: list[str], proxy: str | None) -> None:
|
|
7
7
|
for url in urls:
|
|
8
8
|
res = rpc.get_block_height(url, proxy=proxy, timeout=10)
|
|
9
|
-
|
|
9
|
+
print_json({url: res.ok_or_err()})
|
|
@@ -1,41 +1,194 @@
|
|
|
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, ConfigValidators, 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_valid_pubkey
|
|
16
|
+
from mm_sol.cli import calcs, cli_utils, validators
|
|
17
|
+
from mm_sol.converters import lamports_to_sol
|
|
18
|
+
from mm_sol.utils import get_client
|
|
9
19
|
|
|
10
20
|
|
|
11
21
|
class Config(BaseConfig):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
|
|
23
|
+
routes: Annotated[list[TxRoute], BeforeValidator(ConfigValidators.routes(is_valid_pubkey))]
|
|
24
|
+
routes_from_file: Path | None = None
|
|
25
|
+
routes_to_file: Path | None = None
|
|
26
|
+
private_keys: Annotated[
|
|
27
|
+
AddressToPrivate, Field(default_factory=AddressToPrivate), BeforeValidator(ConfigValidators.private_keys(get_public_key))
|
|
28
|
+
]
|
|
29
|
+
private_keys_file: Path | None = None
|
|
30
|
+
proxies_url: str | None = None
|
|
31
|
+
proxies: list[str] = Field(default_factory=list)
|
|
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(ConfigValidators.log_file())] = None
|
|
37
|
+
log_info: Annotated[Path | None, BeforeValidator(ConfigValidators.log_file())] = None
|
|
21
38
|
|
|
22
39
|
@property
|
|
23
|
-
def
|
|
24
|
-
return
|
|
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_valid_pubkey)
|
|
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
|
+
# value
|
|
65
|
+
if not validators.is_valid_var_lamports(self.value, "balance"):
|
|
66
|
+
raise ValueError(f"wrong value: {self.value}")
|
|
67
|
+
|
|
68
|
+
# value_min_limit
|
|
69
|
+
if not validators.is_valid_var_lamports(self.value_min_limit):
|
|
70
|
+
raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
|
|
25
71
|
|
|
72
|
+
# delay
|
|
73
|
+
if not validators.is_valid_var_lamports(self.delay):
|
|
74
|
+
raise ValueError(f"wrong delay: {self.delay}")
|
|
26
75
|
|
|
27
|
-
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run(
|
|
80
|
+
config_path: str,
|
|
81
|
+
*,
|
|
82
|
+
print_balances: bool,
|
|
83
|
+
print_config: bool,
|
|
84
|
+
debug: bool,
|
|
85
|
+
no_confirmation: bool,
|
|
86
|
+
emulate: bool,
|
|
87
|
+
) -> None:
|
|
28
88
|
config = Config.read_config_or_exit(config_path)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
89
|
+
|
|
90
|
+
if print_config:
|
|
91
|
+
config.print_and_exit({"private_keys", "proxies"})
|
|
92
|
+
|
|
93
|
+
mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
94
|
+
|
|
95
|
+
if print_balances:
|
|
96
|
+
cli_utils.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits, proxies=config.proxies)
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
|
|
99
|
+
_run_transfers(config, no_confirmation=no_confirmation, emulate=emulate)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _run_transfers(config: Config, *, no_confirmation: bool, emulate: bool) -> None:
|
|
103
|
+
logger.info(f"started at {utc_now()} UTC")
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"config={config.model_dump(exclude={'private_keys', 'addresses_map', 'proxies'}) | {'version': cli_utils.get_version()}}"
|
|
106
|
+
)
|
|
107
|
+
for i, route in enumerate(config.routes):
|
|
108
|
+
_transfer(
|
|
109
|
+
from_address=route.from_address,
|
|
110
|
+
to_address=route.to_address,
|
|
111
|
+
config=config,
|
|
112
|
+
no_confirmation=no_confirmation,
|
|
113
|
+
emulate=emulate,
|
|
39
114
|
)
|
|
40
|
-
|
|
41
|
-
|
|
115
|
+
if not emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
116
|
+
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
117
|
+
logger.debug(f"delay {delay_value} seconds")
|
|
118
|
+
time.sleep(float(delay_value))
|
|
119
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _transfer(*, from_address: str, to_address: str, config: Config, no_confirmation: bool, emulate: bool) -> None:
|
|
123
|
+
log_prefix = f"{from_address}->{to_address}"
|
|
124
|
+
fee = 5000
|
|
125
|
+
# get value
|
|
126
|
+
value = calcs.calc_sol_value(
|
|
127
|
+
nodes=config.nodes, value_str=config.value, address=from_address, proxies=config.proxies, log_prefix=log_prefix, fee=fee
|
|
128
|
+
)
|
|
129
|
+
if value is None:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# value_min_limit
|
|
133
|
+
if calcs.is_sol_value_less_min_limit(config.value_min_limit, value, log_prefix=log_prefix):
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
tx_params = {
|
|
137
|
+
"fee": fee,
|
|
138
|
+
"value": value,
|
|
139
|
+
"to": to_address,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# emulate?
|
|
143
|
+
if emulate:
|
|
144
|
+
msg = f"{log_prefix}: emulate, value={lamports_to_sol(value, config.round_ndigits)}SOL,"
|
|
145
|
+
msg += f" fee={fee}"
|
|
146
|
+
logger.info(msg)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
logger.debug(f"{log_prefix}: tx_params={tx_params}")
|
|
150
|
+
|
|
151
|
+
res = transfer.transfer_sol_with_retries(
|
|
152
|
+
nodes=config.nodes,
|
|
153
|
+
from_address=from_address,
|
|
154
|
+
private_key=config.private_keys[from_address],
|
|
155
|
+
to_address=to_address,
|
|
156
|
+
lamports=value,
|
|
157
|
+
proxies=config.proxies,
|
|
158
|
+
retries=3,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if isinstance(res, Err):
|
|
162
|
+
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
163
|
+
return
|
|
164
|
+
signature = res.ok
|
|
165
|
+
|
|
166
|
+
if no_confirmation:
|
|
167
|
+
msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}"
|
|
168
|
+
logger.info(msg)
|
|
169
|
+
else:
|
|
170
|
+
logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
|
|
171
|
+
status = "UNKNOWN"
|
|
172
|
+
if _wait_confirmation(config, signature, log_prefix):
|
|
173
|
+
status = "OK"
|
|
174
|
+
msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
|
|
175
|
+
logger.info(msg)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _wait_confirmation(config: Config, signature: Signature, log_prefix: str) -> bool:
|
|
179
|
+
count = 0
|
|
180
|
+
while True:
|
|
181
|
+
try:
|
|
182
|
+
node = mm_crypto_utils.random_node(config.nodes)
|
|
183
|
+
proxy = mm_crypto_utils.random_proxy(config.proxies)
|
|
184
|
+
client = get_client(node, proxy=proxy)
|
|
185
|
+
res = client.get_transaction(signature)
|
|
186
|
+
if res.value and res.value.slot: # check for tx error
|
|
187
|
+
return True
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"{log_prefix}: can't get confirmation, error={e}")
|
|
190
|
+
time.sleep(1)
|
|
191
|
+
count += 1
|
|
192
|
+
if count > 30:
|
|
193
|
+
logger.error(f"{log_prefix}: can't get confirmation, timeout")
|
|
194
|
+
return False
|
|
@@ -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,11 @@
|
|
|
1
|
+
from mm_sol.cli import calcs
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_valid_var_lamports(value: str | None, base_name: str = "var", decimals: int | None = None) -> bool:
|
|
5
|
+
if value is None:
|
|
6
|
+
return True # check for None on BaseModel.field type level
|
|
7
|
+
try:
|
|
8
|
+
calcs.calc_var_value(value, var_value=123, var_name=base_name, decimals=decimals)
|
|
9
|
+
return True # noqa: TRY300
|
|
10
|
+
except ValueError:
|
|
11
|
+
return False
|
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,77 +1,9 @@
|
|
|
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]:
|
|
@@ -88,46 +20,12 @@ def get_decimals_with_retries(
|
|
|
88
20
|
) -> Result[int]:
|
|
89
21
|
res: Result[int] = Err("not started yet")
|
|
90
22
|
for _ in range(retries):
|
|
91
|
-
res = get_decimals(
|
|
23
|
+
res = get_decimals(
|
|
24
|
+
node=mm_crypto_utils.random_node(nodes),
|
|
25
|
+
token_mint_address=token_mint_address,
|
|
26
|
+
timeout=timeout,
|
|
27
|
+
proxy=mm_crypto_utils.random_proxy(proxies),
|
|
28
|
+
)
|
|
92
29
|
if res.is_ok():
|
|
93
30
|
return res
|
|
94
31
|
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))
|
mm_sol/transfer.py
CHANGED
|
@@ -1,66 +1,165 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
+
import mm_crypto_utils
|
|
3
4
|
import pydash
|
|
5
|
+
from mm_crypto_utils import Nodes, Proxies
|
|
4
6
|
from mm_std import Err, Ok, Result
|
|
5
7
|
from pydantic import BaseModel
|
|
6
|
-
from solana.rpc.api import Client
|
|
7
8
|
from solders.message import Message
|
|
8
9
|
from solders.pubkey import Pubkey
|
|
10
|
+
from solders.signature import Signature
|
|
9
11
|
from solders.system_program import TransferParams, transfer
|
|
10
12
|
from solders.transaction import Transaction
|
|
13
|
+
from spl.token.client import Token
|
|
14
|
+
from spl.token.constants import TOKEN_PROGRAM_ID
|
|
15
|
+
from spl.token.instructions import get_associated_token_address
|
|
11
16
|
|
|
17
|
+
import mm_sol.converters
|
|
12
18
|
from mm_sol import rpc, utils
|
|
13
19
|
from mm_sol.account import check_private_key, get_keypair
|
|
14
20
|
|
|
15
21
|
|
|
22
|
+
def transfer_token(
|
|
23
|
+
*,
|
|
24
|
+
node: str,
|
|
25
|
+
token_mint_address: str | Pubkey,
|
|
26
|
+
from_address: str | Pubkey,
|
|
27
|
+
private_key: str,
|
|
28
|
+
to_address: str | Pubkey,
|
|
29
|
+
amount: Decimal,
|
|
30
|
+
decimals: int,
|
|
31
|
+
proxy: str | None = None,
|
|
32
|
+
timeout: float = 10,
|
|
33
|
+
create_token_account_if_not_exists: bool = True,
|
|
34
|
+
) -> Result[Signature]:
|
|
35
|
+
acc = get_keypair(private_key)
|
|
36
|
+
if not check_private_key(from_address, private_key):
|
|
37
|
+
return Err("invalid_private_key")
|
|
38
|
+
|
|
39
|
+
from_address = utils.pubkey(from_address)
|
|
40
|
+
token_mint_address = utils.pubkey(token_mint_address)
|
|
41
|
+
to_address = utils.pubkey(to_address)
|
|
42
|
+
|
|
43
|
+
client = utils.get_client(node, proxy=proxy, timeout=timeout)
|
|
44
|
+
token_client = Token(conn=client, pubkey=token_mint_address, program_id=TOKEN_PROGRAM_ID, payer=acc)
|
|
45
|
+
|
|
46
|
+
recipient_token_account = get_associated_token_address(to_address, token_mint_address, token_program_id=TOKEN_PROGRAM_ID)
|
|
47
|
+
from_token_account = get_associated_token_address(from_address, token_mint_address, token_program_id=TOKEN_PROGRAM_ID)
|
|
48
|
+
|
|
49
|
+
data: list[object] = []
|
|
50
|
+
|
|
51
|
+
account_info_res = client.get_account_info(recipient_token_account)
|
|
52
|
+
if account_info_res.value is None:
|
|
53
|
+
if create_token_account_if_not_exists:
|
|
54
|
+
create_account_res = token_client.create_account(to_address, skip_confirmation=False)
|
|
55
|
+
data.append(create_account_res)
|
|
56
|
+
else:
|
|
57
|
+
return Err("no_token_account")
|
|
58
|
+
|
|
59
|
+
res = token_client.transfer_checked(
|
|
60
|
+
source=from_token_account,
|
|
61
|
+
dest=recipient_token_account,
|
|
62
|
+
owner=from_address,
|
|
63
|
+
amount=mm_sol.converters.sol_to_lamports(amount),
|
|
64
|
+
decimals=decimals,
|
|
65
|
+
)
|
|
66
|
+
data.append(res)
|
|
67
|
+
|
|
68
|
+
return Ok(res.value, data=data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def transfer_token_with_retries(
|
|
72
|
+
*,
|
|
73
|
+
nodes: Nodes,
|
|
74
|
+
token_mint_address: str | Pubkey,
|
|
75
|
+
from_address: str | Pubkey,
|
|
76
|
+
private_key: str,
|
|
77
|
+
to_address: str | Pubkey,
|
|
78
|
+
amount: Decimal,
|
|
79
|
+
decimals: int,
|
|
80
|
+
proxies: Proxies = None,
|
|
81
|
+
timeout: float = 10,
|
|
82
|
+
retries: int = 3,
|
|
83
|
+
) -> Result[Signature]:
|
|
84
|
+
res: Result[Signature] = Err("not started yet")
|
|
85
|
+
for _ in range(retries):
|
|
86
|
+
res = transfer_token(
|
|
87
|
+
node=mm_crypto_utils.random_node(nodes),
|
|
88
|
+
token_mint_address=token_mint_address,
|
|
89
|
+
from_address=from_address,
|
|
90
|
+
private_key=private_key,
|
|
91
|
+
to_address=to_address,
|
|
92
|
+
amount=amount,
|
|
93
|
+
decimals=decimals,
|
|
94
|
+
proxy=mm_crypto_utils.random_proxy(proxies),
|
|
95
|
+
timeout=timeout,
|
|
96
|
+
)
|
|
97
|
+
if res.is_ok():
|
|
98
|
+
return res
|
|
99
|
+
return res
|
|
100
|
+
|
|
101
|
+
|
|
16
102
|
def transfer_sol(
|
|
17
103
|
*,
|
|
104
|
+
node: str,
|
|
18
105
|
from_address: str,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
) -> Result[
|
|
25
|
-
acc = get_keypair(
|
|
26
|
-
if not check_private_key(from_address,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
error = None
|
|
106
|
+
private_key: str,
|
|
107
|
+
to_address: str,
|
|
108
|
+
lamports: int,
|
|
109
|
+
proxy: str | None = None,
|
|
110
|
+
timeout: float = 10,
|
|
111
|
+
) -> Result[Signature]:
|
|
112
|
+
acc = get_keypair(private_key)
|
|
113
|
+
if not check_private_key(from_address, private_key):
|
|
114
|
+
return Err("invalid_private_key")
|
|
115
|
+
|
|
116
|
+
client = utils.get_client(node, proxy=proxy, timeout=timeout)
|
|
31
117
|
data = None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
118
|
+
try:
|
|
119
|
+
ixs = [transfer(TransferParams(from_pubkey=acc.pubkey(), to_pubkey=Pubkey.from_string(to_address), lamports=lamports))]
|
|
120
|
+
msg = Message(ixs, acc.pubkey())
|
|
121
|
+
tx = Transaction([acc], msg, client.get_latest_blockhash().value.blockhash)
|
|
122
|
+
res = client.send_transaction(tx)
|
|
123
|
+
data = res.to_json()
|
|
124
|
+
return Ok(res.value, data=data)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return Err(e, data=data)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def transfer_sol_with_retries(
|
|
130
|
+
*,
|
|
131
|
+
nodes: Nodes,
|
|
132
|
+
from_address: str,
|
|
133
|
+
private_key: str,
|
|
134
|
+
to_address: str,
|
|
135
|
+
lamports: int,
|
|
136
|
+
proxies: Proxies = None,
|
|
137
|
+
timeout: float = 10,
|
|
138
|
+
retries: int = 3,
|
|
139
|
+
) -> Result[Signature]:
|
|
140
|
+
res: Result[Signature] = Err("not started yet")
|
|
141
|
+
for _ in range(retries):
|
|
142
|
+
res = transfer_sol(
|
|
143
|
+
node=mm_crypto_utils.random_node(nodes),
|
|
144
|
+
from_address=from_address,
|
|
145
|
+
private_key=private_key,
|
|
146
|
+
to_address=to_address,
|
|
147
|
+
lamports=lamports,
|
|
148
|
+
proxy=mm_crypto_utils.random_proxy(proxies),
|
|
149
|
+
timeout=timeout,
|
|
150
|
+
)
|
|
151
|
+
if res.is_ok():
|
|
152
|
+
return res
|
|
153
|
+
return res
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class SolTransferInfo(BaseModel):
|
|
58
157
|
source: str
|
|
59
158
|
destination: str
|
|
60
159
|
lamports: int
|
|
61
160
|
|
|
62
161
|
|
|
63
|
-
def
|
|
162
|
+
def find_sol_transfers(node: str, tx_signature: str) -> Result[list[SolTransferInfo]]:
|
|
64
163
|
res = rpc.get_transaction(node, tx_signature, encoding="jsonParsed")
|
|
65
164
|
if res.is_err():
|
|
66
165
|
return res # type: ignore[return-value]
|
|
@@ -74,7 +173,7 @@ def find_transfers(node: str, tx_signature: str) -> Result[list[TransferInfo]]:
|
|
|
74
173
|
destination = pydash.get(ix, "parsed.info.destination")
|
|
75
174
|
lamports = pydash.get(ix, "parsed.info.lamports")
|
|
76
175
|
if source and destination and lamports:
|
|
77
|
-
result.append(
|
|
176
|
+
result.append(SolTransferInfo(source=source, destination=destination, lamports=lamports))
|
|
78
177
|
return Ok(result, data=res.data)
|
|
79
178
|
except Exception as e:
|
|
80
179
|
return Err(e, res.data)
|
mm_sol/utils.py
CHANGED
|
@@ -1,31 +1,6 @@
|
|
|
1
|
-
import random
|
|
2
|
-
from decimal import Decimal
|
|
3
|
-
|
|
4
1
|
from solana.rpc.api import Client
|
|
5
2
|
from solana.rpc.commitment import Commitment
|
|
6
|
-
|
|
7
|
-
from mm_sol.rpc import DEFAULT_MAINNET_RPC
|
|
8
|
-
from mm_sol.types import Nodes, Proxies
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
|
|
12
|
-
return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_node(nodes: Nodes | None = None) -> str:
|
|
16
|
-
if nodes is None:
|
|
17
|
-
return DEFAULT_MAINNET_RPC
|
|
18
|
-
if isinstance(nodes, str):
|
|
19
|
-
return nodes
|
|
20
|
-
return random.choice(nodes)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def get_proxy(proxies: Proxies) -> str | None:
|
|
24
|
-
if not proxies:
|
|
25
|
-
return None
|
|
26
|
-
if isinstance(proxies, str):
|
|
27
|
-
return proxies
|
|
28
|
-
return random.choice(proxies)
|
|
3
|
+
from solders.pubkey import Pubkey
|
|
29
4
|
|
|
30
5
|
|
|
31
6
|
def get_client(
|
|
@@ -36,3 +11,9 @@ def get_client(
|
|
|
36
11
|
timeout: float = 10,
|
|
37
12
|
) -> Client:
|
|
38
13
|
return Client(endpoint, commitment=commitment, extra_headers=extra_headers, timeout=timeout, proxy=proxy)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pubkey(value: str | Pubkey) -> Pubkey:
|
|
17
|
+
if isinstance(value, Pubkey):
|
|
18
|
+
return value
|
|
19
|
+
return Pubkey.from_string(value)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-sol
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Requires-Python: >=3.
|
|
3
|
+
Version: 0.2.6
|
|
4
|
+
Requires-Python: >=3.13
|
|
5
5
|
Requires-Dist: base58~=2.1.1
|
|
6
6
|
Requires-Dist: jinja2>=3.1.5
|
|
7
|
-
Requires-Dist: mm-
|
|
8
|
-
Requires-Dist: solana~=0.36.
|
|
7
|
+
Requires-Dist: mm-crypto-utils>=0.0.13
|
|
8
|
+
Requires-Dist: solana~=0.36.3
|
|
9
9
|
Requires-Dist: typer>=0.15.1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
mm_sol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mm_sol/account.py,sha256=a0pt1tVdY1O2a4qmgfUd5nP-ftqv3ey0T-2ru5qtUWE,3222
|
|
3
|
+
mm_sol/balance.py,sha256=Idx7h9yhRLbrIEDFCBI5QSHE7OT2pYWEZrbHj9XFrkM,3147
|
|
4
|
+
mm_sol/block.py,sha256=4Lc4TANgpGvPflVumC9MR-3vIl1dedGyci3cgzczuds,1794
|
|
5
|
+
mm_sol/converters.py,sha256=jY3wZeo1326z8M-AMS4OEzspRaGt4Tbk0y2SPI-oDFE,1153
|
|
6
|
+
mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
mm_sol/rpc.py,sha256=TspD_KZQp_KJDQzRknxaT8DR03okok26UWDQWF7Zflg,8031
|
|
8
|
+
mm_sol/solana_cli.py,sha256=ig3OoTvmkrl7MFQSZjRHIraLSmtse0_9kn5Nsw8_zb0,8258
|
|
9
|
+
mm_sol/token.py,sha256=RpXHeM3KBH7q9SUyY_U2dRAgLPC7YgumOTAqxxf_JtI,1046
|
|
10
|
+
mm_sol/transfer.py,sha256=-76gSHbQGOZeOgVgo10QWjAcv_9mnGrBNvpRCvXBlZQ,5872
|
|
11
|
+
mm_sol/utils.py,sha256=NE0G564GiT9d7rW_lPPxUb1eq62WiXh28xtvtzNQIqw,556
|
|
12
|
+
mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
mm_sol/cli/calcs.py,sha256=1IQF1j6c9mRBH7UAnX-MwrCC4PJ3Vp1pg9haa2kG22I,3698
|
|
14
|
+
mm_sol/cli/cli.py,sha256=9rWEDAd1iAwJYnC7voTo4ltqMtRd9IuHoGTmqwmDL8Q,3627
|
|
15
|
+
mm_sol/cli/cli_utils.py,sha256=EJTWdccXg8mvkK7E-Dt7zmULzc1WoqFlbu8ASGT2SVI,1381
|
|
16
|
+
mm_sol/cli/validators.py,sha256=RFbkSl67g6CXJJhgkNi0wvx_iXjXyQEqX5GmNYatshk,407
|
|
17
|
+
mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
mm_sol/cli/cmd/balance_cmd.py,sha256=DfUZY3-Hr-F7Y0xp1faol0yLnPu6iDU3b839VhdwAbw,2587
|
|
19
|
+
mm_sol/cli/cmd/balances_cmd.py,sha256=rnsomOKu1_BsRGYvbjTSXVKQ1pqzv4F0fIAnHMjp9kk,2639
|
|
20
|
+
mm_sol/cli/cmd/example_cmd.py,sha256=bK_z4du0UPGAoiHnYdi6iaZim_kKlYw4NKBbzvyes28,221
|
|
21
|
+
mm_sol/cli/cmd/node_cmd.py,sha256=2AEAjq2M9f8-RZiI0rif6wITdns9QUb4Kr34QPsI2CA,238
|
|
22
|
+
mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=vz2_yHmx0cFCD60ubgwru9seABJN71146vW8c-fPtew,6991
|
|
23
|
+
mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=cRHVVTs9zNYmUozZ8ZlJoutn9V6r8I1AEHBrszR7dTE,538
|
|
25
|
+
mm_sol/cli/cmd/wallet/new_cmd.py,sha256=hcP9NZPkwYkTBEvT5rBntFtCIvx1QnPGn5FUDnvz5sM,425
|
|
26
|
+
mm_sol/cli/examples/balances.yml,sha256=SoFcf_IhgA2zrbihrVpqm-ikes80wLFGVzafrjO00UY,290
|
|
27
|
+
mm_sol/cli/examples/transfer-sol.yml,sha256=YFIM36NhaSEPeck6yQMvcgy3MWxgsfDqmMLRqz6P1dk,346
|
|
28
|
+
mm_sol-0.2.6.dist-info/METADATA,sha256=UVHWer8SaDg55zgUGXeoXXORcQuuakRCfiFGQ9msbYY,230
|
|
29
|
+
mm_sol-0.2.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
+
mm_sol-0.2.6.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
|
|
31
|
+
mm_sol-0.2.6.dist-info/RECORD,,
|
mm_sol/types.py
DELETED
mm_sol-0.2.5.dist-info/RECORD
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
mm_sol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
mm_sol/account.py,sha256=WlcbPsRpseC507J3E_G-A2-PJGUToNqzSGjrg0pc-B8,3144
|
|
3
|
-
mm_sol/balance.py,sha256=QzLjzW2fCGH9Uo8gjxerUnwCStsbkVrguXdPs5N6R5I,667
|
|
4
|
-
mm_sol/block.py,sha256=4Lc4TANgpGvPflVumC9MR-3vIl1dedGyci3cgzczuds,1794
|
|
5
|
-
mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
mm_sol/rpc.py,sha256=TspD_KZQp_KJDQzRknxaT8DR03okok26UWDQWF7Zflg,8031
|
|
7
|
-
mm_sol/solana_cli.py,sha256=odXNPe8G7dvsGcZxujAFjkd0vWM6fdVvt_mzPulgDZQ,8292
|
|
8
|
-
mm_sol/token.py,sha256=LpFnmTzbtKo_Z5hWX96k1Z153640N8VzXHCvsH5zArs,4903
|
|
9
|
-
mm_sol/transfer.py,sha256=Bdh3amO-sO180Clq5RVgB9y7NXHhu1EbFNgEsGzHjFY,2911
|
|
10
|
-
mm_sol/types.py,sha256=vXXP5Dc72BpHv5tsyws0KDZebG1W1-5HH0UjL7N2Mgc,113
|
|
11
|
-
mm_sol/utils.py,sha256=tjV2d6ZZ-Z61cS0WWTPX2yZ2VoJUQv11CNW0p3nO54M,1016
|
|
12
|
-
mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
mm_sol/cli/cli.py,sha256=l3AKhrgO6GEFrwh3gov9_cfjyFZY0VbZGc0o7NqUWrU,3078
|
|
14
|
-
mm_sol/cli/cli_utils.py,sha256=bOrU1LFgndBv4hlNNviHQOob3NfiOsS5YWWDm6fzpCs,1032
|
|
15
|
-
mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
mm_sol/cli/cmd/balance_cmd.py,sha256=IryMXq_8yGF9lDXO5kmUo6TRGgedl_9rS3bE1iRPTw0,2516
|
|
17
|
-
mm_sol/cli/cmd/balances_cmd.py,sha256=x6PdXqZgkSmHAsbxim06T4_iPbihUIB4GLy0ZPy5t0s,2168
|
|
18
|
-
mm_sol/cli/cmd/example_cmd.py,sha256=bK_z4du0UPGAoiHnYdi6iaZim_kKlYw4NKBbzvyes28,221
|
|
19
|
-
mm_sol/cli/cmd/node_cmd.py,sha256=UKlZZzd8MVfQHqAjigJRwPj95Mwq2cpHfWgH8BVuwaw,242
|
|
20
|
-
mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=Ta3H4TXxlCCKgcedphN6PU90iYQiZvmYYuKPO044ciw,1196
|
|
21
|
-
mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=7Dk2eT6hfWRLI0uHO_rAQS8xRwY2S6Uk4iBEIO9aQ_w,561
|
|
23
|
-
mm_sol/cli/cmd/wallet/new_cmd.py,sha256=nvFy5lkk8XUM5jchx0rn0aHHQ0LGVgbwpn7-kej9ccI,448
|
|
24
|
-
mm_sol/cli/examples/balances.yml,sha256=SoFcf_IhgA2zrbihrVpqm-ikes80wLFGVzafrjO00UY,290
|
|
25
|
-
mm_sol/cli/examples/transfer-sol.yml,sha256=fEf5lxnotLwQHt85JzwN0o6kax_Am88VKH5e-n1HP94,290
|
|
26
|
-
mm_sol-0.2.5.dist-info/METADATA,sha256=CARYWN8yylvN_Py6sDi3DBhOcrMlXja6IPlK-LxoiF4,221
|
|
27
|
-
mm_sol-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
28
|
-
mm_sol-0.2.5.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
|
|
29
|
-
mm_sol-0.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|