@agentlayer.tech/wallet 0.1.15 → 0.1.16

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.
@@ -0,0 +1,186 @@
1
+ """Flash Trade provider helpers for perps market and position reads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from agent_wallet.config import settings
11
+ from agent_wallet.exceptions import ProviderError
12
+ from agent_wallet.http_client import get_client
13
+
14
+ PROVIDER_NAME = "flash-trade"
15
+
16
+
17
+ def _headers() -> dict[str, str]:
18
+ return {"Accept": "application/json"}
19
+
20
+
21
+ def _gateway_base_url() -> str:
22
+ return os.getenv("PROVIDER_GATEWAY_URL", settings.provider_gateway_url).strip().rstrip("/")
23
+
24
+
25
+ def _gateway_headers() -> dict[str, str]:
26
+ headers = {"Accept": "application/json"}
27
+ bearer = os.getenv(
28
+ "PROVIDER_GATEWAY_BEARER_TOKEN",
29
+ settings.provider_gateway_bearer_token,
30
+ ).strip()
31
+ if bearer:
32
+ headers["Authorization"] = f"Bearer {bearer}"
33
+ return headers
34
+
35
+
36
+ def _gateway_enabled() -> bool:
37
+ return bool(_gateway_base_url())
38
+
39
+
40
+ def _direct_base_url() -> str:
41
+ return os.getenv("FLASH_API_BASE_URL", settings.flash_api_base_url).strip().rstrip("/")
42
+
43
+
44
+ def _direct_enabled() -> bool:
45
+ return bool(_direct_base_url())
46
+
47
+
48
+ def _route_missing(status_code: int, payload: Any) -> bool:
49
+ if status_code == 404:
50
+ return True
51
+ if isinstance(payload, dict):
52
+ message = str(payload.get("error") or payload.get("message") or "").lower()
53
+ if "not found" in message or "unknown route" in message:
54
+ return True
55
+ return False
56
+
57
+
58
+ async def _request_json(
59
+ url: str,
60
+ *,
61
+ params: dict[str, Any] | None,
62
+ headers: dict[str, str],
63
+ ) -> tuple[int, Any]:
64
+ client = get_client()
65
+ try:
66
+ response = await client.get(url, params=params, headers=headers)
67
+ except httpx.HTTPError as exc:
68
+ raise ProviderError(
69
+ PROVIDER_NAME,
70
+ f"HTTP request failed: {exc}",
71
+ ) from exc
72
+ if not response.content:
73
+ return response.status_code, {}
74
+ try:
75
+ return response.status_code, response.json()
76
+ except ValueError:
77
+ return response.status_code, response.text[:500]
78
+
79
+
80
+ def _unwrap_response(
81
+ status_code: int,
82
+ payload: Any,
83
+ *,
84
+ operation: str,
85
+ ) -> Any:
86
+ if isinstance(payload, dict) and payload.get("ok") is False:
87
+ message = str(payload.get("error") or payload.get("message") or f"{operation} failed.")
88
+ raise ProviderError(PROVIDER_NAME, f"{operation} failed: {message}")
89
+
90
+ if status_code != 200:
91
+ message = payload
92
+ if isinstance(payload, dict):
93
+ message = payload.get("error") or payload.get("message") or payload
94
+ raise ProviderError(PROVIDER_NAME, f"{operation} failed: {message}")
95
+
96
+ return payload
97
+
98
+
99
+ async def _get_with_fallback(
100
+ path: str,
101
+ *,
102
+ params: dict[str, Any] | None,
103
+ operation: str,
104
+ ) -> Any:
105
+ if _gateway_enabled():
106
+ status_code, payload = await _request_json(
107
+ f"{_gateway_base_url()}{path}",
108
+ params=params,
109
+ headers=_gateway_headers(),
110
+ )
111
+ if not _route_missing(status_code, payload):
112
+ return _unwrap_response(status_code, payload, operation=operation)
113
+
114
+ if _direct_enabled():
115
+ direct_variants = [path]
116
+ if path == "/v1/flash/perps/markets":
117
+ direct_variants.append("/markets")
118
+ elif path == "/v1/flash/perps/positions":
119
+ direct_variants.append("/positions")
120
+ last_status = 404
121
+ last_payload: Any = {"error": "not found"}
122
+ for direct_path in direct_variants:
123
+ status_code, payload = await _request_json(
124
+ f"{_direct_base_url()}{direct_path}",
125
+ params=params,
126
+ headers=_headers(),
127
+ )
128
+ last_status = status_code
129
+ last_payload = payload
130
+ if _route_missing(status_code, payload) and direct_path != direct_variants[-1]:
131
+ continue
132
+ return _unwrap_response(status_code, payload, operation=operation)
133
+ return _unwrap_response(last_status, last_payload, operation=operation)
134
+
135
+ raise ProviderError(
136
+ PROVIDER_NAME,
137
+ (
138
+ f"{operation} is not configured. "
139
+ "Expose Flash routes on PROVIDER_GATEWAY_URL or set FLASH_API_BASE_URL."
140
+ ),
141
+ )
142
+
143
+
144
+ def _normalize_named_list_response(
145
+ data: Any,
146
+ *,
147
+ key: str,
148
+ ) -> dict[str, Any]:
149
+ if isinstance(data, list):
150
+ return {key: data}
151
+ if isinstance(data, dict):
152
+ items = data.get(key)
153
+ if isinstance(items, list):
154
+ return data
155
+ fallback = data.get("data")
156
+ if isinstance(fallback, list):
157
+ normalized = dict(data)
158
+ normalized[key] = fallback
159
+ return normalized
160
+ raise ProviderError(PROVIDER_NAME, f"Unexpected {key} response from Flash Trade.")
161
+
162
+
163
+ async def fetch_markets(*, pool_name: str | None = None) -> dict[str, Any]:
164
+ params = {"pool_name": pool_name} if pool_name else None
165
+ data = await _get_with_fallback(
166
+ "/v1/flash/perps/markets",
167
+ params=params,
168
+ operation="Flash Trade market lookup",
169
+ )
170
+ return _normalize_named_list_response(data, key="markets")
171
+
172
+
173
+ async def fetch_positions(
174
+ *,
175
+ owner: str,
176
+ pool_name: str | None = None,
177
+ ) -> dict[str, Any]:
178
+ params: dict[str, Any] = {"owner": owner}
179
+ if pool_name:
180
+ params["pool_name"] = pool_name
181
+ data = await _get_with_fallback(
182
+ "/v1/flash/perps/positions",
183
+ params=params,
184
+ operation="Flash Trade position lookup",
185
+ )
186
+ return _normalize_named_list_response(data, key="positions")
@@ -0,0 +1,251 @@
1
+ """Local bridge contract for Flash SDK-backed preview generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import shlex
9
+ from typing import Any
10
+
11
+ from agent_wallet.config import resolve_solana_rpc_url, settings
12
+ from agent_wallet.exceptions import ProviderError
13
+
14
+ PROVIDER_NAME = "flash-sdk-bridge"
15
+
16
+
17
+ def _bridge_command() -> list[str]:
18
+ raw = os.getenv("FLASH_SDK_BRIDGE_COMMAND", settings.flash_sdk_bridge_command).strip()
19
+ if not raw:
20
+ raise ProviderError(
21
+ PROVIDER_NAME,
22
+ "FLASH_SDK_BRIDGE_COMMAND is not configured.",
23
+ )
24
+ try:
25
+ command = shlex.split(raw)
26
+ except ValueError as exc:
27
+ raise ProviderError(PROVIDER_NAME, f"Invalid FLASH_SDK_BRIDGE_COMMAND: {exc}") from exc
28
+ if not command:
29
+ raise ProviderError(PROVIDER_NAME, "FLASH_SDK_BRIDGE_COMMAND is empty.")
30
+ return command
31
+
32
+
33
+ def _bridge_timeout_seconds() -> float:
34
+ raw = os.getenv(
35
+ "FLASH_SDK_BRIDGE_TIMEOUT_SECONDS",
36
+ str(settings.flash_sdk_bridge_timeout_seconds),
37
+ ).strip()
38
+ try:
39
+ timeout = float(raw)
40
+ except ValueError as exc:
41
+ raise ProviderError(
42
+ PROVIDER_NAME,
43
+ "FLASH_SDK_BRIDGE_TIMEOUT_SECONDS must be numeric.",
44
+ ) from exc
45
+ return max(timeout, 1.0)
46
+
47
+
48
+ def _bridge_env(payload: dict[str, Any]) -> dict[str, str]:
49
+ env = os.environ.copy()
50
+
51
+ bridge_mode = env.get("FLASH_SDK_BRIDGE_MODE", "").strip() or settings.flash_sdk_bridge_mode.strip()
52
+ if bridge_mode:
53
+ env["FLASH_SDK_BRIDGE_MODE"] = bridge_mode
54
+
55
+ if not env.get("SOLANA_RPC_URL", "").strip() and not env.get("RPC_URL", "").strip():
56
+ network = str(payload.get("network") or settings.solana_network or "mainnet").strip()
57
+ resolved_rpc_url = resolve_solana_rpc_url(network, settings.solana_rpc_url)
58
+ if resolved_rpc_url:
59
+ env["SOLANA_RPC_URL"] = resolved_rpc_url
60
+
61
+ return env
62
+
63
+
64
+ def _unwrap_bridge_payload(payload: Any, *, operation: str) -> dict[str, Any]:
65
+ if not isinstance(payload, dict):
66
+ raise ProviderError(PROVIDER_NAME, f"{operation} returned a non-object response.")
67
+ if payload.get("ok") is False:
68
+ message = str(payload.get("error") or f"{operation} failed.")
69
+ raise ProviderError(PROVIDER_NAME, message)
70
+ data = payload.get("preview")
71
+ if isinstance(data, dict):
72
+ return data
73
+ data = payload.get("prepared")
74
+ if isinstance(data, dict):
75
+ return data
76
+ if isinstance(payload.get("data"), dict):
77
+ return dict(payload["data"])
78
+ return dict(payload)
79
+
80
+
81
+ async def _call_bridge(payload: dict[str, Any]) -> dict[str, Any]:
82
+ command = _bridge_command()
83
+ env = _bridge_env(payload)
84
+ try:
85
+ process = await asyncio.create_subprocess_exec(
86
+ *command,
87
+ stdin=asyncio.subprocess.PIPE,
88
+ stdout=asyncio.subprocess.PIPE,
89
+ stderr=asyncio.subprocess.PIPE,
90
+ env=env,
91
+ )
92
+ except OSError as exc:
93
+ raise ProviderError(PROVIDER_NAME, f"Could not start Flash SDK bridge: {exc}") from exc
94
+
95
+ stdin_bytes = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
96
+ timeout = _bridge_timeout_seconds()
97
+ try:
98
+ stdout, stderr = await asyncio.wait_for(
99
+ process.communicate(stdin_bytes),
100
+ timeout=timeout,
101
+ )
102
+ except asyncio.TimeoutError as exc:
103
+ process.kill()
104
+ await process.wait()
105
+ raise ProviderError(
106
+ PROVIDER_NAME,
107
+ f"Flash SDK bridge timed out after {timeout:.1f}s.",
108
+ ) from exc
109
+
110
+ if process.returncode != 0:
111
+ message = stderr.decode("utf-8", errors="replace").strip() or stdout.decode(
112
+ "utf-8",
113
+ errors="replace",
114
+ ).strip()
115
+ raise ProviderError(
116
+ PROVIDER_NAME,
117
+ f"Flash SDK bridge failed with exit code {process.returncode}: {message[:500]}",
118
+ )
119
+
120
+ try:
121
+ decoded = json.loads(stdout.decode("utf-8"))
122
+ except json.JSONDecodeError as exc:
123
+ raise ProviderError(
124
+ PROVIDER_NAME,
125
+ "Flash SDK bridge returned invalid JSON.",
126
+ ) from exc
127
+ return decoded
128
+
129
+
130
+ async def preview_open_position_same_collateral(
131
+ *,
132
+ owner: str,
133
+ pool_name: str,
134
+ market_symbol: str,
135
+ collateral_symbol: str,
136
+ collateral_amount_raw: str,
137
+ leverage: str,
138
+ side: str,
139
+ network: str,
140
+ ) -> dict[str, Any]:
141
+ payload = {
142
+ "action": "preview_open_position_same_collateral",
143
+ "owner": owner,
144
+ "pool_name": pool_name,
145
+ "market_symbol": market_symbol,
146
+ "collateral_symbol": collateral_symbol,
147
+ "collateral_amount_raw": collateral_amount_raw,
148
+ "leverage": leverage,
149
+ "side": side,
150
+ "network": network,
151
+ }
152
+ response = await _call_bridge(payload)
153
+ return _unwrap_bridge_payload(response, operation="Flash open-position preview")
154
+
155
+
156
+ async def get_markets(
157
+ *,
158
+ pool_name: str | None,
159
+ network: str,
160
+ ) -> dict[str, Any]:
161
+ payload: dict[str, Any] = {
162
+ "action": "get_markets",
163
+ "network": network,
164
+ }
165
+ if pool_name:
166
+ payload["pool_name"] = pool_name
167
+ response = await _call_bridge(payload)
168
+ return _unwrap_bridge_payload(response, operation="Flash market lookup")
169
+
170
+
171
+ async def get_positions(
172
+ *,
173
+ owner: str,
174
+ pool_name: str | None,
175
+ network: str,
176
+ ) -> dict[str, Any]:
177
+ payload: dict[str, Any] = {
178
+ "action": "get_positions",
179
+ "owner": owner,
180
+ "network": network,
181
+ }
182
+ if pool_name:
183
+ payload["pool_name"] = pool_name
184
+ response = await _call_bridge(payload)
185
+ return _unwrap_bridge_payload(response, operation="Flash position lookup")
186
+
187
+
188
+ async def preview_close_position_same_collateral(
189
+ *,
190
+ owner: str,
191
+ pool_name: str,
192
+ market_symbol: str,
193
+ side: str,
194
+ network: str,
195
+ ) -> dict[str, Any]:
196
+ payload = {
197
+ "action": "preview_close_position_same_collateral",
198
+ "owner": owner,
199
+ "pool_name": pool_name,
200
+ "market_symbol": market_symbol,
201
+ "side": side,
202
+ "network": network,
203
+ }
204
+ response = await _call_bridge(payload)
205
+ return _unwrap_bridge_payload(response, operation="Flash close-position preview")
206
+
207
+
208
+ async def prepare_open_position_same_collateral(
209
+ *,
210
+ owner: str,
211
+ pool_name: str,
212
+ market_symbol: str,
213
+ collateral_symbol: str,
214
+ collateral_amount_raw: str,
215
+ leverage: str,
216
+ side: str,
217
+ network: str,
218
+ ) -> dict[str, Any]:
219
+ payload = {
220
+ "action": "prepare_open_position_same_collateral",
221
+ "owner": owner,
222
+ "pool_name": pool_name,
223
+ "market_symbol": market_symbol,
224
+ "collateral_symbol": collateral_symbol,
225
+ "collateral_amount_raw": collateral_amount_raw,
226
+ "leverage": leverage,
227
+ "side": side,
228
+ "network": network,
229
+ }
230
+ response = await _call_bridge(payload)
231
+ return _unwrap_bridge_payload(response, operation="Flash open-position prepare")
232
+
233
+
234
+ async def prepare_close_position_same_collateral(
235
+ *,
236
+ owner: str,
237
+ pool_name: str,
238
+ market_symbol: str,
239
+ side: str,
240
+ network: str,
241
+ ) -> dict[str, Any]:
242
+ payload = {
243
+ "action": "prepare_close_position_same_collateral",
244
+ "owner": owner,
245
+ "pool_name": pool_name,
246
+ "market_symbol": market_symbol,
247
+ "side": side,
248
+ "network": network,
249
+ }
250
+ response = await _call_bridge(payload)
251
+ return _unwrap_bridge_payload(response, operation="Flash close-position prepare")
@@ -767,3 +767,82 @@ def verify_provider_kamino_lend_transaction(
767
767
  "action": action,
768
768
  "verified": True,
769
769
  }
770
+
771
+
772
+ def verify_provider_flash_transaction(
773
+ message: Any,
774
+ *,
775
+ wallet_address: str,
776
+ market_address: str,
777
+ target_custody_address: str,
778
+ collateral_custody_address: str,
779
+ action: str,
780
+ expected_program_ids: list[str],
781
+ position_address: str | None = None,
782
+ collateral_mint: str | None = None,
783
+ loaded_addresses: list[str] | None = None,
784
+ ) -> dict[str, Any]:
785
+ binding = _assert_basic_wallet_binding(
786
+ message,
787
+ wallet_address=wallet_address,
788
+ loaded_addresses=loaded_addresses,
789
+ )
790
+ keys = binding["account_keys"]
791
+ if market_address not in keys:
792
+ raise WalletBackendError(
793
+ f"{action} transaction does not reference the expected Flash market account."
794
+ )
795
+ if target_custody_address not in keys:
796
+ raise WalletBackendError(
797
+ f"{action} transaction does not reference the expected Flash target custody."
798
+ )
799
+ if collateral_custody_address not in keys:
800
+ raise WalletBackendError(
801
+ f"{action} transaction does not reference the expected Flash collateral custody."
802
+ )
803
+ if position_address and position_address not in keys:
804
+ raise WalletBackendError(
805
+ f"{action} transaction does not reference the expected Flash position account."
806
+ )
807
+ if collateral_mint and collateral_mint not in keys:
808
+ raise WalletBackendError(
809
+ f"{action} transaction does not reference the expected Flash collateral mint."
810
+ )
811
+
812
+ allowed_programs = CORE_PROGRAM_IDS | {pid for pid in expected_program_ids if pid}
813
+ program_ids = _program_ids(message, loaded_addresses)
814
+ unknown_program_ids = _assert_program_allowlist(
815
+ program_ids,
816
+ allowed_programs=allowed_programs,
817
+ label=action,
818
+ reject_unknown=False,
819
+ )
820
+ recognized_flash_program_ids = [
821
+ pid for pid in program_ids if pid in expected_program_ids
822
+ ]
823
+ if not recognized_flash_program_ids:
824
+ raise WalletBackendError(
825
+ f"{action} transaction does not include the expected Flash program."
826
+ )
827
+ return {
828
+ "wallet_address": wallet_address,
829
+ "fee_payer": binding["fee_payer"],
830
+ "required_signer_keys": binding["required_signer_keys"],
831
+ "required_signature_count": binding["required_signature_count"],
832
+ "wallet_signer_index": binding["wallet_signer_index"],
833
+ "sponsored_fee_payer": binding["sponsored_fee_payer"],
834
+ "program_ids": program_ids,
835
+ "unknown_program_ids": unknown_program_ids,
836
+ "recognized_flash_program_ids": recognized_flash_program_ids,
837
+ "has_recognized_flash_program": True,
838
+ "non_core_program_ids": [pid for pid in program_ids if pid not in CORE_PROGRAM_IDS],
839
+ "account_key_count": len(keys),
840
+ "instruction_count": len(_compiled_instructions(message)),
841
+ "market_address": market_address,
842
+ "target_custody_address": target_custody_address,
843
+ "collateral_custody_address": collateral_custody_address,
844
+ "position_address": position_address,
845
+ "collateral_mint": collateral_mint,
846
+ "action": action,
847
+ "verified": True,
848
+ }
@@ -425,6 +425,84 @@ class AgentWalletBackend(ABC):
425
425
  ) -> dict[str, Any]:
426
426
  raise WalletBackendError(f"{self.name} does not support Jupiter Earn earnings.")
427
427
 
428
+ async def get_flash_trade_markets(
429
+ self,
430
+ pool_name: str | None = None,
431
+ ) -> dict[str, Any]:
432
+ raise WalletBackendError(f"{self.name} does not support Flash Trade market lookup.")
433
+
434
+ async def get_flash_trade_positions(
435
+ self,
436
+ owner: str | None = None,
437
+ pool_name: str | None = None,
438
+ ) -> dict[str, Any]:
439
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position lookup.")
440
+
441
+ async def preview_flash_trade_open_position(
442
+ self,
443
+ *,
444
+ pool_name: str,
445
+ market_symbol: str,
446
+ collateral_symbol: str,
447
+ collateral_amount_raw: str,
448
+ leverage: str,
449
+ side: str,
450
+ ) -> dict[str, Any]:
451
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position-open previews.")
452
+
453
+ async def preview_flash_trade_close_position(
454
+ self,
455
+ *,
456
+ pool_name: str,
457
+ market_symbol: str,
458
+ side: str,
459
+ ) -> dict[str, Any]:
460
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position-close previews.")
461
+
462
+ async def prepare_flash_trade_open_position(
463
+ self,
464
+ *,
465
+ pool_name: str,
466
+ market_symbol: str,
467
+ collateral_symbol: str,
468
+ collateral_amount_raw: str,
469
+ leverage: str,
470
+ side: str,
471
+ ) -> dict[str, Any]:
472
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position-open prepare.")
473
+
474
+ async def prepare_flash_trade_close_position(
475
+ self,
476
+ *,
477
+ pool_name: str,
478
+ market_symbol: str,
479
+ side: str,
480
+ ) -> dict[str, Any]:
481
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position-close prepare.")
482
+
483
+ async def execute_flash_trade_open_position(
484
+ self,
485
+ *,
486
+ pool_name: str,
487
+ market_symbol: str,
488
+ collateral_symbol: str,
489
+ collateral_amount_raw: str,
490
+ leverage: str,
491
+ side: str,
492
+ approved_preview: dict[str, Any] | None = None,
493
+ ) -> dict[str, Any]:
494
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position-open execute.")
495
+
496
+ async def execute_flash_trade_close_position(
497
+ self,
498
+ *,
499
+ pool_name: str,
500
+ market_symbol: str,
501
+ side: str,
502
+ approved_preview: dict[str, Any] | None = None,
503
+ ) -> dict[str, Any]:
504
+ raise WalletBackendError(f"{self.name} does not support Flash Trade position-close execute.")
505
+
428
506
  async def get_kamino_lend_markets(self) -> dict[str, Any]:
429
507
  raise WalletBackendError(f"{self.name} does not support Kamino market lookup.")
430
508