@agentlayer.tech/wallet 0.1.25 → 0.1.27
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.
- package/.openclaw/extensions/agent-wallet/README.md +1 -1
- package/.openclaw/extensions/agent-wallet/dist/index.js +35 -9
- package/.openclaw/extensions/agent-wallet/index.ts +35 -9
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -0
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +5 -1
- package/CHANGELOG.md +26 -0
- package/agent-wallet/README.md +1 -1
- package/agent-wallet/agent_wallet/config.py +1 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +242 -15
- package/agent-wallet/agent_wallet/openclaw_cli.py +1 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +127 -14
- package/agent-wallet/agent_wallet/providers/kamino.py +6 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +6 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +131 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +528 -43
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +7 -2
- package/package.json +2 -2
|
@@ -352,6 +352,28 @@ class OpenClawWalletAdapter:
|
|
|
352
352
|
summary[key] = value
|
|
353
353
|
return summary
|
|
354
354
|
|
|
355
|
+
if asset_type == "solana-swap-intent":
|
|
356
|
+
return {
|
|
357
|
+
"operation": action_label,
|
|
358
|
+
"network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
|
|
359
|
+
"owner": payload.get("owner"),
|
|
360
|
+
"input_mint": payload.get("input_mint"),
|
|
361
|
+
"output_mint": payload.get("output_mint"),
|
|
362
|
+
"input_amount_ui": payload.get("input_amount_ui"),
|
|
363
|
+
"input_amount_raw": payload.get("input_amount_raw"),
|
|
364
|
+
"minimum_output_amount_raw": payload.get("minimum_output_amount_raw"),
|
|
365
|
+
"minimum_output_amount_ui": payload.get("minimum_output_amount_ui"),
|
|
366
|
+
"max_slippage_bps": payload.get("max_slippage_bps"),
|
|
367
|
+
"slippage_bps": payload.get("slippage_bps"),
|
|
368
|
+
"max_fee_lamports": payload.get("max_fee_lamports"),
|
|
369
|
+
"valid_until_epoch_seconds": payload.get("valid_until_epoch_seconds"),
|
|
370
|
+
"valid_for_seconds": payload.get("valid_for_seconds"),
|
|
371
|
+
"max_attempts": payload.get("max_attempts"),
|
|
372
|
+
"allowed_providers": payload.get("allowed_providers"),
|
|
373
|
+
"recipient_policy": payload.get("recipient_policy"),
|
|
374
|
+
"spend_policy": payload.get("spend_policy"),
|
|
375
|
+
}
|
|
376
|
+
|
|
355
377
|
if asset_type == "solana-lifi-cross-chain-swap":
|
|
356
378
|
return {
|
|
357
379
|
"operation": action_label,
|
|
@@ -738,6 +760,7 @@ class OpenClawWalletAdapter:
|
|
|
738
760
|
"owner",
|
|
739
761
|
"authority",
|
|
740
762
|
"address",
|
|
763
|
+
"obligation_address",
|
|
741
764
|
"market",
|
|
742
765
|
"reserve",
|
|
743
766
|
"amount_native",
|
|
@@ -2451,7 +2474,9 @@ class OpenClawWalletAdapter:
|
|
|
2451
2474
|
name="swap_solana_tokens",
|
|
2452
2475
|
description=(
|
|
2453
2476
|
"Preview or execute a Solana token swap through Jupiter routing. "
|
|
2454
|
-
"
|
|
2477
|
+
"Prefer intent_preview, then intent_execute after explicit chat confirmation; "
|
|
2478
|
+
"intent_execute fetches a fresh quote and only sends if it remains inside the approved limits. "
|
|
2479
|
+
"OpenClaw should not use legacy execute for Solana swaps because exact Jupiter quote payloads expire quickly."
|
|
2455
2480
|
),
|
|
2456
2481
|
input_schema={
|
|
2457
2482
|
"type": "object",
|
|
@@ -2470,12 +2495,28 @@ class OpenClawWalletAdapter:
|
|
|
2470
2495
|
},
|
|
2471
2496
|
"slippage_bps": {
|
|
2472
2497
|
"type": "integer",
|
|
2473
|
-
"description": "Optional slippage tolerance in basis points. Defaults to
|
|
2498
|
+
"description": "Optional slippage tolerance in basis points. Defaults to 300 (3%) for Solana swaps.",
|
|
2499
|
+
},
|
|
2500
|
+
"minimum_output_amount_raw": {
|
|
2501
|
+
"type": "integer",
|
|
2502
|
+
"description": "Optional approved minimum output in raw output token units for intent_preview. For intent swaps, overly strict values are clamped to the slippage floor.",
|
|
2503
|
+
},
|
|
2504
|
+
"max_fee_lamports": {
|
|
2505
|
+
"type": "integer",
|
|
2506
|
+
"description": "Optional maximum Solana network fee in lamports for intent_preview.",
|
|
2507
|
+
},
|
|
2508
|
+
"valid_for_seconds": {
|
|
2509
|
+
"type": "integer",
|
|
2510
|
+
"description": "Optional intent validity window in seconds. Intent swaps use at least 120 seconds, max 120.",
|
|
2511
|
+
},
|
|
2512
|
+
"max_attempts": {
|
|
2513
|
+
"type": "integer",
|
|
2514
|
+
"description": "Optional number of fresh quote/simulate/execute attempts. Intent swaps use at least 3 attempts, max 5.",
|
|
2474
2515
|
},
|
|
2475
2516
|
"mode": {
|
|
2476
2517
|
"type": "string",
|
|
2477
|
-
"enum": ["preview", "prepare", "execute"],
|
|
2478
|
-
"description": "
|
|
2518
|
+
"enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
|
|
2519
|
+
"description": "intent_preview returns approved risk limits; intent_execute requotes and executes atomically inside those limits. Legacy preview/prepare/execute remains supported.",
|
|
2479
2520
|
},
|
|
2480
2521
|
"purpose": {
|
|
2481
2522
|
"type": "string",
|
|
@@ -2929,6 +2970,10 @@ class OpenClawWalletAdapter:
|
|
|
2929
2970
|
"type": "string",
|
|
2930
2971
|
"description": "Decimal token amount to withdraw, as a string.",
|
|
2931
2972
|
},
|
|
2973
|
+
"obligation_address": {
|
|
2974
|
+
"type": "string",
|
|
2975
|
+
"description": "Optional Kamino obligation address. Required when preview shows multiple matching obligations.",
|
|
2976
|
+
},
|
|
2932
2977
|
"mode": {
|
|
2933
2978
|
"type": "string",
|
|
2934
2979
|
"enum": ["preview", "prepare", "execute"],
|
|
@@ -2963,6 +3008,10 @@ class OpenClawWalletAdapter:
|
|
|
2963
3008
|
"type": "string",
|
|
2964
3009
|
"description": "Decimal token amount to borrow, as a string.",
|
|
2965
3010
|
},
|
|
3011
|
+
"obligation_address": {
|
|
3012
|
+
"type": "string",
|
|
3013
|
+
"description": "Optional Kamino obligation address. Required when preview shows multiple obligations in the selected market.",
|
|
3014
|
+
},
|
|
2966
3015
|
"mode": {
|
|
2967
3016
|
"type": "string",
|
|
2968
3017
|
"enum": ["preview", "prepare", "execute"],
|
|
@@ -2997,6 +3046,10 @@ class OpenClawWalletAdapter:
|
|
|
2997
3046
|
"type": "string",
|
|
2998
3047
|
"description": "Decimal token amount to repay, as a string.",
|
|
2999
3048
|
},
|
|
3049
|
+
"obligation_address": {
|
|
3050
|
+
"type": "string",
|
|
3051
|
+
"description": "Optional Kamino obligation address. Required when preview shows multiple matching debt obligations.",
|
|
3052
|
+
},
|
|
3000
3053
|
"mode": {
|
|
3001
3054
|
"type": "string",
|
|
3002
3055
|
"enum": ["preview", "prepare", "execute"],
|
|
@@ -4963,6 +5016,7 @@ class OpenClawWalletAdapter:
|
|
|
4963
5016
|
market = args.get("market")
|
|
4964
5017
|
reserve = args.get("reserve")
|
|
4965
5018
|
amount_ui = args.get("amount_ui")
|
|
5019
|
+
obligation_address = args.get("obligation_address")
|
|
4966
5020
|
mode = args.get("mode")
|
|
4967
5021
|
purpose = args.get("purpose")
|
|
4968
5022
|
user_intent = args.get("user_intent", False)
|
|
@@ -4974,6 +5028,8 @@ class OpenClawWalletAdapter:
|
|
|
4974
5028
|
raise WalletBackendError("reserve is required.")
|
|
4975
5029
|
if not isinstance(amount_ui, str) or not amount_ui.strip():
|
|
4976
5030
|
raise WalletBackendError("amount_ui is required.")
|
|
5031
|
+
if obligation_address is not None and not isinstance(obligation_address, str):
|
|
5032
|
+
raise WalletBackendError("obligation_address must be a string when provided.")
|
|
4977
5033
|
if mode not in {"preview", "prepare", "execute"}:
|
|
4978
5034
|
raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
|
|
4979
5035
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
@@ -5006,6 +5062,7 @@ class OpenClawWalletAdapter:
|
|
|
5006
5062
|
market=market.strip(),
|
|
5007
5063
|
reserve=reserve.strip(),
|
|
5008
5064
|
amount_ui=amount_ui.strip(),
|
|
5065
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5009
5066
|
)
|
|
5010
5067
|
return AgentToolResult(
|
|
5011
5068
|
tool=tool_name,
|
|
@@ -5023,7 +5080,12 @@ class OpenClawWalletAdapter:
|
|
|
5023
5080
|
market=market.strip(),
|
|
5024
5081
|
reserve=reserve.strip(),
|
|
5025
5082
|
amount_ui=amount_ui.strip(),
|
|
5083
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5026
5084
|
)
|
|
5085
|
+
if bool(preview.get("requires_obligation_address")):
|
|
5086
|
+
raise WalletBackendError(
|
|
5087
|
+
f"{action_label} requires obligation_address when multiple Kamino obligations match the selected position."
|
|
5088
|
+
)
|
|
5027
5089
|
return AgentToolResult(
|
|
5028
5090
|
tool=tool_name,
|
|
5029
5091
|
ok=True,
|
|
@@ -5037,24 +5099,49 @@ class OpenClawWalletAdapter:
|
|
|
5037
5099
|
),
|
|
5038
5100
|
)
|
|
5039
5101
|
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5102
|
+
approved_preview = args.get("_approved_preview")
|
|
5103
|
+
execute_preview = None
|
|
5104
|
+
approval_payload = inspect_approval_token(
|
|
5105
|
+
approval_token,
|
|
5106
|
+
tool_name=tool_name,
|
|
5107
|
+
network=str(getattr(self.backend, "network", "unknown")),
|
|
5108
|
+
require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
|
|
5044
5109
|
)
|
|
5110
|
+
approval_summary = approval_payload.get("binding", {}).get("summary")
|
|
5111
|
+
if not isinstance(approval_summary, dict):
|
|
5112
|
+
raise WalletBackendError(
|
|
5113
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
5114
|
+
)
|
|
5115
|
+
approval_summary_copy = dict(approval_summary)
|
|
5116
|
+
if isinstance(approval_summary_copy.get("_preview_digest"), str):
|
|
5117
|
+
if not isinstance(approved_preview, dict):
|
|
5118
|
+
raise WalletBackendError(
|
|
5119
|
+
f"Approved {action_label} preview payload is required for execute mode. Generate a new preview and approval before execute."
|
|
5120
|
+
)
|
|
5121
|
+
if preview_payload_digest(approved_preview) != approval_summary_copy["_preview_digest"]:
|
|
5122
|
+
raise WalletBackendError(
|
|
5123
|
+
"approved preview payload does not match the approval token. Generate a new preview and approval before execute."
|
|
5124
|
+
)
|
|
5125
|
+
execute_preview = dict(approved_preview)
|
|
5126
|
+
else:
|
|
5127
|
+
execute_preview = await preview_method(
|
|
5128
|
+
market=market.strip(),
|
|
5129
|
+
reserve=reserve.strip(),
|
|
5130
|
+
amount_ui=amount_ui.strip(),
|
|
5131
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5132
|
+
)
|
|
5045
5133
|
self._require_execute_approval(
|
|
5046
5134
|
approval_token=approval_token,
|
|
5047
5135
|
tool_name=tool_name,
|
|
5048
|
-
summary=
|
|
5049
|
-
action_label=action_label,
|
|
5050
|
-
payload=execute_preview,
|
|
5051
|
-
),
|
|
5136
|
+
summary=approval_summary_copy,
|
|
5052
5137
|
action_label=action_label,
|
|
5053
5138
|
)
|
|
5054
5139
|
result = await execute_method(
|
|
5055
5140
|
market=market.strip(),
|
|
5056
5141
|
reserve=reserve.strip(),
|
|
5057
5142
|
amount_ui=amount_ui.strip(),
|
|
5143
|
+
obligation_address=obligation_address.strip() if isinstance(obligation_address, str) and obligation_address.strip() else None,
|
|
5144
|
+
approved_preview=execute_preview,
|
|
5058
5145
|
)
|
|
5059
5146
|
return AgentToolResult(
|
|
5060
5147
|
tool=tool_name,
|
|
@@ -5248,7 +5335,11 @@ class OpenClawWalletAdapter:
|
|
|
5248
5335
|
input_mint = args.get("input_mint")
|
|
5249
5336
|
output_mint = args.get("output_mint")
|
|
5250
5337
|
amount = args.get("amount")
|
|
5251
|
-
slippage_bps = args.get("slippage_bps",
|
|
5338
|
+
slippage_bps = args.get("slippage_bps", 300)
|
|
5339
|
+
minimum_output_amount_raw = args.get("minimum_output_amount_raw")
|
|
5340
|
+
max_fee_lamports = args.get("max_fee_lamports")
|
|
5341
|
+
valid_for_seconds = args.get("valid_for_seconds", 120)
|
|
5342
|
+
max_attempts = args.get("max_attempts", 3)
|
|
5252
5343
|
mode = args.get("mode")
|
|
5253
5344
|
purpose = args.get("purpose")
|
|
5254
5345
|
user_intent = args.get("user_intent", False)
|
|
@@ -5262,10 +5353,28 @@ class OpenClawWalletAdapter:
|
|
|
5262
5353
|
raise WalletBackendError("amount must be a positive number.")
|
|
5263
5354
|
if not isinstance(slippage_bps, int) or slippage_bps <= 0:
|
|
5264
5355
|
raise WalletBackendError("slippage_bps must be a positive integer.")
|
|
5265
|
-
if
|
|
5266
|
-
|
|
5356
|
+
if minimum_output_amount_raw is not None and (
|
|
5357
|
+
not isinstance(minimum_output_amount_raw, int) or minimum_output_amount_raw <= 0
|
|
5358
|
+
):
|
|
5359
|
+
raise WalletBackendError("minimum_output_amount_raw must be a positive integer when provided.")
|
|
5360
|
+
if max_fee_lamports is not None and (
|
|
5361
|
+
not isinstance(max_fee_lamports, int) or max_fee_lamports < 0
|
|
5362
|
+
):
|
|
5363
|
+
raise WalletBackendError("max_fee_lamports must be a non-negative integer when provided.")
|
|
5364
|
+
if not isinstance(valid_for_seconds, int) or valid_for_seconds <= 0 or valid_for_seconds > 120:
|
|
5365
|
+
raise WalletBackendError("valid_for_seconds must be an integer between 1 and 120.")
|
|
5366
|
+
if not isinstance(max_attempts, int) or max_attempts <= 0 or max_attempts > 5:
|
|
5367
|
+
raise WalletBackendError("max_attempts must be an integer between 1 and 5.")
|
|
5368
|
+
if mode not in {"preview", "prepare", "execute", "intent_preview", "intent_execute"}:
|
|
5369
|
+
raise WalletBackendError(
|
|
5370
|
+
"mode must be 'preview', 'prepare', 'execute', 'intent_preview' or 'intent_execute'."
|
|
5371
|
+
)
|
|
5267
5372
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
5268
5373
|
raise WalletBackendError("purpose is required.")
|
|
5374
|
+
if mode in {"intent_preview", "intent_execute"}:
|
|
5375
|
+
slippage_bps = max(slippage_bps, 300)
|
|
5376
|
+
valid_for_seconds = max(valid_for_seconds, 120)
|
|
5377
|
+
max_attempts = max(max_attempts, 3)
|
|
5269
5378
|
|
|
5270
5379
|
if mode == "preview":
|
|
5271
5380
|
preview = await self.backend.preview_swap(
|
|
@@ -5284,6 +5393,27 @@ class OpenClawWalletAdapter:
|
|
|
5284
5393
|
),
|
|
5285
5394
|
)
|
|
5286
5395
|
|
|
5396
|
+
if mode == "intent_preview":
|
|
5397
|
+
intent_preview = await self.backend.preview_swap_intent(
|
|
5398
|
+
input_mint=input_mint.strip(),
|
|
5399
|
+
output_mint=output_mint.strip(),
|
|
5400
|
+
amount_ui=float(amount),
|
|
5401
|
+
slippage_bps=slippage_bps,
|
|
5402
|
+
minimum_output_amount_raw=minimum_output_amount_raw,
|
|
5403
|
+
max_fee_lamports=max_fee_lamports,
|
|
5404
|
+
valid_for_seconds=valid_for_seconds,
|
|
5405
|
+
max_attempts=max_attempts,
|
|
5406
|
+
)
|
|
5407
|
+
return AgentToolResult(
|
|
5408
|
+
tool=tool_name,
|
|
5409
|
+
ok=True,
|
|
5410
|
+
data=self._annotate_sensitive_payload(
|
|
5411
|
+
intent_preview,
|
|
5412
|
+
action_label="Swap intent",
|
|
5413
|
+
mode="preview",
|
|
5414
|
+
),
|
|
5415
|
+
)
|
|
5416
|
+
|
|
5287
5417
|
if mode == "prepare":
|
|
5288
5418
|
self._require_prepare_intent(user_intent)
|
|
5289
5419
|
preview = await self.backend.preview_swap(
|
|
@@ -5305,6 +5435,103 @@ class OpenClawWalletAdapter:
|
|
|
5305
5435
|
),
|
|
5306
5436
|
)
|
|
5307
5437
|
|
|
5438
|
+
if mode == "intent_execute":
|
|
5439
|
+
approval_payload = inspect_approval_token(
|
|
5440
|
+
approval_token,
|
|
5441
|
+
tool_name=tool_name,
|
|
5442
|
+
network=str(getattr(self.backend, "network", "unknown")),
|
|
5443
|
+
require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
|
|
5444
|
+
)
|
|
5445
|
+
approval_summary = approval_payload.get("binding", {}).get("summary")
|
|
5446
|
+
if not isinstance(approval_summary, dict):
|
|
5447
|
+
raise WalletBackendError(
|
|
5448
|
+
"approval_token does not match the requested operation. Generate a new intent preview and approval before execute."
|
|
5449
|
+
)
|
|
5450
|
+
expected_summary = {
|
|
5451
|
+
"operation": "Swap intent",
|
|
5452
|
+
"network": str(getattr(self.backend, "network", "unknown")),
|
|
5453
|
+
"input_mint": input_mint.strip(),
|
|
5454
|
+
"output_mint": output_mint.strip(),
|
|
5455
|
+
}
|
|
5456
|
+
for key, expected_value in expected_summary.items():
|
|
5457
|
+
if approval_summary.get(key) != expected_value:
|
|
5458
|
+
raise WalletBackendError(
|
|
5459
|
+
"approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
|
|
5460
|
+
)
|
|
5461
|
+
current_owner = await self.backend.get_address()
|
|
5462
|
+
approved_owner = approval_summary.get("owner")
|
|
5463
|
+
if approved_owner and current_owner and str(approved_owner) != str(current_owner):
|
|
5464
|
+
raise WalletBackendError(
|
|
5465
|
+
"approval_token does not match the active wallet owner. Generate a fresh intent preview and approval before execute."
|
|
5466
|
+
)
|
|
5467
|
+
try:
|
|
5468
|
+
approved_amount = float(approval_summary.get("input_amount_ui"))
|
|
5469
|
+
approved_slippage = int(
|
|
5470
|
+
approval_summary.get("max_slippage_bps")
|
|
5471
|
+
if approval_summary.get("max_slippage_bps") is not None
|
|
5472
|
+
else approval_summary.get("slippage_bps")
|
|
5473
|
+
)
|
|
5474
|
+
except (TypeError, ValueError):
|
|
5475
|
+
raise WalletBackendError(
|
|
5476
|
+
"approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
|
|
5477
|
+
)
|
|
5478
|
+
if approved_amount != float(amount) or approved_slippage != slippage_bps:
|
|
5479
|
+
raise WalletBackendError(
|
|
5480
|
+
"approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
|
|
5481
|
+
)
|
|
5482
|
+
if approval_summary.get("recipient_policy") != "owner-only":
|
|
5483
|
+
raise WalletBackendError("approved swap intent recipient policy is invalid.")
|
|
5484
|
+
if approval_summary.get("spend_policy") != "exact-input":
|
|
5485
|
+
raise WalletBackendError("approved swap intent spend policy is invalid.")
|
|
5486
|
+
|
|
5487
|
+
approval_summary_copy = dict(approval_summary)
|
|
5488
|
+
self._require_execute_approval(
|
|
5489
|
+
approval_token=approval_token,
|
|
5490
|
+
tool_name=tool_name,
|
|
5491
|
+
summary=approval_summary_copy,
|
|
5492
|
+
action_label="Swap intent",
|
|
5493
|
+
)
|
|
5494
|
+
try:
|
|
5495
|
+
approved_min_output_raw = (
|
|
5496
|
+
int(approval_summary_copy["minimum_output_amount_raw"])
|
|
5497
|
+
if approval_summary_copy.get("minimum_output_amount_raw") is not None
|
|
5498
|
+
else None
|
|
5499
|
+
)
|
|
5500
|
+
approved_max_fee_lamports = (
|
|
5501
|
+
int(approval_summary_copy["max_fee_lamports"])
|
|
5502
|
+
if approval_summary_copy.get("max_fee_lamports") is not None
|
|
5503
|
+
else None
|
|
5504
|
+
)
|
|
5505
|
+
approved_valid_until = (
|
|
5506
|
+
int(approval_summary_copy["valid_until_epoch_seconds"])
|
|
5507
|
+
if approval_summary_copy.get("valid_until_epoch_seconds") is not None
|
|
5508
|
+
else None
|
|
5509
|
+
)
|
|
5510
|
+
approved_max_attempts = int(approval_summary_copy.get("max_attempts") or max_attempts)
|
|
5511
|
+
except (TypeError, ValueError):
|
|
5512
|
+
raise WalletBackendError(
|
|
5513
|
+
"approval_token does not contain valid swap intent limits. Generate a fresh intent preview and approval before execute."
|
|
5514
|
+
)
|
|
5515
|
+
result = await self.backend.execute_swap_intent(
|
|
5516
|
+
input_mint=input_mint.strip(),
|
|
5517
|
+
output_mint=output_mint.strip(),
|
|
5518
|
+
amount_ui=float(amount),
|
|
5519
|
+
slippage_bps=slippage_bps,
|
|
5520
|
+
minimum_output_amount_raw=approved_min_output_raw,
|
|
5521
|
+
max_fee_lamports=approved_max_fee_lamports,
|
|
5522
|
+
valid_until_epoch_seconds=approved_valid_until,
|
|
5523
|
+
max_attempts=approved_max_attempts,
|
|
5524
|
+
)
|
|
5525
|
+
return AgentToolResult(
|
|
5526
|
+
tool=tool_name,
|
|
5527
|
+
ok=True,
|
|
5528
|
+
data=self._annotate_sensitive_payload(
|
|
5529
|
+
result,
|
|
5530
|
+
action_label="Swap",
|
|
5531
|
+
mode="execute",
|
|
5532
|
+
),
|
|
5533
|
+
)
|
|
5534
|
+
|
|
5308
5535
|
approval_payload = inspect_approval_token(
|
|
5309
5536
|
approval_token,
|
|
5310
5537
|
tool_name=tool_name,
|
|
@@ -93,6 +93,7 @@ def _apply_config_overrides(config: dict[str, Any]) -> None:
|
|
|
93
93
|
),
|
|
94
94
|
"openclawHome": ("OPENCLAW_HOME", config.get("openclawHome"), True),
|
|
95
95
|
"jupiterBaseUrl": ("JUPITER_API_BASE_URL", config.get("jupiterBaseUrl"), True),
|
|
96
|
+
"jupiterSwapV2BaseUrl": ("JUPITER_SWAP_V2_API_BASE_URL", config.get("jupiterSwapV2BaseUrl"), True),
|
|
96
97
|
"jupiterUltraBaseUrl": ("JUPITER_ULTRA_API_BASE_URL", config.get("jupiterUltraBaseUrl"), True),
|
|
97
98
|
"jupiterPriceBaseUrl": ("JUPITER_PRICE_API_BASE_URL", config.get("jupiterPriceBaseUrl"), True),
|
|
98
99
|
"jupiterPortfolioBaseUrl": (
|
|
@@ -10,6 +10,8 @@ from agent_wallet.config import settings
|
|
|
10
10
|
from agent_wallet.exceptions import ProviderError
|
|
11
11
|
from agent_wallet.http_client import get_client
|
|
12
12
|
|
|
13
|
+
JUPITER_SWAP_FALLBACK_PRIORITY_MAX_LAMPORTS = 2_000_000
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def _headers() -> dict[str, str]:
|
|
15
17
|
headers = {"Accept": "application/json"}
|
|
@@ -91,6 +93,39 @@ def _direct_jupiter_enabled() -> bool:
|
|
|
91
93
|
return bool(settings.jupiter_api_key.strip())
|
|
92
94
|
|
|
93
95
|
|
|
96
|
+
def _swap_fallback_prioritization_fee() -> dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"priorityLevelWithMaxLamports": {
|
|
99
|
+
"priorityLevel": "veryHigh",
|
|
100
|
+
"maxLamports": JUPITER_SWAP_FALLBACK_PRIORITY_MAX_LAMPORTS,
|
|
101
|
+
"global": False,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _swap_fallback_build_body(
|
|
107
|
+
*,
|
|
108
|
+
user_public_key: str,
|
|
109
|
+
quote_response: dict[str, Any],
|
|
110
|
+
wrap_and_unwrap_sol: bool,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
return {
|
|
113
|
+
"userPublicKey": user_public_key,
|
|
114
|
+
"quoteResponse": quote_response,
|
|
115
|
+
"wrapAndUnwrapSol": wrap_and_unwrap_sol,
|
|
116
|
+
"dynamicComputeUnitLimit": True,
|
|
117
|
+
"dynamicSlippage": True,
|
|
118
|
+
"prioritizationFeeLamports": _swap_fallback_prioritization_fee(),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _swap_v2_base_url() -> str:
|
|
123
|
+
return os.getenv(
|
|
124
|
+
"JUPITER_SWAP_V2_API_BASE_URL",
|
|
125
|
+
settings.jupiter_swap_v2_api_base_url,
|
|
126
|
+
).strip().rstrip("/")
|
|
127
|
+
|
|
128
|
+
|
|
94
129
|
def _unwrap_gateway_payload(
|
|
95
130
|
status_code: int,
|
|
96
131
|
payload: Any,
|
|
@@ -367,6 +402,52 @@ async def fetch_ultra_order(
|
|
|
367
402
|
return data
|
|
368
403
|
|
|
369
404
|
|
|
405
|
+
async def fetch_swap_v2_order(
|
|
406
|
+
*,
|
|
407
|
+
input_mint: str,
|
|
408
|
+
output_mint: str,
|
|
409
|
+
amount_raw: int,
|
|
410
|
+
taker: str,
|
|
411
|
+
slippage_bps: int | str | None = None,
|
|
412
|
+
exclude_routers: list[str] | None = None,
|
|
413
|
+
swap_mode: str = "ExactIn",
|
|
414
|
+
) -> dict[str, Any]:
|
|
415
|
+
"""Fetch a Jupiter Swap API V2 meta-aggregator order."""
|
|
416
|
+
client = get_client()
|
|
417
|
+
params: dict[str, Any] = {
|
|
418
|
+
"inputMint": input_mint,
|
|
419
|
+
"outputMint": output_mint,
|
|
420
|
+
"amount": str(amount_raw),
|
|
421
|
+
"taker": taker,
|
|
422
|
+
}
|
|
423
|
+
if swap_mode != "ExactIn":
|
|
424
|
+
params["swapMode"] = swap_mode
|
|
425
|
+
if slippage_bps is not None:
|
|
426
|
+
params["slippageBps"] = str(slippage_bps)
|
|
427
|
+
if exclude_routers:
|
|
428
|
+
params["excludeRouters"] = ",".join(str(item).strip() for item in exclude_routers if str(item).strip())
|
|
429
|
+
|
|
430
|
+
response = await client.get(
|
|
431
|
+
f"{_swap_v2_base_url()}/order",
|
|
432
|
+
params=params,
|
|
433
|
+
headers=_headers(),
|
|
434
|
+
)
|
|
435
|
+
if response.status_code != 200:
|
|
436
|
+
raise ProviderError("jupiter-v2", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
437
|
+
data = response.json()
|
|
438
|
+
if not isinstance(data, dict):
|
|
439
|
+
raise ProviderError("jupiter-v2", "Unexpected order response from Jupiter Swap V2.")
|
|
440
|
+
if data.get("error") or data.get("errorCode"):
|
|
441
|
+
raise ProviderError(
|
|
442
|
+
"jupiter-v2",
|
|
443
|
+
str(data.get("error") or data.get("errorCode") or "Unknown Swap V2 order error."),
|
|
444
|
+
details=data,
|
|
445
|
+
)
|
|
446
|
+
if "outAmount" not in data:
|
|
447
|
+
raise ProviderError("jupiter-v2", "Unexpected order response from Jupiter Swap V2.")
|
|
448
|
+
return data
|
|
449
|
+
|
|
450
|
+
|
|
370
451
|
async def build_swap_transaction(
|
|
371
452
|
*,
|
|
372
453
|
user_public_key: str,
|
|
@@ -403,13 +484,11 @@ async def _build_swap_direct(
|
|
|
403
484
|
) -> dict[str, Any]:
|
|
404
485
|
"""Build a swap transaction directly via Jupiter API."""
|
|
405
486
|
client = get_client()
|
|
406
|
-
body =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
"prioritizationFeeLamports": "auto",
|
|
412
|
-
}
|
|
487
|
+
body = _swap_fallback_build_body(
|
|
488
|
+
user_public_key=user_public_key,
|
|
489
|
+
quote_response=quote_response,
|
|
490
|
+
wrap_and_unwrap_sol=wrap_and_unwrap_sol,
|
|
491
|
+
)
|
|
413
492
|
response = await client.post(
|
|
414
493
|
f"{settings.jupiter_api_base_url.rstrip('/')}/swap",
|
|
415
494
|
json=body,
|
|
@@ -430,13 +509,11 @@ async def _build_swap_via_gateway(
|
|
|
430
509
|
wrap_and_unwrap_sol: bool = True,
|
|
431
510
|
) -> dict[str, Any]:
|
|
432
511
|
"""Build a swap transaction via provider gateway (uses API key)."""
|
|
433
|
-
body =
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
"prioritizationFeeLamports": "auto",
|
|
439
|
-
}
|
|
512
|
+
body = _swap_fallback_build_body(
|
|
513
|
+
user_public_key=user_public_key,
|
|
514
|
+
quote_response=quote_response,
|
|
515
|
+
wrap_and_unwrap_sol=wrap_and_unwrap_sol,
|
|
516
|
+
)
|
|
440
517
|
status_code, payload = await _gateway_post("swap", body=body)
|
|
441
518
|
if status_code != 200:
|
|
442
519
|
error_msg = payload if isinstance(payload, str) else json.dumps(payload)
|
|
@@ -477,6 +554,42 @@ async def execute_ultra_order(
|
|
|
477
554
|
return data
|
|
478
555
|
|
|
479
556
|
|
|
557
|
+
async def execute_swap_v2_order(
|
|
558
|
+
*,
|
|
559
|
+
signed_transaction_base64: str,
|
|
560
|
+
request_id: str,
|
|
561
|
+
last_valid_block_height: int | str | None = None,
|
|
562
|
+
) -> dict[str, Any]:
|
|
563
|
+
"""Execute a signed Jupiter Swap API V2 order."""
|
|
564
|
+
client = get_client()
|
|
565
|
+
body: dict[str, Any] = {
|
|
566
|
+
"signedTransaction": signed_transaction_base64,
|
|
567
|
+
"requestId": request_id,
|
|
568
|
+
}
|
|
569
|
+
if last_valid_block_height is not None:
|
|
570
|
+
body["lastValidBlockHeight"] = str(last_valid_block_height)
|
|
571
|
+
response = await client.post(
|
|
572
|
+
f"{_swap_v2_base_url()}/execute",
|
|
573
|
+
json=body,
|
|
574
|
+
headers={**_headers(), "Content-Type": "application/json"},
|
|
575
|
+
)
|
|
576
|
+
if response.status_code != 200:
|
|
577
|
+
raise ProviderError("jupiter-v2", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
578
|
+
data = response.json()
|
|
579
|
+
if not isinstance(data, dict):
|
|
580
|
+
raise ProviderError("jupiter-v2", "Unexpected execute response from Jupiter Swap V2.")
|
|
581
|
+
if data.get("error") or data.get("errorCode"):
|
|
582
|
+
raise ProviderError(
|
|
583
|
+
"jupiter-v2",
|
|
584
|
+
str(data.get("error") or data.get("errorCode") or "Unknown Swap V2 execute error."),
|
|
585
|
+
details=data,
|
|
586
|
+
)
|
|
587
|
+
if str(data.get("status") or "").strip().lower() == "failed":
|
|
588
|
+
message = data.get("error") or data.get("code") or "Swap V2 execute failed."
|
|
589
|
+
raise ProviderError("jupiter-v2", str(message), details=data)
|
|
590
|
+
return data
|
|
591
|
+
|
|
592
|
+
|
|
480
593
|
async def fetch_prices(
|
|
481
594
|
*,
|
|
482
595
|
mints: list[str],
|
|
@@ -8,6 +8,8 @@ from agent_wallet.config import settings
|
|
|
8
8
|
from agent_wallet.exceptions import ProviderError
|
|
9
9
|
from agent_wallet.http_client import get_client
|
|
10
10
|
|
|
11
|
+
KAMINO_BUILD_TIMEOUT_SECONDS = 20.0
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
def _normalized_api_base() -> str:
|
|
13
15
|
return settings.kamino_api_base_url.rstrip("/")
|
|
@@ -140,6 +142,7 @@ async def build_lend_deposit_transaction(
|
|
|
140
142
|
"reserve": reserve,
|
|
141
143
|
"amount": amount_ui,
|
|
142
144
|
},
|
|
145
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
143
146
|
)
|
|
144
147
|
if response.status_code != 200:
|
|
145
148
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -163,6 +166,7 @@ async def build_lend_withdraw_transaction(
|
|
|
163
166
|
"reserve": reserve,
|
|
164
167
|
"amount": amount_ui,
|
|
165
168
|
},
|
|
169
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
166
170
|
)
|
|
167
171
|
if response.status_code != 200:
|
|
168
172
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -186,6 +190,7 @@ async def build_lend_borrow_transaction(
|
|
|
186
190
|
"reserve": reserve,
|
|
187
191
|
"amount": amount_ui,
|
|
188
192
|
},
|
|
193
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
189
194
|
)
|
|
190
195
|
if response.status_code != 200:
|
|
191
196
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -209,6 +214,7 @@ async def build_lend_repay_transaction(
|
|
|
209
214
|
"reserve": reserve,
|
|
210
215
|
"amount": amount_ui,
|
|
211
216
|
},
|
|
217
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
212
218
|
)
|
|
213
219
|
if response.status_code != 200:
|
|
214
220
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
@@ -718,6 +718,7 @@ def verify_provider_kamino_lend_transaction(
|
|
|
718
718
|
market_address: str,
|
|
719
719
|
reserve_address: str,
|
|
720
720
|
action: str,
|
|
721
|
+
obligation_address: str | None = None,
|
|
721
722
|
loaded_addresses: list[str] | None = None,
|
|
722
723
|
) -> dict[str, Any]:
|
|
723
724
|
binding = _assert_basic_wallet_binding(
|
|
@@ -734,6 +735,10 @@ def verify_provider_kamino_lend_transaction(
|
|
|
734
735
|
raise WalletBackendError(
|
|
735
736
|
f"{action} transaction does not reference the expected Kamino reserve."
|
|
736
737
|
)
|
|
738
|
+
if obligation_address and obligation_address not in keys:
|
|
739
|
+
raise WalletBackendError(
|
|
740
|
+
f"{action} transaction does not reference the expected Kamino obligation."
|
|
741
|
+
)
|
|
737
742
|
program_ids = _program_ids(message, loaded_addresses)
|
|
738
743
|
unknown_program_ids = _assert_program_allowlist(
|
|
739
744
|
program_ids,
|
|
@@ -764,6 +769,7 @@ def verify_provider_kamino_lend_transaction(
|
|
|
764
769
|
"instruction_count": len(_compiled_instructions(message)),
|
|
765
770
|
"market_address": market_address,
|
|
766
771
|
"reserve_address": reserve_address,
|
|
772
|
+
"obligation_address": obligation_address,
|
|
767
773
|
"action": action,
|
|
768
774
|
"verified": True,
|
|
769
775
|
}
|