uni-exec-engine 0.2.0__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.
Files changed (80) hide show
  1. uni_exec_engine-0.2.0.dist-info/METADATA +576 -0
  2. uni_exec_engine-0.2.0.dist-info/RECORD +80 -0
  3. uni_exec_engine-0.2.0.dist-info/WHEEL +5 -0
  4. uni_exec_engine-0.2.0.dist-info/licenses/LICENSE +21 -0
  5. uni_exec_engine-0.2.0.dist-info/top_level.txt +1 -0
  6. uniswap_autopilot/__init__.py +3 -0
  7. uniswap_autopilot/analytics/__init__.py +0 -0
  8. uniswap_autopilot/analytics/il_calculator.py +525 -0
  9. uniswap_autopilot/analytics/portfolio.py +234 -0
  10. uniswap_autopilot/analytics/position.py +433 -0
  11. uniswap_autopilot/analytics/range_suggest.py +270 -0
  12. uniswap_autopilot/audit.py +194 -0
  13. uniswap_autopilot/common/__init__.py +0 -0
  14. uniswap_autopilot/common/approval_cleanup.py +255 -0
  15. uniswap_autopilot/common/check_balance.py +64 -0
  16. uniswap_autopilot/common/common.py +804 -0
  17. uniswap_autopilot/common/deep_link.py +132 -0
  18. uniswap_autopilot/common/gas.py +141 -0
  19. uniswap_autopilot/data/auto_trade_policy.example.json +29 -0
  20. uniswap_autopilot/data/chains.json +135 -0
  21. uniswap_autopilot/data/common-token-addresses.json +777 -0
  22. uniswap_autopilot/execute/__init__.py +0 -0
  23. uniswap_autopilot/execute/_internal/__init__.py +5 -0
  24. uniswap_autopilot/execute/_internal/constants.py +15 -0
  25. uniswap_autopilot/execute/_internal/preflight.py +150 -0
  26. uniswap_autopilot/execute/_internal/pure_signer.py +182 -0
  27. uniswap_autopilot/execute/_internal/rpc.py +462 -0
  28. uniswap_autopilot/execute/_internal/signer.py +298 -0
  29. uniswap_autopilot/execute/_internal/submit.py +73 -0
  30. uniswap_autopilot/execute/_internal/tx.py +370 -0
  31. uniswap_autopilot/execute/broadcast.py +380 -0
  32. uniswap_autopilot/execute/detect.py +52 -0
  33. uniswap_autopilot/execute/telegram_confirm.py +272 -0
  34. uniswap_autopilot/lp/compare_pools.py +338 -0
  35. uniswap_autopilot/lp/v2/__init__.py +0 -0
  36. uniswap_autopilot/lp/v2/approve.py +100 -0
  37. uniswap_autopilot/lp/v2/build_tx.py +226 -0
  38. uniswap_autopilot/lp/v2/flow.py +204 -0
  39. uniswap_autopilot/lp/v2/pair.py +135 -0
  40. uniswap_autopilot/lp/v2/positions.py +177 -0
  41. uniswap_autopilot/lp/v3/__init__.py +0 -0
  42. uniswap_autopilot/lp/v3/approve.py +109 -0
  43. uniswap_autopilot/lp/v3/auto_rebalance.py +282 -0
  44. uniswap_autopilot/lp/v3/build_tx.py +465 -0
  45. uniswap_autopilot/lp/v3/compound.py +258 -0
  46. uniswap_autopilot/lp/v3/flow.py +358 -0
  47. uniswap_autopilot/lp/v3/pool.py +175 -0
  48. uniswap_autopilot/lp/v3/position.py +112 -0
  49. uniswap_autopilot/lp/v3/tick.py +75 -0
  50. uniswap_autopilot/lp/v4/__init__.py +0 -0
  51. uniswap_autopilot/lp/v4/approve.py +105 -0
  52. uniswap_autopilot/lp/v4/build_tx.py +669 -0
  53. uniswap_autopilot/lp/v4/flow.py +368 -0
  54. uniswap_autopilot/lp/v4/pool.py +174 -0
  55. uniswap_autopilot/lp/v4/position.py +185 -0
  56. uniswap_autopilot/policy.py +371 -0
  57. uniswap_autopilot/price_feed.py +100 -0
  58. uniswap_autopilot/py.typed +0 -0
  59. uniswap_autopilot/search/__init__.py +0 -0
  60. uniswap_autopilot/search/risk.py +200 -0
  61. uniswap_autopilot/search/search.py +580 -0
  62. uniswap_autopilot/state_machine.py +315 -0
  63. uniswap_autopilot/swap/__init__.py +1 -0
  64. uniswap_autopilot/swap/deep_link.py +69 -0
  65. uniswap_autopilot/swap/extensions/__init__.py +2 -0
  66. uniswap_autopilot/swap/extensions/bridge.py +197 -0
  67. uniswap_autopilot/swap/extensions/limit_order.py +272 -0
  68. uniswap_autopilot/swap/extensions/slippage.py +123 -0
  69. uniswap_autopilot/swap/flow.py +693 -0
  70. uniswap_autopilot/swap/flow_core/__init__.py +2 -0
  71. uniswap_autopilot/swap/flow_core/artifacts.py +11 -0
  72. uniswap_autopilot/swap/flow_core/broadcast.py +50 -0
  73. uniswap_autopilot/swap/flow_core/diagnostics.py +216 -0
  74. uniswap_autopilot/swap/flow_core/paper.py +113 -0
  75. uniswap_autopilot/swap/flow_core/policy.py +142 -0
  76. uniswap_autopilot/swap/links/__init__.py +2 -0
  77. uniswap_autopilot/swap/links/deep_link.py +69 -0
  78. uniswap_autopilot/swap/trading_api/permit.py +41 -0
  79. uniswap_autopilot/swap/trading_api/quote.py +282 -0
  80. uniswap_autopilot/swap/trading_api/swap.py +248 -0
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ import uuid
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from uniswap_autopilot import policy as _policy
13
+ from uniswap_autopilot import state_machine
14
+ from uniswap_autopilot.audit import (
15
+ EVENT_BROADCAST,
16
+ EVENT_CONFIRM,
17
+ EVENT_ERROR,
18
+ EVENT_PREFLIGHT,
19
+ log_event,
20
+ )
21
+ from uniswap_autopilot.common.common import dump_json, load_local_env
22
+ from uniswap_autopilot.execute._internal.constants import (
23
+ APPROVE_SELECTOR,
24
+ CHAIN_BY_ID,
25
+ DEFAULT_PRIVATE_KEY_ENV,
26
+ GLOBAL_RPC_ENV_CANDIDATES,
27
+ HEX_DATA_RE,
28
+ HOT_WALLET_ACCOUNT_ENV_NAME,
29
+ HOT_WALLET_KEYSTORE_ENV_NAME,
30
+ HOT_WALLET_PASSWORD_FILE_ENV_NAME,
31
+ HOT_WALLET_PRIVATE_KEY_ENV_NAME,
32
+ )
33
+ from uniswap_autopilot.execute._internal.preflight import build_preflight_report
34
+ from uniswap_autopilot.execute._internal.rpc import (
35
+ estimate_transaction_gas,
36
+ execute_cast_receipt,
37
+ parse_cast_int_output,
38
+ query_erc20_allowance,
39
+ query_erc20_balance,
40
+ query_gas_price,
41
+ query_native_balance,
42
+ receipt_succeeded,
43
+ resolve_rpc_url,
44
+ rpc_env_candidates,
45
+ run_cast_text,
46
+ )
47
+ from uniswap_autopilot.execute._internal.signer import (
48
+ add_signer_arguments,
49
+ auto_select_signer_args,
50
+ build_signer_namespace,
51
+ detect_hot_wallet_backend,
52
+ ensure_signer_backend,
53
+ has_direct_signer,
54
+ sign_typed_data_with_backend,
55
+ )
56
+ from uniswap_autopilot.execute._internal.submit import (
57
+ broadcast_with_backend,
58
+ build_broadcast_package,
59
+ extract_transaction_hash,
60
+ )
61
+ from uniswap_autopilot.execute._internal.tx import (
62
+ base_units_to_human,
63
+ build_confirmation_phrase,
64
+ build_execute_preview,
65
+ decode_erc20_approve_call,
66
+ load_approval_transaction,
67
+ load_json,
68
+ load_swap_transaction,
69
+ lookup_token_decimals,
70
+ lookup_token_symbol,
71
+ normalize_transaction,
72
+ optional_address,
73
+ parse_intish,
74
+ summarize_transaction,
75
+ validate_hex_data,
76
+ )
77
+
78
+ __all__ = [
79
+ # audit
80
+ "EVENT_BROADCAST", "EVENT_CONFIRM", "EVENT_ERROR", "EVENT_PREFLIGHT", "log_event",
81
+ # common
82
+ "dump_json", "load_local_env",
83
+ # constants
84
+ "APPROVE_SELECTOR", "CHAIN_BY_ID", "DEFAULT_PRIVATE_KEY_ENV",
85
+ "GLOBAL_RPC_ENV_CANDIDATES", "HEX_DATA_RE",
86
+ "HOT_WALLET_ACCOUNT_ENV_NAME", "HOT_WALLET_KEYSTORE_ENV_NAME",
87
+ "HOT_WALLET_PASSWORD_FILE_ENV_NAME", "HOT_WALLET_PRIVATE_KEY_ENV_NAME",
88
+ # preflight
89
+ "build_preflight_report",
90
+ # rpc
91
+ "estimate_transaction_gas", "execute_cast_receipt", "parse_cast_int_output",
92
+ "query_erc20_allowance", "query_erc20_balance", "query_gas_price",
93
+ "query_native_balance", "receipt_succeeded", "resolve_rpc_url",
94
+ "rpc_env_candidates", "run_cast_text",
95
+ # signer
96
+ "add_signer_arguments", "auto_select_signer_args", "build_signer_namespace",
97
+ "detect_hot_wallet_backend", "ensure_signer_backend", "has_direct_signer",
98
+ "sign_typed_data_with_backend",
99
+ # submit
100
+ "broadcast_with_backend", "build_broadcast_package", "extract_transaction_hash",
101
+ # tx
102
+ "base_units_to_human", "build_confirmation_phrase", "build_execute_preview",
103
+ "decode_erc20_approve_call", "load_approval_transaction", "load_json",
104
+ "load_swap_transaction", "lookup_token_decimals", "lookup_token_symbol",
105
+ "normalize_transaction", "optional_address", "parse_intish",
106
+ "summarize_transaction", "validate_hex_data",
107
+ # entry point
108
+ "main",
109
+ ]
110
+
111
+
112
+ def main() -> None:
113
+ parser = argparse.ArgumentParser(description="对 approval 或 swap 交易做安全广播")
114
+ source_group = parser.add_mutually_exclusive_group(required=True)
115
+ source_group.add_argument("--swap-file", help="swap_dry_run.py 的输出文件")
116
+ source_group.add_argument(
117
+ "--approval-file", help="trading_api_quote.py 的 quote 输出文件;会读取 approvalCheck.approval"
118
+ )
119
+ source_group.add_argument("--lp-file", help="LP 脚本(build_lp_tx.py / run_lp_flow.py)的输出文件")
120
+ parser.add_argument("--rpc-url", help="显式指定 RPC URL;否则尝试从环境变量解析")
121
+ parser.add_argument("--broadcast", action="store_true", help="真的调用 cast send 广播交易")
122
+ parser.add_argument("--confirm", help="必须与脚本给出的 confirmation 完全一致才允许广播")
123
+ parser.add_argument(
124
+ "--telegram-confirm", action="store_true", help="下单前推送 Telegram inline button 等待用户确认"
125
+ )
126
+ parser.add_argument("--receipt-confirmations", type=int, default=1, help="广播后查询 receipt 的确认数,默认 1")
127
+ add_signer_arguments(parser)
128
+ parser.add_argument("--output", help="把完整 JSON 输出写入文件")
129
+ args = parser.parse_args()
130
+
131
+ run_id: str | None = None
132
+ try:
133
+ load_local_env()
134
+
135
+ if args.swap_file:
136
+ tx = load_swap_transaction(args.swap_file)
137
+ elif args.approval_file:
138
+ tx = load_approval_transaction(args.approval_file)
139
+ elif args.lp_file:
140
+ blob = json.loads(Path(args.lp_file).read_text(encoding="utf-8"))
141
+ lp_tx = (
142
+ blob.get("transaction")
143
+ or blob.get("mint", {}).get("transaction")
144
+ or blob.get("increase", {}).get("transaction")
145
+ or blob.get("decrease", {}).get("transaction")
146
+ or blob.get("collect", {}).get("transaction")
147
+ )
148
+ if not lp_tx:
149
+ raise ValueError("LP file must contain 'transaction' or a sub-action with 'transaction'")
150
+ tx = normalize_transaction(lp_tx, tx_kind="lp", source_file=str(args.lp_file), source_kind="lp-file")
151
+ else:
152
+ raise ValueError("one of --swap-file, --approval-file, --lp-file is required")
153
+
154
+ preview = build_execute_preview(tx, explicit_rpc_url=args.rpc_url)
155
+ summary = preview["summary"]
156
+ rpc_url = summary["rpcUrlResolved"]
157
+ _rpc_candidates = summary["rpcEnvCandidates"]
158
+
159
+ response: dict[str, Any] = {
160
+ "action": "execute_transaction",
161
+ "summary": summary,
162
+ "broadcastRequested": args.broadcast,
163
+ "commandPreview": preview["commandPreview"],
164
+ }
165
+ response["preflight"] = build_preflight_report(tx, explicit_rpc_url=args.rpc_url)
166
+
167
+ # Telegram confirmation if requested or exceeds threshold
168
+ THRESHOLD_USD = float(os.environ.get("TRADE_CONFIRM_THRESHOLD_USD", "0"))
169
+
170
+ # Calculate trade value in USD
171
+ from uniswap_autopilot.execute._internal.tx import estimate_trade_value_usd
172
+
173
+ estimate_data = {
174
+ **summary,
175
+ "tokenIn": tx.get("inputTokenSymbol", ""),
176
+ "tokenOut": tx.get("outputTokenSymbol", ""),
177
+ "amountIn": tx.get("inputAmount", "0"),
178
+ "amountOut": tx.get("outputAmount", "0"),
179
+ "inputToken": tx.get("inputToken"),
180
+ "outputToken": tx.get("outputToken"),
181
+ "chain": (tx.get("chainKey") or "").lower(),
182
+ }
183
+ trade_value_usd = estimate_trade_value_usd(estimate_data)
184
+
185
+ if args.telegram_confirm or (THRESHOLD_USD > 0 and trade_value_usd >= THRESHOLD_USD):
186
+ from uniswap_autopilot.execute.telegram_confirm import request_trade_confirmation
187
+
188
+ trade_details = {
189
+ "chain": summary.get("chain"),
190
+ "tokenIn": summary.get("tokenIn"),
191
+ "tokenOut": summary.get("tokenOut"),
192
+ "amountIn": summary.get("amountIn"),
193
+ "amountOut": summary.get("amountOut"),
194
+ "slippage": summary.get("slippage"),
195
+ "valueUsd": f"${trade_value_usd:.2f}",
196
+ }
197
+
198
+ if not request_trade_confirmation(trade_details, timeout_seconds=300):
199
+ print("❌ Trade rejected or timeout")
200
+ sys.exit(0)
201
+
202
+ # Telegram 确认后自动补上确认短语,免去手动 --confirm
203
+ if not args.confirm:
204
+ from uniswap_autopilot.execute._internal.tx import build_confirmation_phrase
205
+
206
+ args.confirm = build_confirmation_phrase(tx)
207
+
208
+ response["telegramConfirmation"] = {
209
+ "status": "approved",
210
+ "valueUsd": f"${trade_value_usd:.2f}",
211
+ }
212
+ else:
213
+ response["telegramConfirmation"] = {
214
+ "status": "skipped",
215
+ "reason": "below threshold",
216
+ "valueUsd": f"${trade_value_usd:.2f}",
217
+ }
218
+
219
+ chain_key = (summary.get("chain") or tx.get("chainKey") or "").lower() or None
220
+ wallet_addr = (tx.get("from") if isinstance(tx, dict) else None) or summary.get("from") or summary.get("sender")
221
+
222
+ if args.broadcast:
223
+ response["preflight"] = build_preflight_report(
224
+ tx,
225
+ explicit_rpc_url=args.rpc_url,
226
+ strict=True,
227
+ )
228
+ # --- state machine: init run_id + preflight checkpoint ---
229
+ run_id = (
230
+ os.environ.get("AUDIT_RUN_ID")
231
+ or os.environ.get("STAGEFORGE_RUN_ID")
232
+ or f"uniswap-{uuid.uuid4().hex[:12]}"
233
+ )
234
+ action = state_machine.next_action(run_id)
235
+ if action is None:
236
+ raise RuntimeError(f"run {run_id} is in terminal state — cannot proceed")
237
+ if action == state_machine.STATE_PREFLIGHT:
238
+ state_machine.transition(run_id, state_machine.STATE_PREFLIGHT)
239
+ log_event(
240
+ event=EVENT_PREFLIGHT,
241
+ chain=chain_key,
242
+ wallet=wallet_addr,
243
+ error_code=None if response["preflight"].get("ok") else "preflight_failed",
244
+ details={"ok": response["preflight"].get("ok"), "issues": response["preflight"].get("issues")},
245
+ )
246
+ if response["preflight"].get("ok") is False:
247
+ raise RuntimeError(
248
+ "broadcast preflight failed: " + "; ".join(response["preflight"].get("issues") or [])
249
+ )
250
+ # --- policy gate (PREFLIGHT → SIGNED) ---
251
+ pol = _policy.load_policy()
252
+ policy_ctx = {
253
+ "chain": chain_key,
254
+ "sender": wallet_addr,
255
+ "receiver": tx.get("to"),
256
+ "amount": tx.get("value"),
257
+ }
258
+ policy_result = _policy.check_uniswap(pol, policy_ctx)
259
+ response["policyCheck"] = policy_result.to_dict()
260
+ if not policy_result.allowed:
261
+ state_machine.transition(run_id, state_machine.STATE_FAILED,
262
+ payload={"policy_violations": policy_result.to_dict()["violations"]})
263
+ log_event(
264
+ event=EVENT_ERROR,
265
+ chain=chain_key,
266
+ wallet=wallet_addr,
267
+ error_code="policy_rejected",
268
+ details={"violations": policy_result.to_dict()["violations"]},
269
+ )
270
+ raise RuntimeError(
271
+ f"Policy rejected: {'; '.join(v.message for v in policy_result.violations)}"
272
+ )
273
+ if policy_result.warnings:
274
+ log_event(
275
+ event=EVENT_PREFLIGHT,
276
+ chain=chain_key,
277
+ wallet=wallet_addr,
278
+ details={"stage": "policy", "warnings": policy_result.to_dict()["warnings"]},
279
+ )
280
+ # --- sign + broadcast (guard side effects before execution) ---
281
+ action = state_machine.next_action(run_id)
282
+ tx_hash_ = ""
283
+ receipt_rpc_url = rpc_url
284
+ if action == state_machine.STATE_SIGNED:
285
+ state_machine.transition(run_id, state_machine.STATE_SIGNED)
286
+ broadcast = broadcast_with_backend(
287
+ tx=tx,
288
+ explicit_rpc_url=args.rpc_url,
289
+ confirm=args.confirm,
290
+ signer_args_source=args,
291
+ )
292
+ response["commandPreview"] = broadcast["commandPreview"]
293
+ response["broadcastResult"] = broadcast["broadcastResult"]
294
+ response["signerBackend"] = broadcast["signerBackend"]
295
+ if broadcast.get("serviceDecision") is not None:
296
+ response["serviceDecision"] = broadcast["serviceDecision"]
297
+ tx_hash_ = extract_transaction_hash(response["broadcastResult"]) or ""
298
+ response["transactionHash"] = tx_hash_
299
+ receipt_rpc_url = broadcast["rpcUrl"]
300
+ log_event(
301
+ event=EVENT_BROADCAST,
302
+ chain=chain_key,
303
+ wallet=wallet_addr,
304
+ tx_hash=tx_hash_ or None,
305
+ details={
306
+ "signerBackend": broadcast["signerBackend"],
307
+ "tokenIn": tx.get("inputTokenSymbol"),
308
+ "tokenOut": tx.get("outputTokenSymbol"),
309
+ "amountIn": str(tx.get("inputAmount", "")),
310
+ "amountOut": str(tx.get("outputAmount", "")),
311
+ },
312
+ )
313
+ state_machine.transition(run_id, state_machine.STATE_BROADCAST, payload={"tx_hash": tx_hash_})
314
+ elif action == state_machine.STATE_BROADCAST:
315
+ # Already broadcast in a previous run: recover persisted tx hash.
316
+ saved = state_machine.load_state(run_id) or {}
317
+ tx_hash_ = str(saved.get("payload", {}).get("tx_hash", "") or "")
318
+ if not tx_hash_:
319
+ raise RuntimeError(f"run {run_id} is BROADCAST but missing tx_hash payload")
320
+ response["transactionHash"] = tx_hash_
321
+ response["broadcastResult"] = {"transactionHash": tx_hash_}
322
+ elif action == state_machine.STATE_CONFIRMED:
323
+ saved = state_machine.load_state(run_id) or {}
324
+ tx_hash_ = str(saved.get("payload", {}).get("tx_hash", "") or "")
325
+ response["transactionHash"] = tx_hash_
326
+ response["note"] = f"run {run_id} already confirmed; skipping broadcast/confirm"
327
+ elif action is None:
328
+ raise RuntimeError(f"run {run_id} is in terminal state — cannot sign/broadcast")
329
+ else:
330
+ raise RuntimeError(f"unexpected next_action={action!r} before broadcast")
331
+
332
+ if tx_hash_ and state_machine.next_action(run_id) == state_machine.STATE_CONFIRMED:
333
+ response["receipt"] = execute_cast_receipt(
334
+ tx_hash=tx_hash_,
335
+ rpc_url=receipt_rpc_url,
336
+ confirmations=args.receipt_confirmations,
337
+ )
338
+ if "receipt" in response:
339
+ success = receipt_succeeded(response["receipt"])
340
+ log_event(
341
+ event=EVENT_CONFIRM,
342
+ chain=chain_key,
343
+ wallet=wallet_addr,
344
+ tx_hash=tx_hash_ or response.get("transactionHash"),
345
+ error_code=None if success else "receipt_failed",
346
+ details={
347
+ "status": response["receipt"].get("status"),
348
+ "confirmations": args.receipt_confirmations,
349
+ },
350
+ )
351
+ # --- state machine: confirmed ---
352
+ action = state_machine.next_action(run_id)
353
+ if action == state_machine.STATE_CONFIRMED:
354
+ state_machine.transition(run_id, state_machine.STATE_CONFIRMED)
355
+ if not success:
356
+ raise RuntimeError(f"broadcast receipt status is not successful: {response['receipt'].get('status')}")
357
+
358
+ if args.output:
359
+ Path(args.output).write_text(
360
+ json.dumps(response, ensure_ascii=False, indent=2) + "\n",
361
+ encoding="utf-8",
362
+ )
363
+ dump_json(response)
364
+ except Exception as exc: # noqa: BLE001
365
+ # --- state machine: failed (transition error = bug, let it surface) ---
366
+ if run_id:
367
+ state_machine.transition(run_id, state_machine.STATE_FAILED, payload={"error_code": type(exc).__name__})
368
+ log_event(
369
+ event=EVENT_ERROR,
370
+ chain=None,
371
+ wallet=None,
372
+ error_code=type(exc).__name__,
373
+ details={"message": str(exc), "action": "execute_transaction"},
374
+ )
375
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
376
+ sys.exit(1)
377
+
378
+
379
+ if __name__ == "__main__":
380
+ main()
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """Detect signing backend availability."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from uniswap_autopilot.common.common import dump_json, load_local_env
8
+ from uniswap_autopilot.execute._internal.signer import detect_hot_wallet_backend
9
+
10
+
11
+ def sanitize_backend_status(status: dict[str, object]) -> dict[str, object]:
12
+ sanitized = dict(status)
13
+ signer_args = sanitized.pop("signerArgs", None)
14
+ if signer_args is None:
15
+ return sanitized
16
+
17
+ mode = None
18
+ wallet_source = None
19
+ if getattr(signer_args, "private_key_env", None):
20
+ mode = "private-key-env"
21
+ wallet_source = getattr(signer_args, "wallet_source", None)
22
+
23
+ sanitized["signerConfig"] = {
24
+ "mode": mode,
25
+ "walletSource": wallet_source,
26
+ }
27
+ return sanitized
28
+
29
+
30
+ def main() -> None:
31
+ parser = argparse.ArgumentParser(description="Detect signing backend availability")
32
+ _ = parser.parse_args()
33
+
34
+ load_local_env()
35
+ hot = sanitize_backend_status(detect_hot_wallet_backend())
36
+
37
+ selected_backend = None
38
+ selected_wallet = None
39
+ if hot.get("available"):
40
+ selected_backend = "pure-python"
41
+ selected_wallet = hot.get("wallet")
42
+
43
+ dump_json({
44
+ "action": "detect_execution_context",
45
+ "signer": hot,
46
+ "selectedBackend": selected_backend,
47
+ "selectedWallet": selected_wallet,
48
+ })
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()