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,270 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ from uniswap_autopilot.common.common import (
12
+ dump_json,
13
+ load_local_env,
14
+ normalize_chain,
15
+ resolve_token,
16
+ validate_fee_tier,
17
+ )
18
+ from uniswap_autopilot.lp.v3.pool import query_pool_full_info
19
+ from uniswap_autopilot.lp.v3.tick import price_to_tick, tick_to_price, nearest_usable_tick, fee_tier_to_tick_spacing
20
+ from uniswap_autopilot.search.search import _ds_lookup
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Pair-type detection
25
+ # ---------------------------------------------------------------------------
26
+
27
+ WRAPPED_NATIVE_SYMBOLS = {"WETH", "WBTC", "WMATIC", "WBNB", "WAVAX", "WGLMR", "WSTAID"}
28
+
29
+ MAJOR_TOKEN_SYMBOLS = {
30
+ "WETH", "WBTC", "WMATIC", "WBNB", "WAVAX",
31
+ "ETH", "BTC", "USDC", "USDT", "DAI",
32
+ "ARB", "OP", "UNI", "LINK", "AAVE",
33
+ "CBETH", "RETH", "WEETH",
34
+ }
35
+
36
+
37
+ def detect_pair_type(token_a_info: dict[str, Any], token_b_info: dict[str, Any]) -> str:
38
+ """Classify the token pair to determine appropriate range widths."""
39
+ a_stable = bool(token_a_info.get("isStable"))
40
+ b_stable = bool(token_b_info.get("isStable"))
41
+ a_category = token_a_info.get("category")
42
+ b_category = token_b_info.get("category")
43
+
44
+ # Both tokens are stablecoins
45
+ if a_stable and b_stable:
46
+ return "stable_stable"
47
+ if a_category == "stablecoin" and b_category == "stablecoin":
48
+ return "stable_stable"
49
+
50
+ a_symbol = token_a_info.get("symbol", "").upper()
51
+ b_symbol = token_b_info.get("symbol", "").upper()
52
+
53
+ # Both are major tokens (WETH, WBTC, etc.)
54
+ if a_symbol in MAJOR_TOKEN_SYMBOLS and b_symbol in MAJOR_TOKEN_SYMBOLS:
55
+ return "correlated"
56
+
57
+ # One wrapped native + one non-stable
58
+ a_is_wrapped = a_symbol in WRAPPED_NATIVE_SYMBOLS
59
+ b_is_wrapped = b_symbol in WRAPPED_NATIVE_SYMBOLS
60
+ if (a_is_wrapped and not b_stable) or (b_is_wrapped and not a_stable):
61
+ return "major_volatile"
62
+
63
+ return "volatile"
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Range width presets (percentage offset from current price)
68
+ # ---------------------------------------------------------------------------
69
+
70
+ _RANGE_WIDTHS: dict[str, dict[str, float]] = {
71
+ "stable_stable": {"CONSERVATIVE": 0.5, "MODERATE": 1.0, "AGGRESSIVE": 2.0},
72
+ "correlated": {"CONSERVATIVE": 5.0, "MODERATE": 10.0, "AGGRESSIVE": 20.0},
73
+ "major_volatile": {"CONSERVATIVE": 10.0, "MODERATE": 20.0, "AGGRESSIVE": 40.0},
74
+ "volatile": {"CONSERVATIVE": 15.0, "MODERATE": 30.0, "AGGRESSIVE": 60.0},
75
+ }
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Range suggestion calculator
80
+ # ---------------------------------------------------------------------------
81
+
82
+ def calculate_range_suggestions(
83
+ current_tick: int,
84
+ tick_spacing: int,
85
+ decimals0: int,
86
+ decimals1: int,
87
+ pair_type: str,
88
+ price_change_24h: float | None = None,
89
+ ) -> list[dict[str, Any]]:
90
+ """Generate tick ranges for CONSERVATIVE / MODERATE / AGGRESSIVE profiles."""
91
+ current_price = tick_to_price(current_tick, decimals0, decimals1)
92
+ widths = _RANGE_WIDTHS.get(pair_type, _RANGE_WIDTHS["volatile"])
93
+
94
+ # If 24h price change exceeds a profile's width, widen it by the change magnitude
95
+ change_factor = abs(price_change_24h) if price_change_24h is not None else 0.0
96
+
97
+ suggestions: list[dict[str, Any]] = []
98
+ for profile, base_width in widths.items():
99
+ # Widen the range if recent volatility exceeds the preset width
100
+ effective_width = max(base_width, change_factor * 1.2)
101
+
102
+ price_lower = current_price * (1 - effective_width / 100)
103
+ price_upper = current_price * (1 + effective_width / 100)
104
+
105
+ tick_lower_raw = price_to_tick(price_lower, decimals0, decimals1)
106
+ tick_upper_raw = price_to_tick(price_upper, decimals0, decimals1)
107
+
108
+ tick_lower = nearest_usable_tick(tick_lower_raw, tick_spacing)
109
+ tick_upper = nearest_usable_tick(tick_upper_raw, tick_spacing)
110
+
111
+ # Convert aligned ticks back to human-readable prices
112
+ price_lower_aligned = tick_to_price(tick_lower, decimals0, decimals1)
113
+ price_upper_aligned = tick_to_price(tick_upper, decimals0, decimals1)
114
+
115
+ suggestions.append({
116
+ "profile": profile,
117
+ "tickLower": tick_lower,
118
+ "tickUpper": tick_upper,
119
+ "priceLower": f"{price_lower_aligned:.12g}",
120
+ "priceUpper": f"{price_upper_aligned:.12g}",
121
+ "rangeWidthPct": round(effective_width, 1),
122
+ })
123
+
124
+ return suggestions
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Main entry point
129
+ # ---------------------------------------------------------------------------
130
+
131
+ def suggest_ranges(
132
+ chain_name: str,
133
+ token_a: str,
134
+ token_b: str,
135
+ fee_tier: int,
136
+ rpc_url: str | None = None,
137
+ ) -> dict[str, Any]:
138
+ """Resolve pool state, detect pair type, and return range suggestions."""
139
+ load_local_env()
140
+
141
+ chain = normalize_chain(chain_name)
142
+ fee = validate_fee_tier(fee_tier)
143
+
144
+ # Resolve tokens (needs RPC for on-chain metadata of raw addresses)
145
+ from uniswap_autopilot.execute._internal.rpc import resolve_rpc_url
146
+ rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
147
+ token_a_info = resolve_token(chain, token_a, rpc)
148
+ token_b_info = resolve_token(chain, token_b, rpc)
149
+
150
+ # Query pool state
151
+ pool_info = query_pool_full_info(chain_name, token_a, token_b, fee, rpc_url)
152
+ if not pool_info.get("exists"):
153
+ raise RuntimeError(
154
+ f"No V3 pool found for {token_a}/{token_b} with fee tier {fee_tier} on {chain_name}"
155
+ )
156
+
157
+ current_tick: int = pool_info["currentTick"]
158
+ current_price = pool_info["currentPrice"]
159
+ tick_spacing = fee_tier_to_tick_spacing(fee)
160
+
161
+ # Determine decimals for token0 / token1 ordering
162
+ token0_addr = pool_info["token0"]
163
+ addr_a = token_a_info.get("address", "")
164
+ if token_a_info.get("address") == "NATIVE":
165
+ wrapped = chain.tokens.get(chain.wrapped_native_symbol.upper())
166
+ addr_a = wrapped.address if wrapped else resolve_token(chain, chain.wrapped_native_symbol, rpc)["address"]
167
+
168
+ if token0_addr.lower() == addr_a.lower():
169
+ decimals0 = token_a_info["decimals"]
170
+ decimals1 = token_b_info["decimals"]
171
+ else:
172
+ decimals0 = token_b_info["decimals"]
173
+ decimals1 = token_a_info["decimals"]
174
+
175
+ # Fetch 24h price change from DexScreener
176
+ price_change_24h: float | None = None
177
+ try:
178
+ ds_token_a = _ds_lookup(chain.key, addr_a)
179
+ if ds_token_a and ds_token_a.get("priceChange"):
180
+ price_change_24h = ds_token_a["priceChange"].get("h24")
181
+ except Exception:
182
+ pass
183
+
184
+ # Detect pair type
185
+ pair_type = detect_pair_type(token_a_info, token_b_info)
186
+
187
+ # Calculate suggestions
188
+ suggestions = calculate_range_suggestions(
189
+ current_tick=current_tick,
190
+ tick_spacing=tick_spacing,
191
+ decimals0=decimals0,
192
+ decimals1=decimals1,
193
+ pair_type=pair_type,
194
+ price_change_24h=price_change_24h,
195
+ )
196
+
197
+ return {
198
+ "action": "range_suggestions",
199
+ "chain": {"key": chain.key, "chainId": chain.chain_id},
200
+ "tokenA": token_a_info,
201
+ "tokenB": token_b_info,
202
+ "feeTier": fee,
203
+ "currentTick": current_tick,
204
+ "currentPrice": f"{current_price:.12g}",
205
+ "pairType": pair_type,
206
+ "priceChange24h": price_change_24h,
207
+ "suggestions": suggestions,
208
+ }
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Human-readable table
213
+ # ---------------------------------------------------------------------------
214
+
215
+ def _print_suggestion_table(result: dict[str, Any]) -> None:
216
+ """Print a human-readable table of range suggestions."""
217
+ token_a_sym = result["tokenA"].get("symbol", "?")
218
+ token_b_sym = result["tokenB"].get("symbol", "?")
219
+ pair = f"{token_a_sym}/{token_b_sym}"
220
+ chain = result["chain"]["key"]
221
+ fee = result["feeTier"]
222
+ pair_type = result["pairType"]
223
+ current_price = result["currentPrice"]
224
+ pct_24h = result.get("priceChange24h")
225
+ pct_str = f"{pct_24h:+.2f}%" if pct_24h is not None else "N/A"
226
+
227
+ print(f" Range Suggestions: {pair} on {chain} (fee={fee})")
228
+ print(f" Pair type: {pair_type} | Current price: {current_price} | 24h change: {pct_str}")
229
+ print()
230
+ print(f" {'Profile':<14} {'TickLower':>10} {'TickUpper':>10} {'PriceLower':>18} {'PriceUpper':>18} {'Width':>7}")
231
+ print(f" {'-'*14} {'-'*10} {'-'*10} {'-'*18} {'-'*18} {'-'*7}")
232
+ for s in result["suggestions"]:
233
+ print(
234
+ f" {s['profile']:<14} {s['tickLower']:>10} {s['tickUpper']:>10} "
235
+ f"{s['priceLower']:>18} {s['priceUpper']:>18} {s['rangeWidthPct']:>6.1f}%"
236
+ )
237
+ print()
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # CLI
242
+ # ---------------------------------------------------------------------------
243
+
244
+ def main() -> None:
245
+ parser = argparse.ArgumentParser(
246
+ description="Suggest V3 price ranges for a Uniswap pool based on pair type and recent volatility"
247
+ )
248
+ parser.add_argument("--chain", required=True, help="Chain name, e.g. base / ethereum")
249
+ parser.add_argument("--token-a", required=True, help="Token symbol or address")
250
+ parser.add_argument("--token-b", required=True, help="Token symbol or address")
251
+ parser.add_argument("--fee-tier", type=int, required=True, help="Fee tier: 100 / 500 / 3000 / 10000")
252
+ parser.add_argument("--rpc-url", help="RPC URL (reads from env if omitted)")
253
+ parser.add_argument("--output", help="Output JSON file path")
254
+ args = parser.parse_args()
255
+
256
+ try:
257
+ result = suggest_ranges(args.chain, args.token_a, args.token_b, args.fee_tier, args.rpc_url)
258
+ _print_suggestion_table(result)
259
+ if args.output:
260
+ Path(args.output).write_text(
261
+ json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
262
+ )
263
+ dump_json(result)
264
+ except Exception as exc:
265
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2), file=sys.stderr)
266
+ sys.exit(1)
267
+
268
+
269
+ if __name__ == "__main__":
270
+ main()
@@ -0,0 +1,194 @@
1
+ """Structured audit logging — single source of truth for cross-project events.
2
+
3
+ Every transaction-touching project emits the same JSON-line schema, so a single
4
+ ``cat *.jsonl | jq`` can correlate Uniswap swaps, Hyperliquid orders, and
5
+ Polymarket trades by ``run_id``.
6
+
7
+ Schema (one JSON object per line):
8
+
9
+ {
10
+ "ts": <ISO-8601 UTC, e.g. "2026-05-28T07:55:00.123Z">,
11
+ "ts_unix": <float seconds since epoch>,
12
+ "event": <enum: see EVENT_* constants below>,
13
+ "project": <e.g. "evm-wallet-scanner", "uniswap-autopilot">,
14
+ "run_id": <str|null — populated from STAGEFORGE_RUN_ID or caller>,
15
+ "chain": <str|null — e.g. "ethereum", "hyperliquid">,
16
+ "wallet": <str|null — 0x... or trader id>,
17
+ "tx_hash": <str|null — populated once broadcast>,
18
+ "error_code": <str|null — short stable code like "rpc_timeout">,
19
+ "details": <dict — free-form, never None>
20
+ }
21
+
22
+ Required fields are always present (null when unknown) so downstream consumers
23
+ can rely on the schema without defensive ``in`` checks.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import sys
31
+ import threading
32
+ from datetime import datetime, timezone
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ # ── Event enum (stable strings) ────────────────────────────────────────────
37
+
38
+ EVENT_PREFLIGHT = "preflight"
39
+ EVENT_QUOTE = "quote"
40
+ EVENT_SIGN = "sign"
41
+ EVENT_BROADCAST = "broadcast"
42
+ EVENT_CONFIRM = "confirm"
43
+ EVENT_CANCEL = "cancel"
44
+ EVENT_ERROR = "error"
45
+
46
+ ALLOWED_EVENTS = frozenset(
47
+ {
48
+ EVENT_PREFLIGHT,
49
+ EVENT_QUOTE,
50
+ EVENT_SIGN,
51
+ EVENT_BROADCAST,
52
+ EVENT_CONFIRM,
53
+ EVENT_CANCEL,
54
+ EVENT_ERROR,
55
+ }
56
+ )
57
+
58
+ # Required-keys order is fixed so jq/grep-based downstream tools have
59
+ # predictable JSON-line layouts.
60
+ REQUIRED_KEYS: tuple[str, ...] = (
61
+ "ts",
62
+ "ts_unix",
63
+ "event",
64
+ "project",
65
+ "run_id",
66
+ "chain",
67
+ "wallet",
68
+ "tx_hash",
69
+ "error_code",
70
+ "details",
71
+ )
72
+
73
+
74
+ _write_lock = threading.Lock()
75
+ _DEFAULT_PROJECT = __name__.split(".")[0].replace("_", "-")
76
+
77
+
78
+ def _now() -> tuple[str, float]:
79
+ now = datetime.now(tz=timezone.utc)
80
+ return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z", now.timestamp()
81
+
82
+
83
+ def _resolve_run_id(explicit: str | None) -> str | None:
84
+ if explicit:
85
+ return explicit
86
+ for name in ("STAGEFORGE_RUN_ID", "AUDIT_RUN_ID", "RUN_ID"):
87
+ value = (os.environ.get(name) or "").strip()
88
+ if value:
89
+ return value
90
+ return None
91
+
92
+
93
+ def build_record(
94
+ *,
95
+ event: str,
96
+ project: str = _DEFAULT_PROJECT,
97
+ run_id: str | None = None,
98
+ chain: str | None = None,
99
+ wallet: str | None = None,
100
+ tx_hash: str | None = None,
101
+ error_code: str | None = None,
102
+ details: dict[str, Any] | None = None,
103
+ ) -> dict[str, Any]:
104
+ """Build the canonical record dict (does not emit it)."""
105
+ if event not in ALLOWED_EVENTS:
106
+ raise ValueError(
107
+ f"unknown audit event {event!r}; allowed: {sorted(ALLOWED_EVENTS)}"
108
+ )
109
+ ts_iso, ts_unix = _now()
110
+ return {
111
+ "ts": ts_iso,
112
+ "ts_unix": ts_unix,
113
+ "event": event,
114
+ "project": project,
115
+ "run_id": _resolve_run_id(run_id),
116
+ "chain": chain,
117
+ "wallet": wallet,
118
+ "tx_hash": tx_hash,
119
+ "error_code": error_code,
120
+ "details": details or {},
121
+ }
122
+
123
+
124
+ def emit(record: dict[str, Any]) -> None:
125
+ """Write a record to the configured sink(s).
126
+
127
+ The sink target order is:
128
+
129
+ 1. The file at ``AUDIT_LOG_PATH`` if set, appended.
130
+ 2. stderr — always, so a tail-friendly trail exists.
131
+ """
132
+ line = json.dumps(record, ensure_ascii=False, separators=(",", ":"))
133
+
134
+ with _write_lock:
135
+ path = (os.environ.get("AUDIT_LOG_PATH") or "").strip()
136
+ if path:
137
+ try:
138
+ p = Path(path)
139
+ p.parent.mkdir(parents=True, exist_ok=True)
140
+ with p.open("a", encoding="utf-8") as fh:
141
+ fh.write(line + "\n")
142
+ except OSError:
143
+ # Never let logging failure break the trade path.
144
+ pass
145
+
146
+ # stderr fallback — present even when AUDIT_LOG_PATH is set so the
147
+ # operator can ``tail -f`` without finding the file first.
148
+ try:
149
+ sys.stderr.write(line + "\n")
150
+ sys.stderr.flush()
151
+ except Exception: # noqa: BLE001
152
+ pass
153
+
154
+
155
+ def log_event(
156
+ *,
157
+ event: str,
158
+ project: str = _DEFAULT_PROJECT,
159
+ run_id: str | None = None,
160
+ chain: str | None = None,
161
+ wallet: str | None = None,
162
+ tx_hash: str | None = None,
163
+ error_code: str | None = None,
164
+ details: dict[str, Any] | None = None,
165
+ ) -> dict[str, Any]:
166
+ """Shorthand: build + emit. Returns the emitted record."""
167
+ record = build_record(
168
+ event=event,
169
+ project=project,
170
+ run_id=run_id,
171
+ chain=chain,
172
+ wallet=wallet,
173
+ tx_hash=tx_hash,
174
+ error_code=error_code,
175
+ details=details,
176
+ )
177
+ emit(record)
178
+ return record
179
+
180
+
181
+ __all__ = [
182
+ "ALLOWED_EVENTS",
183
+ "EVENT_BROADCAST",
184
+ "EVENT_CANCEL",
185
+ "EVENT_CONFIRM",
186
+ "EVENT_ERROR",
187
+ "EVENT_PREFLIGHT",
188
+ "EVENT_QUOTE",
189
+ "EVENT_SIGN",
190
+ "REQUIRED_KEYS",
191
+ "build_record",
192
+ "emit",
193
+ "log_event",
194
+ ]
File without changes