sol-parser-sdk-python 0.4.4__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.
- sol_parser/__init__.py +400 -0
- sol_parser/account_dispatcher.py +209 -0
- sol_parser/account_fillers/__init__.py +5 -0
- sol_parser/account_fillers/bonk.py +30 -0
- sol_parser/account_fillers/meteora.py +51 -0
- sol_parser/account_fillers/orca.py +40 -0
- sol_parser/account_fillers/pumpfun.py +97 -0
- sol_parser/account_fillers/pumpswap.py +93 -0
- sol_parser/account_fillers/raydium.py +119 -0
- sol_parser/accounts/__init__.py +461 -0
- sol_parser/accounts/rpc_wallet.py +64 -0
- sol_parser/accounts/utils.py +71 -0
- sol_parser/check_migration.py +18 -0
- sol_parser/clock.py +10 -0
- sol_parser/common/__init__.py +27 -0
- sol_parser/dex_parsers.py +2576 -0
- sol_parser/entries_decode.py +186 -0
- sol_parser/env_config.py +215 -0
- sol_parser/event_types.py +1750 -0
- sol_parser/geyser_pb2.py +148 -0
- sol_parser/geyser_pb2_grpc.py +398 -0
- sol_parser/grpc/__init__.py +61 -0
- sol_parser/grpc/geyser_connect.py +42 -0
- sol_parser/grpc/subscribe_builder.py +133 -0
- sol_parser/grpc/transaction_meta.py +183 -0
- sol_parser/grpc_client.py +870 -0
- sol_parser/grpc_instruction_parser.py +318 -0
- sol_parser/grpc_types.py +919 -0
- sol_parser/inner_instruction_parser.py +281 -0
- sol_parser/instr/__init__.py +15 -0
- sol_parser/instr_account_utils.py +58 -0
- sol_parser/instructions.py +1026 -0
- sol_parser/json_util.py +41 -0
- sol_parser/log_instr_dedup.py +284 -0
- sol_parser/logs/__init__.py +15 -0
- sol_parser/merger.py +233 -0
- sol_parser/order_buffer.py +171 -0
- sol_parser/parser.py +300 -0
- sol_parser/pumpfun_fee_enrich.py +75 -0
- sol_parser/rpc_parser.py +655 -0
- sol_parser/rust_api_inventory.py +42 -0
- sol_parser/rust_event_json.py +50 -0
- sol_parser/shredstream_client.py +191 -0
- sol_parser/shredstream_pb2.py +40 -0
- sol_parser/shredstream_pb2_grpc.py +81 -0
- sol_parser/shredstream_pumpfun.py +296 -0
- sol_parser/solana_storage_pb2.py +75 -0
- sol_parser/solana_storage_pb2_grpc.py +24 -0
- sol_parser/u128_parity.py +115 -0
- sol_parser/verify_discriminators.py +85 -0
- sol_parser_sdk_python-0.4.4.dist-info/METADATA +14 -0
- sol_parser_sdk_python-0.4.4.dist-info/RECORD +54 -0
- sol_parser_sdk_python-0.4.4.dist-info/WHEEL +4 -0
- sol_parser_sdk_python-0.4.4.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""gRPC ShredStream ``Entry.entries`` 负载解码(对齐 Node ``entries_decode.ts`` / Go ``DecodeEntriesBincode``)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
MAX_VEC_ENTRIES = 100_000
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_u64_le(buf: bytes, offset: int) -> int:
|
|
13
|
+
return struct.unpack_from("<Q", buf, offset)[0]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def bincode_vec_entry_count(entries_bytes: bytes) -> int:
|
|
17
|
+
if len(entries_bytes) < 8:
|
|
18
|
+
raise ValueError("shredstream: entries payload too short for vec length")
|
|
19
|
+
n = read_u64_le(entries_bytes, 0)
|
|
20
|
+
if n > MAX_VEC_ENTRIES:
|
|
21
|
+
raise ValueError(f"shredstream: corrupt entry_count {n} exceeds limit")
|
|
22
|
+
return n
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def decode_compact_u16(buf: bytes, pos: int) -> Optional[tuple[int, int]]:
|
|
26
|
+
if pos >= len(buf):
|
|
27
|
+
return None
|
|
28
|
+
b0 = buf[pos]
|
|
29
|
+
if b0 < 0x80:
|
|
30
|
+
return (b0, 1)
|
|
31
|
+
if pos + 1 >= len(buf):
|
|
32
|
+
return None
|
|
33
|
+
b1 = buf[pos + 1]
|
|
34
|
+
if b1 < 0x80:
|
|
35
|
+
return ((b0 & 0x7F) | (b1 << 7), 2)
|
|
36
|
+
if pos + 2 >= len(buf):
|
|
37
|
+
return None
|
|
38
|
+
b2 = buf[pos + 2]
|
|
39
|
+
return ((b0 & 0x7F) | ((b1 & 0x7F) << 7) | (b2 << 14), 3)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class _ParsedTx:
|
|
44
|
+
tx_len: int
|
|
45
|
+
signatures: List[bytes]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_transaction_wire(buf: bytes, pos: int) -> Optional[_ParsedTx]:
|
|
49
|
+
start = pos
|
|
50
|
+
if pos >= len(buf):
|
|
51
|
+
return None
|
|
52
|
+
sig_count_enc = decode_compact_u16(buf, pos)
|
|
53
|
+
if not sig_count_enc:
|
|
54
|
+
return None
|
|
55
|
+
p = pos + sig_count_enc[1]
|
|
56
|
+
sig_count = sig_count_enc[0]
|
|
57
|
+
sigs_end = p + sig_count * 64
|
|
58
|
+
if sigs_end > len(buf):
|
|
59
|
+
return None
|
|
60
|
+
sigs: List[bytes] = []
|
|
61
|
+
for _ in range(sig_count):
|
|
62
|
+
sigs.append(buf[p : p + 64])
|
|
63
|
+
p += 64
|
|
64
|
+
if p >= len(buf):
|
|
65
|
+
return None
|
|
66
|
+
msg_first = buf[p]
|
|
67
|
+
is_v0 = msg_first >= 0x80
|
|
68
|
+
if is_v0:
|
|
69
|
+
p += 1
|
|
70
|
+
p += 3
|
|
71
|
+
if p > len(buf):
|
|
72
|
+
return None
|
|
73
|
+
acct_enc = decode_compact_u16(buf, p)
|
|
74
|
+
if not acct_enc:
|
|
75
|
+
return None
|
|
76
|
+
p += acct_enc[1]
|
|
77
|
+
p += acct_enc[0] * 32
|
|
78
|
+
if p > len(buf):
|
|
79
|
+
return None
|
|
80
|
+
ix_count_enc = decode_compact_u16(buf, p)
|
|
81
|
+
if not ix_count_enc:
|
|
82
|
+
return None
|
|
83
|
+
p += ix_count_enc[1]
|
|
84
|
+
ix_count = ix_count_enc[0]
|
|
85
|
+
for _ in range(ix_count):
|
|
86
|
+
p += 1
|
|
87
|
+
if p > len(buf):
|
|
88
|
+
return None
|
|
89
|
+
acct_len_enc = decode_compact_u16(buf, p)
|
|
90
|
+
if not acct_len_enc:
|
|
91
|
+
return None
|
|
92
|
+
p += acct_len_enc[1]
|
|
93
|
+
p += acct_len_enc[0]
|
|
94
|
+
if p > len(buf):
|
|
95
|
+
return None
|
|
96
|
+
data_len_enc = decode_compact_u16(buf, p)
|
|
97
|
+
if not data_len_enc:
|
|
98
|
+
return None
|
|
99
|
+
p += data_len_enc[1]
|
|
100
|
+
p += data_len_enc[0]
|
|
101
|
+
if p > len(buf):
|
|
102
|
+
return None
|
|
103
|
+
if is_v0:
|
|
104
|
+
if p >= len(buf):
|
|
105
|
+
return None
|
|
106
|
+
atl_count_enc = decode_compact_u16(buf, p)
|
|
107
|
+
if not atl_count_enc:
|
|
108
|
+
return None
|
|
109
|
+
p += atl_count_enc[1]
|
|
110
|
+
atl_count = atl_count_enc[0]
|
|
111
|
+
for _ in range(atl_count):
|
|
112
|
+
p += 32
|
|
113
|
+
if p > len(buf):
|
|
114
|
+
return None
|
|
115
|
+
w_len_enc = decode_compact_u16(buf, p)
|
|
116
|
+
if not w_len_enc:
|
|
117
|
+
return None
|
|
118
|
+
p += w_len_enc[1]
|
|
119
|
+
p += w_len_enc[0]
|
|
120
|
+
if p > len(buf):
|
|
121
|
+
return None
|
|
122
|
+
r_len_enc = decode_compact_u16(buf, p)
|
|
123
|
+
if not r_len_enc:
|
|
124
|
+
return None
|
|
125
|
+
p += r_len_enc[1]
|
|
126
|
+
p += r_len_enc[0]
|
|
127
|
+
if p > len(buf):
|
|
128
|
+
return None
|
|
129
|
+
return _ParsedTx(tx_len=p - start, signatures=sigs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _BatchDecoder:
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
self.buffer = bytearray()
|
|
135
|
+
self.expected_entry_count = 0
|
|
136
|
+
self.entries_yielded = 0
|
|
137
|
+
self.cursor = 0
|
|
138
|
+
|
|
139
|
+
def append(self, payload: bytes) -> None:
|
|
140
|
+
self.buffer.extend(payload)
|
|
141
|
+
|
|
142
|
+
def try_decode_entry(self) -> Optional[List[bytes]]:
|
|
143
|
+
pos = self.cursor
|
|
144
|
+
buf = self.buffer
|
|
145
|
+
if pos + 48 > len(buf):
|
|
146
|
+
return None
|
|
147
|
+
p = pos + 8 + 32
|
|
148
|
+
tx_count = read_u64_le(buf, p)
|
|
149
|
+
p += 8
|
|
150
|
+
txs: List[bytes] = []
|
|
151
|
+
for _ in range(tx_count):
|
|
152
|
+
tx_start = p
|
|
153
|
+
parsed = parse_transaction_wire(buf, p)
|
|
154
|
+
if not parsed:
|
|
155
|
+
raise ValueError("shredstream: truncated transaction in entry")
|
|
156
|
+
raw = bytes(buf[tx_start : tx_start + parsed.tx_len])
|
|
157
|
+
txs.append(raw)
|
|
158
|
+
p = tx_start + parsed.tx_len
|
|
159
|
+
self.cursor = p
|
|
160
|
+
return txs
|
|
161
|
+
|
|
162
|
+
def push_flat(self, payload: bytes) -> List[bytes]:
|
|
163
|
+
self.append(payload)
|
|
164
|
+
if self.expected_entry_count == 0 and self.entries_yielded == 0 and self.cursor == 0:
|
|
165
|
+
if len(self.buffer) < 8:
|
|
166
|
+
raise ValueError("shredstream: entries payload too short for vec length")
|
|
167
|
+
count = read_u64_le(self.buffer, 0)
|
|
168
|
+
if count > MAX_VEC_ENTRIES:
|
|
169
|
+
raise ValueError(f"shredstream: corrupt entry_count {count} exceeds limit")
|
|
170
|
+
self.expected_entry_count = int(count)
|
|
171
|
+
self.cursor = 8
|
|
172
|
+
out: List[bytes] = []
|
|
173
|
+
while self.entries_yielded < self.expected_entry_count:
|
|
174
|
+
entry_txs = self.try_decode_entry()
|
|
175
|
+
if entry_txs is None:
|
|
176
|
+
raise ValueError(f"shredstream: incomplete entry at offset {self.cursor}")
|
|
177
|
+
out.extend(entry_txs)
|
|
178
|
+
self.entries_yielded += 1
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def decode_entries_bincode_flat(entries_bytes: bytes) -> List[bytes]:
|
|
183
|
+
if not entries_bytes:
|
|
184
|
+
return []
|
|
185
|
+
dec = _BatchDecoder()
|
|
186
|
+
return dec.push_flat(entries_bytes)
|
sol_parser/env_config.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Environment and CLI helpers aligned with sol-parser-sdk-nodejs (GRPC_URL, .env, flags)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_dotenv_silent(dotenv_path: Optional[str] = None) -> bool:
|
|
11
|
+
"""Load ``.env`` from the current working directory (or ``dotenv_path``).
|
|
12
|
+
|
|
13
|
+
Does **not** override variables already set in the process environment
|
|
14
|
+
(same idea as ``dotenv`` on Node). Returns ``False`` if ``python-dotenv``
|
|
15
|
+
is not installed.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
except ImportError:
|
|
20
|
+
return False
|
|
21
|
+
load_dotenv(dotenv_path, override=False)
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _first_nonempty(*values: Optional[str]) -> str:
|
|
26
|
+
for v in values:
|
|
27
|
+
if v is None:
|
|
28
|
+
continue
|
|
29
|
+
s = str(v).strip()
|
|
30
|
+
if s:
|
|
31
|
+
return s
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_grpc_cli(argv: Sequence[str]) -> tuple[Optional[str], Optional[str]]:
|
|
36
|
+
"""Parse ``--grpc-url``, ``--grpc-token``, and short aliases from *argv*."""
|
|
37
|
+
url: Optional[str] = None
|
|
38
|
+
token: Optional[str] = None
|
|
39
|
+
i = 0
|
|
40
|
+
n = len(argv)
|
|
41
|
+
while i < n:
|
|
42
|
+
a = argv[i]
|
|
43
|
+
if a in ("--grpc-url", "--grpc-endpoint", "-g") and i + 1 < n:
|
|
44
|
+
url = argv[i + 1].strip()
|
|
45
|
+
i += 2
|
|
46
|
+
continue
|
|
47
|
+
if a.startswith("--grpc-url="):
|
|
48
|
+
url = a.split("=", 1)[1].strip()
|
|
49
|
+
i += 1
|
|
50
|
+
continue
|
|
51
|
+
if a.startswith("--grpc-endpoint="):
|
|
52
|
+
url = a.split("=", 1)[1].strip()
|
|
53
|
+
i += 1
|
|
54
|
+
continue
|
|
55
|
+
if a in ("--grpc-token", "--token", "-t") and i + 1 < n:
|
|
56
|
+
token = argv[i + 1].strip()
|
|
57
|
+
i += 2
|
|
58
|
+
continue
|
|
59
|
+
if a.startswith("--grpc-token="):
|
|
60
|
+
token = a.split("=", 1)[1].strip()
|
|
61
|
+
i += 1
|
|
62
|
+
continue
|
|
63
|
+
if a.startswith("--token="):
|
|
64
|
+
token = a.split("=", 1)[1].strip()
|
|
65
|
+
i += 1
|
|
66
|
+
continue
|
|
67
|
+
i += 1
|
|
68
|
+
return url, token
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_grpc_credentials(
|
|
72
|
+
argv: Optional[Sequence[str]] = None,
|
|
73
|
+
*,
|
|
74
|
+
default_endpoint: str = "solana-yellowstone-grpc.publicnode.com:443",
|
|
75
|
+
require_token: bool = False,
|
|
76
|
+
) -> Tuple[str, str]:
|
|
77
|
+
"""Resolve Yellowstone gRPC URL and x-token.
|
|
78
|
+
|
|
79
|
+
**Precedence (each field):** CLI flags > ``GRPC_URL`` / ``GRPC_ENDPOINT`` /
|
|
80
|
+
``GRPC_AUTH_TOKEN`` / ``GRPC_TOKEN`` > legacy ``GEYSER_*`` >
|
|
81
|
+
*default_endpoint* (URL only).
|
|
82
|
+
|
|
83
|
+
Call :func:`load_dotenv_silent` first so a project-local ``.env`` is applied.
|
|
84
|
+
|
|
85
|
+
:param require_token: If ``True``, print an error and ``sys.exit(1)`` when the
|
|
86
|
+
token is still empty after resolution.
|
|
87
|
+
"""
|
|
88
|
+
load_dotenv_silent()
|
|
89
|
+
cli_url, cli_token = _parse_grpc_cli(list(argv) if argv is not None else [])
|
|
90
|
+
url = _first_nonempty(
|
|
91
|
+
cli_url,
|
|
92
|
+
os.environ.get("GRPC_URL"),
|
|
93
|
+
os.environ.get("GRPC_ENDPOINT"),
|
|
94
|
+
os.environ.get("GEYSER_ENDPOINT"),
|
|
95
|
+
default_endpoint,
|
|
96
|
+
)
|
|
97
|
+
token = _first_nonempty(
|
|
98
|
+
cli_token,
|
|
99
|
+
os.environ.get("GRPC_AUTH_TOKEN"),
|
|
100
|
+
os.environ.get("GRPC_TOKEN"),
|
|
101
|
+
os.environ.get("GEYSER_API_TOKEN"),
|
|
102
|
+
)
|
|
103
|
+
if require_token and not token:
|
|
104
|
+
print(
|
|
105
|
+
"Error: GRPC_AUTH_TOKEN / GRPC_TOKEN is required (x-token for the gRPC endpoint).\n"
|
|
106
|
+
" Copy .env.example to .env in the package root and set GRPC_AUTH_TOKEN or GRPC_TOKEN, or:\n"
|
|
107
|
+
" export GRPC_AUTH_TOKEN=<your_token>\n"
|
|
108
|
+
" You can also pass: --grpc-token <token> or --token=<token>",
|
|
109
|
+
file=sys.stderr,
|
|
110
|
+
)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
return url, token
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def require_grpc_env(argv: Optional[Sequence[str]] = None) -> Tuple[str, str]:
|
|
116
|
+
"""Require both gRPC URL and token (same contract as Node ``scripts/grpc_env.ts``).
|
|
117
|
+
|
|
118
|
+
No default URL: set ``GRPC_URL`` / ``GEYSER_ENDPOINT`` or ``--grpc-url``.
|
|
119
|
+
"""
|
|
120
|
+
load_dotenv_silent()
|
|
121
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
122
|
+
cli_url, cli_token = _parse_grpc_cli(argv)
|
|
123
|
+
url = _first_nonempty(
|
|
124
|
+
cli_url,
|
|
125
|
+
os.environ.get("GRPC_URL"),
|
|
126
|
+
os.environ.get("GRPC_ENDPOINT"),
|
|
127
|
+
os.environ.get("GEYSER_ENDPOINT"),
|
|
128
|
+
)
|
|
129
|
+
token = _first_nonempty(
|
|
130
|
+
cli_token,
|
|
131
|
+
os.environ.get("GRPC_AUTH_TOKEN"),
|
|
132
|
+
os.environ.get("GRPC_TOKEN"),
|
|
133
|
+
os.environ.get("GEYSER_API_TOKEN"),
|
|
134
|
+
)
|
|
135
|
+
if not url:
|
|
136
|
+
print(
|
|
137
|
+
"Error: GRPC_URL / GRPC_ENDPOINT is required.\n"
|
|
138
|
+
" Copy .env.example to .env in the package root and set GRPC_URL (and GRPC_AUTH_TOKEN), or:\n"
|
|
139
|
+
" export GRPC_URL=https://your-yellowstone-host:443\n"
|
|
140
|
+
" CLI: --grpc-url <url> or -g <url>",
|
|
141
|
+
file=sys.stderr,
|
|
142
|
+
)
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
if not token:
|
|
145
|
+
print(
|
|
146
|
+
"Error: GRPC_AUTH_TOKEN / GRPC_TOKEN is required (x-token header for the gRPC endpoint).\n"
|
|
147
|
+
" Copy .env.example to .env in the package root and set GRPC_AUTH_TOKEN, or:\n"
|
|
148
|
+
" export GRPC_AUTH_TOKEN=<your_token>",
|
|
149
|
+
file=sys.stderr,
|
|
150
|
+
)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
return url, token
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_shredstream_url(
|
|
156
|
+
argv: Optional[Sequence[str]] = None,
|
|
157
|
+
*,
|
|
158
|
+
default_url: str = "http://127.0.0.1:10800",
|
|
159
|
+
) -> str:
|
|
160
|
+
"""ShredStream HTTP endpoint: ``--url`` / ``-u`` / ``--endpoint=`` > ``SHREDSTREAM_URL`` / ``SHRED_URL`` > default."""
|
|
161
|
+
load_dotenv_silent()
|
|
162
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
163
|
+
for i, a in enumerate(argv):
|
|
164
|
+
if a in ("--url", "-u", "--endpoint") and i + 1 < len(argv):
|
|
165
|
+
v = argv[i + 1].strip()
|
|
166
|
+
if v:
|
|
167
|
+
return v
|
|
168
|
+
if a.startswith("--url="):
|
|
169
|
+
return a[6:].strip()
|
|
170
|
+
if a.startswith("--endpoint="):
|
|
171
|
+
return a[11:].strip()
|
|
172
|
+
env_url = _first_nonempty(os.environ.get("SHREDSTREAM_URL"), os.environ.get("SHRED_URL"))
|
|
173
|
+
return env_url or default_url
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_optional_rpc_url(
|
|
177
|
+
argv: Optional[Sequence[str]] = None,
|
|
178
|
+
*,
|
|
179
|
+
default_rpc: str = "https://api.mainnet-beta.solana.com",
|
|
180
|
+
) -> str:
|
|
181
|
+
"""``--rpc`` / ``-r`` / ``--rpc=`` > ``RPC_URL`` > *default_rpc*."""
|
|
182
|
+
load_dotenv_silent()
|
|
183
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
184
|
+
for i, a in enumerate(argv):
|
|
185
|
+
if a in ("--rpc", "-r") and i + 1 < len(argv):
|
|
186
|
+
v = argv[i + 1].strip()
|
|
187
|
+
if v:
|
|
188
|
+
return v
|
|
189
|
+
if a.startswith("--rpc="):
|
|
190
|
+
return a[6:].strip()
|
|
191
|
+
return _first_nonempty(os.environ.get("RPC_URL"), os.environ.get("SOLANA_RPC_URL")) or default_rpc
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _parse_sig_cli(argv: Sequence[str]) -> Optional[str]:
|
|
195
|
+
for i, a in enumerate(argv):
|
|
196
|
+
if a in ("--sig", "-s") and i + 1 < len(argv):
|
|
197
|
+
v = argv[i + 1].strip()
|
|
198
|
+
if v:
|
|
199
|
+
return v
|
|
200
|
+
if a.startswith("--sig="):
|
|
201
|
+
return a[6:].strip()
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def parse_rpc_and_tx_signature(
|
|
206
|
+
argv: Optional[Sequence[str]] = None,
|
|
207
|
+
*,
|
|
208
|
+
default_signature: str,
|
|
209
|
+
) -> Tuple[str, str]:
|
|
210
|
+
"""For ``parse_tx_by_signature``-style tools: RPC URL + transaction signature."""
|
|
211
|
+
load_dotenv_silent()
|
|
212
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
213
|
+
rpc = parse_optional_rpc_url(argv)
|
|
214
|
+
sig = _first_nonempty(_parse_sig_cli(argv), os.environ.get("TX_SIGNATURE"), default_signature)
|
|
215
|
+
return rpc, sig
|