@agentlayer.tech/wallet 0.1.25 → 0.1.26
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 +32 -9
- package/.openclaw/extensions/agent-wallet/index.ts +32 -9
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +4 -1
- package/CHANGELOG.md +14 -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 +187 -7
- package/agent-wallet/agent_wallet/openclaw_cli.py +1 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +89 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +111 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +298 -23
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +6 -2
- package/package.json +2 -2
|
@@ -17,7 +17,7 @@ In practice this means the agent works through explicit tools for:
|
|
|
17
17
|
- EVM native balance, ERC-20 balance/metadata, fee-rate, receipt, Velora swap quote/execute, Aave V3 account/reserve/position flows, and transfer flows through the local `wdk-evm-wallet` backend
|
|
18
18
|
- wallet address, balances, and portfolio reads
|
|
19
19
|
- native SOL and SPL token transfers
|
|
20
|
-
- Jupiter swap and price lookup
|
|
20
|
+
- Jupiter swap and price lookup, including Solana swap intent execution that refreshes quotes inside user-approved limits
|
|
21
21
|
- Jupiter Earn read/deposit/withdraw flows
|
|
22
22
|
- Kamino lending read/deposit/withdraw/borrow/repay flows
|
|
23
23
|
- native Solana staking, stake deactivation, and stake withdrawal
|
|
@@ -76,7 +76,7 @@ function cachePreviewForApproval(userId, toolName, payload) {
|
|
|
76
76
|
const cacheToolName = approvalPreviewToolName(toolName);
|
|
77
77
|
if (!payload || payload.ok !== true || !payload.data || typeof payload.data !== "object") return;
|
|
78
78
|
const approvalSource = payload.data;
|
|
79
|
-
if (!["preview", "prepare"].includes(String(approvalSource.mode || ""))) return;
|
|
79
|
+
if (!["preview", "prepare", "intent_preview"].includes(String(approvalSource.mode || ""))) return;
|
|
80
80
|
if (!approvalSource.confirmation_summary || typeof approvalSource.confirmation_summary !== "object") return;
|
|
81
81
|
pruneApprovalPreviewCache();
|
|
82
82
|
const digest = previewDigest(approvalSource);
|
|
@@ -119,7 +119,21 @@ function cachedPreviewForToken(userId, toolName, token) {
|
|
|
119
119
|
return cached.preview && typeof cached.preview === "object" ? cached.preview : null;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function
|
|
122
|
+
function isSolanaSwapIntentPayload(payload) {
|
|
123
|
+
return (
|
|
124
|
+
payload &&
|
|
125
|
+
typeof payload === "object" &&
|
|
126
|
+
(String(payload.asset_type || "") === "solana-swap-intent" ||
|
|
127
|
+
String(payload.mode || "") === "intent_preview")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isSolanaSwapIntentExecute(params) {
|
|
132
|
+
return String(params?.mode || "") === "intent_execute";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function requiresApprovedPreviewPayload(toolName, params = null) {
|
|
136
|
+
if (toolName === "swap_solana_tokens" && isSolanaSwapIntentExecute(params)) return false;
|
|
123
137
|
return PREVIEW_BOUND_SWAP_TOOLS.has(toolName);
|
|
124
138
|
}
|
|
125
139
|
|
|
@@ -536,7 +550,7 @@ async function issueApprovalToken(api, config, userId, toolName, previewPayload)
|
|
|
536
550
|
if (!summary || typeof summary !== "object") {
|
|
537
551
|
throw new Error(`No confirmation_summary available for ${toolName}.`);
|
|
538
552
|
}
|
|
539
|
-
const summaryForToken = PREVIEW_BOUND_SWAP_TOOLS.has(toolName)
|
|
553
|
+
const summaryForToken = PREVIEW_BOUND_SWAP_TOOLS.has(toolName) && !isSolanaSwapIntentPayload(previewPayload)
|
|
540
554
|
? { ...summary, _preview_digest: previewDigest(previewPayload) }
|
|
541
555
|
: { ...summary };
|
|
542
556
|
const extraArgs = [
|
|
@@ -560,7 +574,12 @@ async function issueApprovalToken(api, config, userId, toolName, previewPayload)
|
|
|
560
574
|
}
|
|
561
575
|
|
|
562
576
|
async function attachApprovalForExecute(api, config, userId, toolName, effectiveParams) {
|
|
563
|
-
if (String(effectiveParams.mode || "")
|
|
577
|
+
if (!["execute", "intent_execute"].includes(String(effectiveParams.mode || ""))) return null;
|
|
578
|
+
if (toolName === "swap_solana_tokens" && String(effectiveParams.mode || "") === "execute") {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"Legacy exact-preview execute is disabled for Solana Jupiter swaps in OpenClaw. Use intent_preview, ask for explicit chat confirmation, then call intent_execute. The intent path binds approval to risk limits instead of a fragile Jupiter quote payload."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
564
583
|
|
|
565
584
|
const cached = latestCachedPreview(userId, toolName);
|
|
566
585
|
if (cached?.preview && cached?.summary) {
|
|
@@ -571,14 +590,14 @@ async function attachApprovalForExecute(api, config, userId, toolName, effective
|
|
|
571
590
|
toolName,
|
|
572
591
|
cached.preview
|
|
573
592
|
);
|
|
574
|
-
if (requiresApprovedPreviewPayload(toolName)) {
|
|
593
|
+
if (requiresApprovedPreviewPayload(toolName, effectiveParams)) {
|
|
575
594
|
effectiveParams._approved_preview = cached.preview;
|
|
576
595
|
}
|
|
577
596
|
return cached;
|
|
578
597
|
}
|
|
579
598
|
|
|
580
599
|
if (
|
|
581
|
-
requiresApprovedPreviewPayload(toolName) &&
|
|
600
|
+
requiresApprovedPreviewPayload(toolName, effectiveParams) &&
|
|
582
601
|
typeof effectiveParams.approval_token === "string" &&
|
|
583
602
|
effectiveParams.approval_token.trim() &&
|
|
584
603
|
effectiveParams._approved_preview === undefined
|
|
@@ -1312,7 +1331,7 @@ const solanaToolDefinitions = [
|
|
|
1312
1331
|
},
|
|
1313
1332
|
{
|
|
1314
1333
|
name: "swap_solana_tokens",
|
|
1315
|
-
description: `Preview
|
|
1334
|
+
description: `Preview or execute a Solana token swap via Jupiter. Use intent_preview followed by intent_execute after the user explicitly confirms the intent summary in chat; intent_execute fetches a fresh quote and only executes inside the approved limits. Do not use legacy execute for Solana swaps. The OpenClaw plugin handles internal execution authorization automatically. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
1316
1335
|
optional: true,
|
|
1317
1336
|
parameters: {
|
|
1318
1337
|
type: "object",
|
|
@@ -1320,8 +1339,12 @@ const solanaToolDefinitions = [
|
|
|
1320
1339
|
input_mint: { type: "string" },
|
|
1321
1340
|
output_mint: { type: "string" },
|
|
1322
1341
|
amount: { type: "number" },
|
|
1323
|
-
slippage_bps: { type: "integer" },
|
|
1324
|
-
|
|
1342
|
+
slippage_bps: { type: "integer", description: "Optional slippage tolerance in basis points. Defaults to 300 (3%) for Solana swaps." },
|
|
1343
|
+
minimum_output_amount_raw: { type: "integer", 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." },
|
|
1344
|
+
max_fee_lamports: { type: "integer" },
|
|
1345
|
+
valid_for_seconds: { type: "integer", description: "Optional intent validity window in seconds. Intent swaps use at least 120 seconds, max 120." },
|
|
1346
|
+
max_attempts: { type: "integer", description: "Optional number of fresh quote/simulate/execute attempts. Intent swaps use at least 3 attempts, max 5." },
|
|
1347
|
+
mode: { type: "string", enum: ["preview", "intent_preview", "intent_execute"] },
|
|
1325
1348
|
purpose: { type: "string" },
|
|
1326
1349
|
user_intent: { type: "boolean" },
|
|
1327
1350
|
},
|
|
@@ -76,7 +76,7 @@ function cachePreviewForApproval(userId, toolName, payload) {
|
|
|
76
76
|
const cacheToolName = approvalPreviewToolName(toolName);
|
|
77
77
|
if (!payload || payload.ok !== true || !payload.data || typeof payload.data !== "object") return;
|
|
78
78
|
const approvalSource = payload.data;
|
|
79
|
-
if (!["preview", "prepare"].includes(String(approvalSource.mode || ""))) return;
|
|
79
|
+
if (!["preview", "prepare", "intent_preview"].includes(String(approvalSource.mode || ""))) return;
|
|
80
80
|
if (!approvalSource.confirmation_summary || typeof approvalSource.confirmation_summary !== "object") return;
|
|
81
81
|
pruneApprovalPreviewCache();
|
|
82
82
|
const digest = previewDigest(approvalSource);
|
|
@@ -119,7 +119,21 @@ function cachedPreviewForToken(userId, toolName, token) {
|
|
|
119
119
|
return cached.preview && typeof cached.preview === "object" ? cached.preview : null;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function
|
|
122
|
+
function isSolanaSwapIntentPayload(payload) {
|
|
123
|
+
return (
|
|
124
|
+
payload &&
|
|
125
|
+
typeof payload === "object" &&
|
|
126
|
+
(String(payload.asset_type || "") === "solana-swap-intent" ||
|
|
127
|
+
String(payload.mode || "") === "intent_preview")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isSolanaSwapIntentExecute(params) {
|
|
132
|
+
return String(params?.mode || "") === "intent_execute";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function requiresApprovedPreviewPayload(toolName, params = null) {
|
|
136
|
+
if (toolName === "swap_solana_tokens" && isSolanaSwapIntentExecute(params)) return false;
|
|
123
137
|
return PREVIEW_BOUND_SWAP_TOOLS.has(toolName);
|
|
124
138
|
}
|
|
125
139
|
|
|
@@ -536,7 +550,7 @@ async function issueApprovalToken(api, config, userId, toolName, previewPayload)
|
|
|
536
550
|
if (!summary || typeof summary !== "object") {
|
|
537
551
|
throw new Error(`No confirmation_summary available for ${toolName}.`);
|
|
538
552
|
}
|
|
539
|
-
const summaryForToken = PREVIEW_BOUND_SWAP_TOOLS.has(toolName)
|
|
553
|
+
const summaryForToken = PREVIEW_BOUND_SWAP_TOOLS.has(toolName) && !isSolanaSwapIntentPayload(previewPayload)
|
|
540
554
|
? { ...summary, _preview_digest: previewDigest(previewPayload) }
|
|
541
555
|
: { ...summary };
|
|
542
556
|
const extraArgs = [
|
|
@@ -560,7 +574,12 @@ async function issueApprovalToken(api, config, userId, toolName, previewPayload)
|
|
|
560
574
|
}
|
|
561
575
|
|
|
562
576
|
async function attachApprovalForExecute(api, config, userId, toolName, effectiveParams) {
|
|
563
|
-
if (String(effectiveParams.mode || "")
|
|
577
|
+
if (!["execute", "intent_execute"].includes(String(effectiveParams.mode || ""))) return null;
|
|
578
|
+
if (toolName === "swap_solana_tokens" && String(effectiveParams.mode || "") === "execute") {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"Legacy exact-preview execute is disabled for Solana Jupiter swaps in OpenClaw. Use intent_preview, ask for explicit chat confirmation, then call intent_execute. The intent path binds approval to risk limits instead of a fragile Jupiter quote payload."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
564
583
|
|
|
565
584
|
const cached = latestCachedPreview(userId, toolName);
|
|
566
585
|
if (cached?.preview && cached?.summary) {
|
|
@@ -571,14 +590,14 @@ async function attachApprovalForExecute(api, config, userId, toolName, effective
|
|
|
571
590
|
toolName,
|
|
572
591
|
cached.preview
|
|
573
592
|
);
|
|
574
|
-
if (requiresApprovedPreviewPayload(toolName)) {
|
|
593
|
+
if (requiresApprovedPreviewPayload(toolName, effectiveParams)) {
|
|
575
594
|
effectiveParams._approved_preview = cached.preview;
|
|
576
595
|
}
|
|
577
596
|
return cached;
|
|
578
597
|
}
|
|
579
598
|
|
|
580
599
|
if (
|
|
581
|
-
requiresApprovedPreviewPayload(toolName) &&
|
|
600
|
+
requiresApprovedPreviewPayload(toolName, effectiveParams) &&
|
|
582
601
|
typeof effectiveParams.approval_token === "string" &&
|
|
583
602
|
effectiveParams.approval_token.trim() &&
|
|
584
603
|
effectiveParams._approved_preview === undefined
|
|
@@ -1312,7 +1331,7 @@ const solanaToolDefinitions = [
|
|
|
1312
1331
|
},
|
|
1313
1332
|
{
|
|
1314
1333
|
name: "swap_solana_tokens",
|
|
1315
|
-
description: `Preview
|
|
1334
|
+
description: `Preview or execute a Solana token swap via Jupiter. Use intent_preview followed by intent_execute after the user explicitly confirms the intent summary in chat; intent_execute fetches a fresh quote and only executes inside the approved limits. Do not use legacy execute for Solana swaps. The OpenClaw plugin handles internal execution authorization automatically. ${WALLET_TOOL_ONLY_GUIDANCE}`,
|
|
1316
1335
|
optional: true,
|
|
1317
1336
|
parameters: {
|
|
1318
1337
|
type: "object",
|
|
@@ -1320,8 +1339,12 @@ const solanaToolDefinitions = [
|
|
|
1320
1339
|
input_mint: { type: "string" },
|
|
1321
1340
|
output_mint: { type: "string" },
|
|
1322
1341
|
amount: { type: "number" },
|
|
1323
|
-
slippage_bps: { type: "integer" },
|
|
1324
|
-
|
|
1342
|
+
slippage_bps: { type: "integer", description: "Optional slippage tolerance in basis points. Defaults to 300 (3%) for Solana swaps." },
|
|
1343
|
+
minimum_output_amount_raw: { type: "integer", 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." },
|
|
1344
|
+
max_fee_lamports: { type: "integer" },
|
|
1345
|
+
valid_for_seconds: { type: "integer", description: "Optional intent validity window in seconds. Intent swaps use at least 120 seconds, max 120." },
|
|
1346
|
+
max_attempts: { type: "integer", description: "Optional number of fresh quote/simulate/execute attempts. Intent swaps use at least 3 attempts, max 5." },
|
|
1347
|
+
mode: { type: "string", enum: ["preview", "intent_preview", "intent_execute"] },
|
|
1325
1348
|
purpose: { type: "string" },
|
|
1326
1349
|
user_intent: { type: "boolean" },
|
|
1327
1350
|
},
|
|
@@ -222,6 +222,10 @@
|
|
|
222
222
|
"type": "string",
|
|
223
223
|
"description": "Optional Jupiter Swap API base URL."
|
|
224
224
|
},
|
|
225
|
+
"jupiterSwapV2BaseUrl": {
|
|
226
|
+
"type": "string",
|
|
227
|
+
"description": "Optional Jupiter Swap API V2 base URL."
|
|
228
|
+
},
|
|
225
229
|
"jupiterUltraBaseUrl": {
|
|
226
230
|
"type": "string",
|
|
227
231
|
"description": "Optional Jupiter Ultra API base URL."
|
|
@@ -11,9 +11,12 @@ Safety rules:
|
|
|
11
11
|
- Use Kamino market/reserve reads before Kamino writes when the user needs lending context.
|
|
12
12
|
- Use Aave account reads before Aave writes when the user needs EVM lending context.
|
|
13
13
|
- For transfers, native staking, swaps, Aave writes, Jupiter Earn writes, and Kamino writes, use `preview` before `prepare` or `execute`.
|
|
14
|
+
- For Solana Jupiter swaps through `swap_solana_tokens`, prefer `intent_preview` then `intent_execute` after explicit chat confirmation. The user confirms risk limits; the backend refreshes the quote and only executes inside those limits.
|
|
15
|
+
- Solana swap intent defaults to at least 300 bps (3%) slippage, 120 seconds validity, and 3 fresh execution attempts. The backend computes the approved minimum output from the indicative output and slippage, clamps hand-tightened minimums to that floor, then executes through Jupiter Swap API V2 `/order` + `/execute` when available.
|
|
16
|
+
- Do not use legacy `execute` for Solana Jupiter swaps in OpenClaw. Exact quote-bound approval is too fragile for active markets and will be rejected by the bridge.
|
|
14
17
|
- For `swap_solana_privately`, use `preview` and then `execute` after explicit user approval. Do not use `prepare` for this tool.
|
|
15
18
|
- Use `prepare` only when the user clearly intends to produce an execution plan.
|
|
16
|
-
- Use `execute` only after the
|
|
19
|
+
- Use `execute` only after the user explicitly confirms the shown summary in chat. OpenClaw handles the internal approval token; do not ask for `/approve`, buttons, popups, or a manual token. For Solana swap intents, the token is bound to the approved intent limits instead of one fragile quote.
|
|
17
20
|
- On `mainnet`, require an approval token that includes explicit mainnet confirmation before any execution.
|
|
18
21
|
- Before any `mainnet` execute, restate the network, operation type, asset, amount, and destination, validator, or stake account.
|
|
19
22
|
- If a preview or prepare result includes `confirmation_summary` or `mainnet_warning`, surface that summary before asking for confirmation.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.1.26 - 2026-05-26
|
|
6
|
+
|
|
7
|
+
- Reworked Solana Jupiter swaps to prefer intent approvals, so OpenClaw confirms
|
|
8
|
+
risk limits and executes against a fresh quote instead of binding approval to
|
|
9
|
+
a fragile exact quote payload.
|
|
10
|
+
- Added Jupiter Swap API V2 `/order` + `/execute` routing with managed landing
|
|
11
|
+
support and fallback routing for Solana swaps.
|
|
12
|
+
- Hardened Solana swap intent defaults to 3% slippage, a 120-second execution
|
|
13
|
+
window, three fresh execution attempts, and safer minimum-output handling.
|
|
14
|
+
- Fixed Jupiter V2 execution payload compatibility by sending
|
|
15
|
+
`lastValidBlockHeight` in the string form expected by the API.
|
|
16
|
+
- Disabled legacy exact-preview Solana swap execute in the OpenClaw bridge to
|
|
17
|
+
prevent stale approval-token mismatches on active markets.
|
|
18
|
+
|
|
5
19
|
## v0.1.24 - 2026-05-23
|
|
6
20
|
|
|
7
21
|
- Fixed the published npm package CLI metadata so
|
package/agent-wallet/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Current safe tools:
|
|
|
88
88
|
- `transfer_sol`
|
|
89
89
|
- `stake_sol_native`
|
|
90
90
|
- `transfer_spl_token`
|
|
91
|
-
- `swap_solana_tokens`
|
|
91
|
+
- `swap_solana_tokens` - Solana Jupiter swaps; prefer `intent_preview` -> chat confirmation -> `intent_execute` so execution refreshes the quote inside approved limits.
|
|
92
92
|
- `swap_solana_privately` - Houdini-backed private Solana payout flow for same-token `SOL->SOL` or `USDC->USDC` transfers to a destination wallet.
|
|
93
93
|
- `get_solana_private_swap_status`
|
|
94
94
|
- `get_jupiter_earn_tokens`
|
|
@@ -45,6 +45,7 @@ class Settings(BaseSettings):
|
|
|
45
45
|
wdk_evm_account_index: int = 0
|
|
46
46
|
|
|
47
47
|
jupiter_api_base_url: str = "https://lite-api.jup.ag/swap/v1"
|
|
48
|
+
jupiter_swap_v2_api_base_url: str = "https://api.jup.ag/swap/v2"
|
|
48
49
|
jupiter_ultra_api_base_url: str = "https://lite-api.jup.ag/ultra/v1"
|
|
49
50
|
jupiter_price_api_base_url: str = "https://lite-api.jup.ag/price/v3"
|
|
50
51
|
jupiter_portfolio_api_base_url: str = "https://api.jup.ag/portfolio/v1"
|
|
@@ -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,
|
|
@@ -2451,7 +2473,9 @@ class OpenClawWalletAdapter:
|
|
|
2451
2473
|
name="swap_solana_tokens",
|
|
2452
2474
|
description=(
|
|
2453
2475
|
"Preview or execute a Solana token swap through Jupiter routing. "
|
|
2454
|
-
"
|
|
2476
|
+
"Prefer intent_preview, then intent_execute after explicit chat confirmation; "
|
|
2477
|
+
"intent_execute fetches a fresh quote and only sends if it remains inside the approved limits. "
|
|
2478
|
+
"OpenClaw should not use legacy execute for Solana swaps because exact Jupiter quote payloads expire quickly."
|
|
2455
2479
|
),
|
|
2456
2480
|
input_schema={
|
|
2457
2481
|
"type": "object",
|
|
@@ -2470,12 +2494,28 @@ class OpenClawWalletAdapter:
|
|
|
2470
2494
|
},
|
|
2471
2495
|
"slippage_bps": {
|
|
2472
2496
|
"type": "integer",
|
|
2473
|
-
"description": "Optional slippage tolerance in basis points. Defaults to
|
|
2497
|
+
"description": "Optional slippage tolerance in basis points. Defaults to 300 (3%) for Solana swaps.",
|
|
2498
|
+
},
|
|
2499
|
+
"minimum_output_amount_raw": {
|
|
2500
|
+
"type": "integer",
|
|
2501
|
+
"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.",
|
|
2502
|
+
},
|
|
2503
|
+
"max_fee_lamports": {
|
|
2504
|
+
"type": "integer",
|
|
2505
|
+
"description": "Optional maximum Solana network fee in lamports for intent_preview.",
|
|
2506
|
+
},
|
|
2507
|
+
"valid_for_seconds": {
|
|
2508
|
+
"type": "integer",
|
|
2509
|
+
"description": "Optional intent validity window in seconds. Intent swaps use at least 120 seconds, max 120.",
|
|
2510
|
+
},
|
|
2511
|
+
"max_attempts": {
|
|
2512
|
+
"type": "integer",
|
|
2513
|
+
"description": "Optional number of fresh quote/simulate/execute attempts. Intent swaps use at least 3 attempts, max 5.",
|
|
2474
2514
|
},
|
|
2475
2515
|
"mode": {
|
|
2476
2516
|
"type": "string",
|
|
2477
|
-
"enum": ["preview", "prepare", "execute"],
|
|
2478
|
-
"description": "
|
|
2517
|
+
"enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
|
|
2518
|
+
"description": "intent_preview returns approved risk limits; intent_execute requotes and executes atomically inside those limits. Legacy preview/prepare/execute remains supported.",
|
|
2479
2519
|
},
|
|
2480
2520
|
"purpose": {
|
|
2481
2521
|
"type": "string",
|
|
@@ -5248,7 +5288,11 @@ class OpenClawWalletAdapter:
|
|
|
5248
5288
|
input_mint = args.get("input_mint")
|
|
5249
5289
|
output_mint = args.get("output_mint")
|
|
5250
5290
|
amount = args.get("amount")
|
|
5251
|
-
slippage_bps = args.get("slippage_bps",
|
|
5291
|
+
slippage_bps = args.get("slippage_bps", 300)
|
|
5292
|
+
minimum_output_amount_raw = args.get("minimum_output_amount_raw")
|
|
5293
|
+
max_fee_lamports = args.get("max_fee_lamports")
|
|
5294
|
+
valid_for_seconds = args.get("valid_for_seconds", 120)
|
|
5295
|
+
max_attempts = args.get("max_attempts", 3)
|
|
5252
5296
|
mode = args.get("mode")
|
|
5253
5297
|
purpose = args.get("purpose")
|
|
5254
5298
|
user_intent = args.get("user_intent", False)
|
|
@@ -5262,10 +5306,28 @@ class OpenClawWalletAdapter:
|
|
|
5262
5306
|
raise WalletBackendError("amount must be a positive number.")
|
|
5263
5307
|
if not isinstance(slippage_bps, int) or slippage_bps <= 0:
|
|
5264
5308
|
raise WalletBackendError("slippage_bps must be a positive integer.")
|
|
5265
|
-
if
|
|
5266
|
-
|
|
5309
|
+
if minimum_output_amount_raw is not None and (
|
|
5310
|
+
not isinstance(minimum_output_amount_raw, int) or minimum_output_amount_raw <= 0
|
|
5311
|
+
):
|
|
5312
|
+
raise WalletBackendError("minimum_output_amount_raw must be a positive integer when provided.")
|
|
5313
|
+
if max_fee_lamports is not None and (
|
|
5314
|
+
not isinstance(max_fee_lamports, int) or max_fee_lamports < 0
|
|
5315
|
+
):
|
|
5316
|
+
raise WalletBackendError("max_fee_lamports must be a non-negative integer when provided.")
|
|
5317
|
+
if not isinstance(valid_for_seconds, int) or valid_for_seconds <= 0 or valid_for_seconds > 120:
|
|
5318
|
+
raise WalletBackendError("valid_for_seconds must be an integer between 1 and 120.")
|
|
5319
|
+
if not isinstance(max_attempts, int) or max_attempts <= 0 or max_attempts > 5:
|
|
5320
|
+
raise WalletBackendError("max_attempts must be an integer between 1 and 5.")
|
|
5321
|
+
if mode not in {"preview", "prepare", "execute", "intent_preview", "intent_execute"}:
|
|
5322
|
+
raise WalletBackendError(
|
|
5323
|
+
"mode must be 'preview', 'prepare', 'execute', 'intent_preview' or 'intent_execute'."
|
|
5324
|
+
)
|
|
5267
5325
|
if not isinstance(purpose, str) or not purpose.strip():
|
|
5268
5326
|
raise WalletBackendError("purpose is required.")
|
|
5327
|
+
if mode in {"intent_preview", "intent_execute"}:
|
|
5328
|
+
slippage_bps = max(slippage_bps, 300)
|
|
5329
|
+
valid_for_seconds = max(valid_for_seconds, 120)
|
|
5330
|
+
max_attempts = max(max_attempts, 3)
|
|
5269
5331
|
|
|
5270
5332
|
if mode == "preview":
|
|
5271
5333
|
preview = await self.backend.preview_swap(
|
|
@@ -5284,6 +5346,27 @@ class OpenClawWalletAdapter:
|
|
|
5284
5346
|
),
|
|
5285
5347
|
)
|
|
5286
5348
|
|
|
5349
|
+
if mode == "intent_preview":
|
|
5350
|
+
intent_preview = await self.backend.preview_swap_intent(
|
|
5351
|
+
input_mint=input_mint.strip(),
|
|
5352
|
+
output_mint=output_mint.strip(),
|
|
5353
|
+
amount_ui=float(amount),
|
|
5354
|
+
slippage_bps=slippage_bps,
|
|
5355
|
+
minimum_output_amount_raw=minimum_output_amount_raw,
|
|
5356
|
+
max_fee_lamports=max_fee_lamports,
|
|
5357
|
+
valid_for_seconds=valid_for_seconds,
|
|
5358
|
+
max_attempts=max_attempts,
|
|
5359
|
+
)
|
|
5360
|
+
return AgentToolResult(
|
|
5361
|
+
tool=tool_name,
|
|
5362
|
+
ok=True,
|
|
5363
|
+
data=self._annotate_sensitive_payload(
|
|
5364
|
+
intent_preview,
|
|
5365
|
+
action_label="Swap intent",
|
|
5366
|
+
mode="preview",
|
|
5367
|
+
),
|
|
5368
|
+
)
|
|
5369
|
+
|
|
5287
5370
|
if mode == "prepare":
|
|
5288
5371
|
self._require_prepare_intent(user_intent)
|
|
5289
5372
|
preview = await self.backend.preview_swap(
|
|
@@ -5305,6 +5388,103 @@ class OpenClawWalletAdapter:
|
|
|
5305
5388
|
),
|
|
5306
5389
|
)
|
|
5307
5390
|
|
|
5391
|
+
if mode == "intent_execute":
|
|
5392
|
+
approval_payload = inspect_approval_token(
|
|
5393
|
+
approval_token,
|
|
5394
|
+
tool_name=tool_name,
|
|
5395
|
+
network=str(getattr(self.backend, "network", "unknown")),
|
|
5396
|
+
require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
|
|
5397
|
+
)
|
|
5398
|
+
approval_summary = approval_payload.get("binding", {}).get("summary")
|
|
5399
|
+
if not isinstance(approval_summary, dict):
|
|
5400
|
+
raise WalletBackendError(
|
|
5401
|
+
"approval_token does not match the requested operation. Generate a new intent preview and approval before execute."
|
|
5402
|
+
)
|
|
5403
|
+
expected_summary = {
|
|
5404
|
+
"operation": "Swap intent",
|
|
5405
|
+
"network": str(getattr(self.backend, "network", "unknown")),
|
|
5406
|
+
"input_mint": input_mint.strip(),
|
|
5407
|
+
"output_mint": output_mint.strip(),
|
|
5408
|
+
}
|
|
5409
|
+
for key, expected_value in expected_summary.items():
|
|
5410
|
+
if approval_summary.get(key) != expected_value:
|
|
5411
|
+
raise WalletBackendError(
|
|
5412
|
+
"approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
|
|
5413
|
+
)
|
|
5414
|
+
current_owner = await self.backend.get_address()
|
|
5415
|
+
approved_owner = approval_summary.get("owner")
|
|
5416
|
+
if approved_owner and current_owner and str(approved_owner) != str(current_owner):
|
|
5417
|
+
raise WalletBackendError(
|
|
5418
|
+
"approval_token does not match the active wallet owner. Generate a fresh intent preview and approval before execute."
|
|
5419
|
+
)
|
|
5420
|
+
try:
|
|
5421
|
+
approved_amount = float(approval_summary.get("input_amount_ui"))
|
|
5422
|
+
approved_slippage = int(
|
|
5423
|
+
approval_summary.get("max_slippage_bps")
|
|
5424
|
+
if approval_summary.get("max_slippage_bps") is not None
|
|
5425
|
+
else approval_summary.get("slippage_bps")
|
|
5426
|
+
)
|
|
5427
|
+
except (TypeError, ValueError):
|
|
5428
|
+
raise WalletBackendError(
|
|
5429
|
+
"approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
|
|
5430
|
+
)
|
|
5431
|
+
if approved_amount != float(amount) or approved_slippage != slippage_bps:
|
|
5432
|
+
raise WalletBackendError(
|
|
5433
|
+
"approval_token does not match the requested swap intent. Generate a fresh intent preview and approval before execute."
|
|
5434
|
+
)
|
|
5435
|
+
if approval_summary.get("recipient_policy") != "owner-only":
|
|
5436
|
+
raise WalletBackendError("approved swap intent recipient policy is invalid.")
|
|
5437
|
+
if approval_summary.get("spend_policy") != "exact-input":
|
|
5438
|
+
raise WalletBackendError("approved swap intent spend policy is invalid.")
|
|
5439
|
+
|
|
5440
|
+
approval_summary_copy = dict(approval_summary)
|
|
5441
|
+
self._require_execute_approval(
|
|
5442
|
+
approval_token=approval_token,
|
|
5443
|
+
tool_name=tool_name,
|
|
5444
|
+
summary=approval_summary_copy,
|
|
5445
|
+
action_label="Swap intent",
|
|
5446
|
+
)
|
|
5447
|
+
try:
|
|
5448
|
+
approved_min_output_raw = (
|
|
5449
|
+
int(approval_summary_copy["minimum_output_amount_raw"])
|
|
5450
|
+
if approval_summary_copy.get("minimum_output_amount_raw") is not None
|
|
5451
|
+
else None
|
|
5452
|
+
)
|
|
5453
|
+
approved_max_fee_lamports = (
|
|
5454
|
+
int(approval_summary_copy["max_fee_lamports"])
|
|
5455
|
+
if approval_summary_copy.get("max_fee_lamports") is not None
|
|
5456
|
+
else None
|
|
5457
|
+
)
|
|
5458
|
+
approved_valid_until = (
|
|
5459
|
+
int(approval_summary_copy["valid_until_epoch_seconds"])
|
|
5460
|
+
if approval_summary_copy.get("valid_until_epoch_seconds") is not None
|
|
5461
|
+
else None
|
|
5462
|
+
)
|
|
5463
|
+
approved_max_attempts = int(approval_summary_copy.get("max_attempts") or max_attempts)
|
|
5464
|
+
except (TypeError, ValueError):
|
|
5465
|
+
raise WalletBackendError(
|
|
5466
|
+
"approval_token does not contain valid swap intent limits. Generate a fresh intent preview and approval before execute."
|
|
5467
|
+
)
|
|
5468
|
+
result = await self.backend.execute_swap_intent(
|
|
5469
|
+
input_mint=input_mint.strip(),
|
|
5470
|
+
output_mint=output_mint.strip(),
|
|
5471
|
+
amount_ui=float(amount),
|
|
5472
|
+
slippage_bps=slippage_bps,
|
|
5473
|
+
minimum_output_amount_raw=approved_min_output_raw,
|
|
5474
|
+
max_fee_lamports=approved_max_fee_lamports,
|
|
5475
|
+
valid_until_epoch_seconds=approved_valid_until,
|
|
5476
|
+
max_attempts=approved_max_attempts,
|
|
5477
|
+
)
|
|
5478
|
+
return AgentToolResult(
|
|
5479
|
+
tool=tool_name,
|
|
5480
|
+
ok=True,
|
|
5481
|
+
data=self._annotate_sensitive_payload(
|
|
5482
|
+
result,
|
|
5483
|
+
action_label="Swap",
|
|
5484
|
+
mode="execute",
|
|
5485
|
+
),
|
|
5486
|
+
)
|
|
5487
|
+
|
|
5308
5488
|
approval_payload = inspect_approval_token(
|
|
5309
5489
|
approval_token,
|
|
5310
5490
|
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": (
|
|
@@ -91,6 +91,13 @@ def _direct_jupiter_enabled() -> bool:
|
|
|
91
91
|
return bool(settings.jupiter_api_key.strip())
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _swap_v2_base_url() -> str:
|
|
95
|
+
return os.getenv(
|
|
96
|
+
"JUPITER_SWAP_V2_API_BASE_URL",
|
|
97
|
+
settings.jupiter_swap_v2_api_base_url,
|
|
98
|
+
).strip().rstrip("/")
|
|
99
|
+
|
|
100
|
+
|
|
94
101
|
def _unwrap_gateway_payload(
|
|
95
102
|
status_code: int,
|
|
96
103
|
payload: Any,
|
|
@@ -367,6 +374,52 @@ async def fetch_ultra_order(
|
|
|
367
374
|
return data
|
|
368
375
|
|
|
369
376
|
|
|
377
|
+
async def fetch_swap_v2_order(
|
|
378
|
+
*,
|
|
379
|
+
input_mint: str,
|
|
380
|
+
output_mint: str,
|
|
381
|
+
amount_raw: int,
|
|
382
|
+
taker: str,
|
|
383
|
+
slippage_bps: int | str | None = None,
|
|
384
|
+
exclude_routers: list[str] | None = None,
|
|
385
|
+
swap_mode: str = "ExactIn",
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
"""Fetch a Jupiter Swap API V2 meta-aggregator order."""
|
|
388
|
+
client = get_client()
|
|
389
|
+
params: dict[str, Any] = {
|
|
390
|
+
"inputMint": input_mint,
|
|
391
|
+
"outputMint": output_mint,
|
|
392
|
+
"amount": str(amount_raw),
|
|
393
|
+
"taker": taker,
|
|
394
|
+
}
|
|
395
|
+
if swap_mode != "ExactIn":
|
|
396
|
+
params["swapMode"] = swap_mode
|
|
397
|
+
if slippage_bps is not None:
|
|
398
|
+
params["slippageBps"] = str(slippage_bps)
|
|
399
|
+
if exclude_routers:
|
|
400
|
+
params["excludeRouters"] = ",".join(str(item).strip() for item in exclude_routers if str(item).strip())
|
|
401
|
+
|
|
402
|
+
response = await client.get(
|
|
403
|
+
f"{_swap_v2_base_url()}/order",
|
|
404
|
+
params=params,
|
|
405
|
+
headers=_headers(),
|
|
406
|
+
)
|
|
407
|
+
if response.status_code != 200:
|
|
408
|
+
raise ProviderError("jupiter-v2", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
409
|
+
data = response.json()
|
|
410
|
+
if not isinstance(data, dict):
|
|
411
|
+
raise ProviderError("jupiter-v2", "Unexpected order response from Jupiter Swap V2.")
|
|
412
|
+
if data.get("error") or data.get("errorCode"):
|
|
413
|
+
raise ProviderError(
|
|
414
|
+
"jupiter-v2",
|
|
415
|
+
str(data.get("error") or data.get("errorCode") or "Unknown Swap V2 order error."),
|
|
416
|
+
details=data,
|
|
417
|
+
)
|
|
418
|
+
if "outAmount" not in data:
|
|
419
|
+
raise ProviderError("jupiter-v2", "Unexpected order response from Jupiter Swap V2.")
|
|
420
|
+
return data
|
|
421
|
+
|
|
422
|
+
|
|
370
423
|
async def build_swap_transaction(
|
|
371
424
|
*,
|
|
372
425
|
user_public_key: str,
|
|
@@ -477,6 +530,42 @@ async def execute_ultra_order(
|
|
|
477
530
|
return data
|
|
478
531
|
|
|
479
532
|
|
|
533
|
+
async def execute_swap_v2_order(
|
|
534
|
+
*,
|
|
535
|
+
signed_transaction_base64: str,
|
|
536
|
+
request_id: str,
|
|
537
|
+
last_valid_block_height: int | str | None = None,
|
|
538
|
+
) -> dict[str, Any]:
|
|
539
|
+
"""Execute a signed Jupiter Swap API V2 order."""
|
|
540
|
+
client = get_client()
|
|
541
|
+
body: dict[str, Any] = {
|
|
542
|
+
"signedTransaction": signed_transaction_base64,
|
|
543
|
+
"requestId": request_id,
|
|
544
|
+
}
|
|
545
|
+
if last_valid_block_height is not None:
|
|
546
|
+
body["lastValidBlockHeight"] = str(last_valid_block_height)
|
|
547
|
+
response = await client.post(
|
|
548
|
+
f"{_swap_v2_base_url()}/execute",
|
|
549
|
+
json=body,
|
|
550
|
+
headers={**_headers(), "Content-Type": "application/json"},
|
|
551
|
+
)
|
|
552
|
+
if response.status_code != 200:
|
|
553
|
+
raise ProviderError("jupiter-v2", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
554
|
+
data = response.json()
|
|
555
|
+
if not isinstance(data, dict):
|
|
556
|
+
raise ProviderError("jupiter-v2", "Unexpected execute response from Jupiter Swap V2.")
|
|
557
|
+
if data.get("error") or data.get("errorCode"):
|
|
558
|
+
raise ProviderError(
|
|
559
|
+
"jupiter-v2",
|
|
560
|
+
str(data.get("error") or data.get("errorCode") or "Unknown Swap V2 execute error."),
|
|
561
|
+
details=data,
|
|
562
|
+
)
|
|
563
|
+
if str(data.get("status") or "").strip().lower() == "failed":
|
|
564
|
+
message = data.get("error") or data.get("code") or "Swap V2 execute failed."
|
|
565
|
+
raise ProviderError("jupiter-v2", str(message), details=data)
|
|
566
|
+
return data
|
|
567
|
+
|
|
568
|
+
|
|
480
569
|
async def fetch_prices(
|
|
481
570
|
*,
|
|
482
571
|
mints: list[str],
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
import time
|
|
6
7
|
from dataclasses import asdict, dataclass, field
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
@@ -731,16 +732,74 @@ class AgentWalletBackend(ABC):
|
|
|
731
732
|
input_mint: str,
|
|
732
733
|
output_mint: str,
|
|
733
734
|
amount_ui: float,
|
|
734
|
-
slippage_bps: int =
|
|
735
|
+
slippage_bps: int = 300,
|
|
735
736
|
) -> dict[str, Any]:
|
|
736
737
|
raise WalletBackendError(f"{self.name} does not support swap previews.")
|
|
737
738
|
|
|
739
|
+
async def preview_swap_intent(
|
|
740
|
+
self,
|
|
741
|
+
input_mint: str,
|
|
742
|
+
output_mint: str,
|
|
743
|
+
amount_ui: float,
|
|
744
|
+
slippage_bps: int = 300,
|
|
745
|
+
minimum_output_amount_raw: int | None = None,
|
|
746
|
+
max_fee_lamports: int | None = None,
|
|
747
|
+
valid_for_seconds: int = 30,
|
|
748
|
+
max_attempts: int = 2,
|
|
749
|
+
) -> dict[str, Any]:
|
|
750
|
+
preview = await self.preview_swap(
|
|
751
|
+
input_mint=input_mint,
|
|
752
|
+
output_mint=output_mint,
|
|
753
|
+
amount_ui=amount_ui,
|
|
754
|
+
slippage_bps=slippage_bps,
|
|
755
|
+
)
|
|
756
|
+
fee_summary = preview.get("fee_summary") if isinstance(preview.get("fee_summary"), dict) else {}
|
|
757
|
+
network_fee_lamports = fee_summary.get("network_fee_lamports")
|
|
758
|
+
if max_fee_lamports is None and isinstance(network_fee_lamports, int):
|
|
759
|
+
max_fee_lamports = max(network_fee_lamports * 3, network_fee_lamports + 100_000)
|
|
760
|
+
resolved_min_raw = minimum_output_amount_raw
|
|
761
|
+
if resolved_min_raw is None and isinstance(preview.get("minimum_output_amount_raw"), int):
|
|
762
|
+
resolved_min_raw = int(preview["minimum_output_amount_raw"])
|
|
763
|
+
output_decimals = preview.get("output_decimals")
|
|
764
|
+
minimum_output_amount_ui = preview.get("minimum_output_amount_ui")
|
|
765
|
+
if resolved_min_raw is not None and isinstance(output_decimals, int):
|
|
766
|
+
minimum_output_amount_ui = int(resolved_min_raw) / (10**output_decimals)
|
|
767
|
+
return {
|
|
768
|
+
"chain": preview.get("chain", "solana"),
|
|
769
|
+
"network": preview.get("network", getattr(self, "network", "unknown")),
|
|
770
|
+
"mode": "intent_preview",
|
|
771
|
+
"asset_type": "solana-swap-intent",
|
|
772
|
+
"owner": preview.get("owner"),
|
|
773
|
+
"input_mint": preview.get("input_mint", input_mint),
|
|
774
|
+
"output_mint": preview.get("output_mint", output_mint),
|
|
775
|
+
"input_amount_ui": preview.get("input_amount_ui", amount_ui),
|
|
776
|
+
"input_amount_raw": preview.get("input_amount_raw"),
|
|
777
|
+
"minimum_output_amount_raw": resolved_min_raw,
|
|
778
|
+
"minimum_output_amount_ui": minimum_output_amount_ui,
|
|
779
|
+
"indicative_output_amount_ui": preview.get("estimated_output_amount_ui"),
|
|
780
|
+
"indicative_output_amount_raw": preview.get("estimated_output_amount_raw"),
|
|
781
|
+
"max_slippage_bps": slippage_bps,
|
|
782
|
+
"slippage_bps": slippage_bps,
|
|
783
|
+
"max_fee_lamports": max_fee_lamports,
|
|
784
|
+
"valid_for_seconds": valid_for_seconds,
|
|
785
|
+
"valid_until_epoch_seconds": int(time.time()) + valid_for_seconds,
|
|
786
|
+
"max_attempts": max_attempts,
|
|
787
|
+
"allowed_providers": ["jupiter-ultra", "jupiter-metis"],
|
|
788
|
+
"recipient_policy": "owner-only",
|
|
789
|
+
"spend_policy": "exact-input",
|
|
790
|
+
"indicative_swap_provider": preview.get("swap_provider"),
|
|
791
|
+
"indicative_fee_summary": fee_summary,
|
|
792
|
+
"can_send": preview.get("can_send"),
|
|
793
|
+
"sign_only": preview.get("sign_only"),
|
|
794
|
+
"source": "swap-intent",
|
|
795
|
+
}
|
|
796
|
+
|
|
738
797
|
async def prepare_swap(
|
|
739
798
|
self,
|
|
740
799
|
input_mint: str,
|
|
741
800
|
output_mint: str,
|
|
742
801
|
amount_ui: float,
|
|
743
|
-
slippage_bps: int =
|
|
802
|
+
slippage_bps: int = 300,
|
|
744
803
|
) -> dict[str, Any]:
|
|
745
804
|
raise WalletBackendError(f"{self.name} does not support swap preparation.")
|
|
746
805
|
|
|
@@ -752,7 +811,7 @@ class AgentWalletBackend(ABC):
|
|
|
752
811
|
input_mint=str(preview["input_mint"]),
|
|
753
812
|
output_mint=str(preview["output_mint"]),
|
|
754
813
|
amount_ui=float(preview["input_amount_ui"]),
|
|
755
|
-
slippage_bps=int(preview.get("slippage_bps") or
|
|
814
|
+
slippage_bps=int(preview.get("slippage_bps") or 300),
|
|
756
815
|
)
|
|
757
816
|
|
|
758
817
|
async def execute_swap(
|
|
@@ -760,7 +819,7 @@ class AgentWalletBackend(ABC):
|
|
|
760
819
|
input_mint: str,
|
|
761
820
|
output_mint: str,
|
|
762
821
|
amount_ui: float,
|
|
763
|
-
slippage_bps: int =
|
|
822
|
+
slippage_bps: int = 300,
|
|
764
823
|
) -> dict[str, Any]:
|
|
765
824
|
raise WalletBackendError(f"{self.name} does not support swaps.")
|
|
766
825
|
|
|
@@ -772,8 +831,55 @@ class AgentWalletBackend(ABC):
|
|
|
772
831
|
input_mint=str(preview["input_mint"]),
|
|
773
832
|
output_mint=str(preview["output_mint"]),
|
|
774
833
|
amount_ui=float(preview["input_amount_ui"]),
|
|
775
|
-
slippage_bps=int(preview.get("slippage_bps") or
|
|
834
|
+
slippage_bps=int(preview.get("slippage_bps") or 300),
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
async def execute_swap_intent(
|
|
838
|
+
self,
|
|
839
|
+
*,
|
|
840
|
+
input_mint: str,
|
|
841
|
+
output_mint: str,
|
|
842
|
+
amount_ui: float,
|
|
843
|
+
slippage_bps: int = 300,
|
|
844
|
+
minimum_output_amount_raw: int | None = None,
|
|
845
|
+
max_fee_lamports: int | None = None,
|
|
846
|
+
valid_until_epoch_seconds: int | None = None,
|
|
847
|
+
max_attempts: int = 2,
|
|
848
|
+
) -> dict[str, Any]:
|
|
849
|
+
if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
|
|
850
|
+
raise WalletBackendError("Approved swap intent has expired. Create a fresh intent preview.")
|
|
851
|
+
preview = await self.preview_swap(
|
|
852
|
+
input_mint=input_mint,
|
|
853
|
+
output_mint=output_mint,
|
|
854
|
+
amount_ui=amount_ui,
|
|
855
|
+
slippage_bps=slippage_bps,
|
|
776
856
|
)
|
|
857
|
+
output_raw = preview.get("estimated_output_amount_raw")
|
|
858
|
+
if (
|
|
859
|
+
minimum_output_amount_raw is not None
|
|
860
|
+
and isinstance(output_raw, int)
|
|
861
|
+
and output_raw < int(minimum_output_amount_raw)
|
|
862
|
+
):
|
|
863
|
+
raise WalletBackendError(
|
|
864
|
+
"Fresh swap quote is below the approved minimum output. Funds were not moved."
|
|
865
|
+
)
|
|
866
|
+
fee_summary = preview.get("fee_summary") if isinstance(preview.get("fee_summary"), dict) else {}
|
|
867
|
+
network_fee_lamports = fee_summary.get("network_fee_lamports")
|
|
868
|
+
if (
|
|
869
|
+
max_fee_lamports is not None
|
|
870
|
+
and isinstance(network_fee_lamports, int)
|
|
871
|
+
and network_fee_lamports > int(max_fee_lamports)
|
|
872
|
+
):
|
|
873
|
+
raise WalletBackendError("Fresh swap fee exceeds the approved fee limit. Funds were not moved.")
|
|
874
|
+
result = await self.execute_swap_from_preview(preview)
|
|
875
|
+
result["intent_execution"] = {
|
|
876
|
+
"approved_minimum_output_amount_raw": minimum_output_amount_raw,
|
|
877
|
+
"approved_max_fee_lamports": max_fee_lamports,
|
|
878
|
+
"fresh_quote_used": True,
|
|
879
|
+
"attempt_count": 1,
|
|
880
|
+
"max_attempts": max_attempts,
|
|
881
|
+
}
|
|
882
|
+
return result
|
|
777
883
|
|
|
778
884
|
async def get_bags_claimable_positions(
|
|
779
885
|
self,
|
|
@@ -7,6 +7,7 @@ import base64
|
|
|
7
7
|
import binascii
|
|
8
8
|
import hashlib
|
|
9
9
|
import json
|
|
10
|
+
import time
|
|
10
11
|
from decimal import Decimal, InvalidOperation
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -48,6 +49,8 @@ TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
|
48
49
|
NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
|
|
49
50
|
STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
|
|
50
51
|
HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
|
|
52
|
+
SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
|
|
53
|
+
SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
def _load_signing_key():
|
|
@@ -2062,6 +2065,29 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
2062
2065
|
parts.append(f"route fee {route_fee_bps} bps (already reflected in quoted output)")
|
|
2063
2066
|
return "; ".join(parts)
|
|
2064
2067
|
|
|
2068
|
+
def _swap_fee_lamports(self, payload: dict[str, Any]) -> int | None:
|
|
2069
|
+
fee_summary = payload.get("fee_summary")
|
|
2070
|
+
if isinstance(fee_summary, dict):
|
|
2071
|
+
network_fee = _coerce_int(fee_summary.get("network_fee_lamports"))
|
|
2072
|
+
if network_fee is not None:
|
|
2073
|
+
return network_fee
|
|
2074
|
+
return None
|
|
2075
|
+
|
|
2076
|
+
def _default_swap_intent_max_fee_lamports(self, fee_summary: dict[str, Any]) -> int:
|
|
2077
|
+
estimated_fee = _coerce_int(fee_summary.get("network_fee_lamports")) or 0
|
|
2078
|
+
return max(
|
|
2079
|
+
estimated_fee * 3,
|
|
2080
|
+
estimated_fee + 100_000,
|
|
2081
|
+
SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS,
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
def _swap_minimum_output_floor(self, *, out_amount_raw: int, slippage_bps: int) -> int:
|
|
2085
|
+
if out_amount_raw <= 0:
|
|
2086
|
+
return 0
|
|
2087
|
+
if slippage_bps <= 0:
|
|
2088
|
+
raise WalletBackendError("slippage_bps must be greater than zero.")
|
|
2089
|
+
return max(1, (out_amount_raw * max(0, 10_000 - slippage_bps)) // 10_000)
|
|
2090
|
+
|
|
2065
2091
|
def _require_mainnet_bags(self, feature: str) -> None:
|
|
2066
2092
|
if self.network != "mainnet":
|
|
2067
2093
|
raise WalletBackendError(f"{feature} is only enabled for Solana mainnet.")
|
|
@@ -5426,7 +5452,8 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5426
5452
|
input_mint: str,
|
|
5427
5453
|
output_mint: str,
|
|
5428
5454
|
amount_ui: float,
|
|
5429
|
-
slippage_bps: int =
|
|
5455
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5456
|
+
exclude_routers: list[str] | None = None,
|
|
5430
5457
|
) -> dict[str, Any]:
|
|
5431
5458
|
if self.network != "mainnet":
|
|
5432
5459
|
raise WalletBackendError("Provider-routed swaps are only enabled for Solana mainnet.")
|
|
@@ -5447,29 +5474,38 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5447
5474
|
raise WalletBackendError("amount is too small for the input token decimals.")
|
|
5448
5475
|
|
|
5449
5476
|
sender = await self.get_address()
|
|
5450
|
-
quote_source = "jupiter-
|
|
5477
|
+
quote_source = "jupiter-v2-order"
|
|
5451
5478
|
try:
|
|
5452
|
-
quote = await jupiter.
|
|
5479
|
+
quote = await jupiter.fetch_swap_v2_order(
|
|
5453
5480
|
input_mint=input_mint,
|
|
5454
5481
|
output_mint=output_mint,
|
|
5455
5482
|
amount_raw=raw_amount,
|
|
5456
5483
|
taker=sender,
|
|
5457
|
-
|
|
5484
|
+
exclude_routers=exclude_routers,
|
|
5458
5485
|
)
|
|
5459
5486
|
except ProviderError:
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5487
|
+
quote_source = "jupiter-ultra"
|
|
5488
|
+
try:
|
|
5489
|
+
quote = await jupiter.fetch_ultra_order(
|
|
5490
|
+
input_mint=input_mint,
|
|
5491
|
+
output_mint=output_mint,
|
|
5492
|
+
amount_raw=raw_amount,
|
|
5493
|
+
taker=sender,
|
|
5494
|
+
slippage_bps=slippage_bps,
|
|
5495
|
+
)
|
|
5496
|
+
except ProviderError:
|
|
5497
|
+
quote = await jupiter.fetch_quote(
|
|
5498
|
+
input_mint=input_mint,
|
|
5499
|
+
output_mint=output_mint,
|
|
5500
|
+
amount_raw=raw_amount,
|
|
5501
|
+
slippage_bps=slippage_bps,
|
|
5502
|
+
)
|
|
5503
|
+
quote_source = "jupiter-metis"
|
|
5467
5504
|
|
|
5468
5505
|
out_amount_raw = int(quote.get("outAmount") or 0)
|
|
5469
|
-
other_threshold_raw =
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
or 0
|
|
5506
|
+
other_threshold_raw = self._swap_minimum_output_floor(
|
|
5507
|
+
out_amount_raw=out_amount_raw,
|
|
5508
|
+
slippage_bps=slippage_bps,
|
|
5473
5509
|
)
|
|
5474
5510
|
fee_summary = self._build_swap_fee_summary(
|
|
5475
5511
|
swap_provider=quote_source,
|
|
@@ -5505,12 +5541,115 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5505
5541
|
"source": quote_source,
|
|
5506
5542
|
}
|
|
5507
5543
|
|
|
5544
|
+
async def preview_swap_intent(
|
|
5545
|
+
self,
|
|
5546
|
+
input_mint: str,
|
|
5547
|
+
output_mint: str,
|
|
5548
|
+
amount_ui: float,
|
|
5549
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5550
|
+
minimum_output_amount_raw: int | None = None,
|
|
5551
|
+
max_fee_lamports: int | None = None,
|
|
5552
|
+
valid_for_seconds: int = 120,
|
|
5553
|
+
max_attempts: int = 3,
|
|
5554
|
+
) -> dict[str, Any]:
|
|
5555
|
+
if valid_for_seconds <= 0 or valid_for_seconds > 120:
|
|
5556
|
+
raise WalletBackendError("valid_for_seconds must be between 1 and 120.")
|
|
5557
|
+
if max_attempts <= 0 or max_attempts > 5:
|
|
5558
|
+
raise WalletBackendError("max_attempts must be between 1 and 5.")
|
|
5559
|
+
slippage_bps = max(int(slippage_bps), SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS)
|
|
5560
|
+
max_attempts = max(int(max_attempts), 3)
|
|
5561
|
+
|
|
5562
|
+
indicative = await self.preview_swap(
|
|
5563
|
+
input_mint=input_mint,
|
|
5564
|
+
output_mint=output_mint,
|
|
5565
|
+
amount_ui=amount_ui,
|
|
5566
|
+
slippage_bps=slippage_bps,
|
|
5567
|
+
)
|
|
5568
|
+
indicative_output_raw = int(indicative.get("estimated_output_amount_raw") or 0)
|
|
5569
|
+
slippage_floor_raw = self._swap_minimum_output_floor(
|
|
5570
|
+
out_amount_raw=indicative_output_raw,
|
|
5571
|
+
slippage_bps=slippage_bps,
|
|
5572
|
+
)
|
|
5573
|
+
requested_min_output_raw = (
|
|
5574
|
+
int(minimum_output_amount_raw)
|
|
5575
|
+
if minimum_output_amount_raw is not None
|
|
5576
|
+
else None
|
|
5577
|
+
)
|
|
5578
|
+
if requested_min_output_raw is not None:
|
|
5579
|
+
min_output_raw = min(requested_min_output_raw, slippage_floor_raw)
|
|
5580
|
+
minimum_output_policy = (
|
|
5581
|
+
"explicit_clamped_to_slippage_floor"
|
|
5582
|
+
if requested_min_output_raw > slippage_floor_raw
|
|
5583
|
+
else "explicit"
|
|
5584
|
+
)
|
|
5585
|
+
else:
|
|
5586
|
+
min_output_raw = slippage_floor_raw
|
|
5587
|
+
minimum_output_policy = "slippage_floor"
|
|
5588
|
+
if min_output_raw <= 0:
|
|
5589
|
+
raise WalletBackendError("minimum_output_amount_raw could not be derived from the indicative quote.")
|
|
5590
|
+
output_decimals = int(indicative.get("output_decimals") or 0)
|
|
5591
|
+
min_output_ui = min_output_raw / (10**output_decimals)
|
|
5592
|
+
|
|
5593
|
+
fee_summary = (
|
|
5594
|
+
indicative.get("fee_summary")
|
|
5595
|
+
if isinstance(indicative.get("fee_summary"), dict)
|
|
5596
|
+
else {}
|
|
5597
|
+
)
|
|
5598
|
+
fee_limit = (
|
|
5599
|
+
int(max_fee_lamports)
|
|
5600
|
+
if max_fee_lamports is not None
|
|
5601
|
+
else self._default_swap_intent_max_fee_lamports(fee_summary)
|
|
5602
|
+
)
|
|
5603
|
+
if fee_limit < 0:
|
|
5604
|
+
raise WalletBackendError("max_fee_lamports must be non-negative.")
|
|
5605
|
+
|
|
5606
|
+
return {
|
|
5607
|
+
"chain": "solana",
|
|
5608
|
+
"network": self.network,
|
|
5609
|
+
"mode": "intent_preview",
|
|
5610
|
+
"asset_type": "solana-swap-intent",
|
|
5611
|
+
"owner": indicative.get("owner"),
|
|
5612
|
+
"input_mint": indicative["input_mint"],
|
|
5613
|
+
"output_mint": indicative["output_mint"],
|
|
5614
|
+
"input_amount_ui": indicative["input_amount_ui"],
|
|
5615
|
+
"input_amount_raw": indicative["input_amount_raw"],
|
|
5616
|
+
"input_decimals": indicative.get("input_decimals"),
|
|
5617
|
+
"output_decimals": indicative.get("output_decimals"),
|
|
5618
|
+
"indicative_output_amount_ui": indicative.get("estimated_output_amount_ui"),
|
|
5619
|
+
"indicative_output_amount_raw": indicative.get("estimated_output_amount_raw"),
|
|
5620
|
+
"minimum_output_amount_ui": min_output_ui,
|
|
5621
|
+
"minimum_output_amount_raw": min_output_raw,
|
|
5622
|
+
"requested_minimum_output_amount_raw": requested_min_output_raw,
|
|
5623
|
+
"minimum_output_policy": minimum_output_policy,
|
|
5624
|
+
"max_slippage_bps": slippage_bps,
|
|
5625
|
+
"slippage_bps": slippage_bps,
|
|
5626
|
+
"max_fee_lamports": fee_limit,
|
|
5627
|
+
"max_fee_sol": fee_limit / solana_rpc.LAMPORTS_PER_SOL,
|
|
5628
|
+
"valid_for_seconds": valid_for_seconds,
|
|
5629
|
+
"valid_until_epoch_seconds": int(time.time()) + valid_for_seconds,
|
|
5630
|
+
"max_attempts": max_attempts,
|
|
5631
|
+
"allowed_providers": ["jupiter-v2-order", "jupiter-ultra", "jupiter-metis"],
|
|
5632
|
+
"recipient_policy": "owner-only",
|
|
5633
|
+
"spend_policy": "exact-input",
|
|
5634
|
+
"indicative_swap_provider": indicative.get("swap_provider"),
|
|
5635
|
+
"indicative_price_impact_pct": indicative.get("price_impact_pct"),
|
|
5636
|
+
"indicative_route_plan": indicative.get("route_plan", []),
|
|
5637
|
+
"indicative_fee_summary": fee_summary,
|
|
5638
|
+
"intent_note": (
|
|
5639
|
+
"This is an intent approval preview. Execute will fetch a fresh quote and "
|
|
5640
|
+
"only sign/send if it remains inside these approved limits."
|
|
5641
|
+
),
|
|
5642
|
+
"can_send": self.get_capabilities().can_send_transaction,
|
|
5643
|
+
"sign_only": self.sign_only,
|
|
5644
|
+
"source": "swap-intent",
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5508
5647
|
async def execute_swap(
|
|
5509
5648
|
self,
|
|
5510
5649
|
input_mint: str,
|
|
5511
5650
|
output_mint: str,
|
|
5512
5651
|
amount_ui: float,
|
|
5513
|
-
slippage_bps: int =
|
|
5652
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5514
5653
|
) -> dict[str, Any]:
|
|
5515
5654
|
preview = await self.preview_swap(
|
|
5516
5655
|
input_mint=input_mint,
|
|
@@ -5520,17 +5659,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5520
5659
|
)
|
|
5521
5660
|
return await self.execute_swap_from_preview(preview)
|
|
5522
5661
|
|
|
5523
|
-
async def
|
|
5662
|
+
async def _submit_prepared_swap(
|
|
5524
5663
|
self,
|
|
5525
|
-
|
|
5664
|
+
prepared: dict[str, Any],
|
|
5526
5665
|
) -> dict[str, Any]:
|
|
5527
|
-
prepared = await self.prepare_swap_from_preview(preview)
|
|
5528
5666
|
if self.sign_only:
|
|
5529
5667
|
raise WalletBackendError(
|
|
5530
5668
|
"This wallet backend is in sign-only mode. Disable sign_only to broadcast transactions."
|
|
5531
5669
|
)
|
|
5532
5670
|
|
|
5533
|
-
if prepared.get("swap_provider") == "jupiter-
|
|
5671
|
+
if prepared.get("swap_provider") == "jupiter-v2-order":
|
|
5672
|
+
submitted = await jupiter.execute_swap_v2_order(
|
|
5673
|
+
signed_transaction_base64=str(prepared["transaction_base64"]),
|
|
5674
|
+
request_id=str(prepared["request_id"]),
|
|
5675
|
+
last_valid_block_height=_coerce_int(prepared.get("last_valid_block_height")),
|
|
5676
|
+
)
|
|
5677
|
+
onchain_signature = submitted.get("signature") or submitted.get("txid")
|
|
5678
|
+
elif prepared.get("swap_provider") == "jupiter-ultra":
|
|
5534
5679
|
submitted = await jupiter.execute_ultra_order(
|
|
5535
5680
|
signed_transaction_base64=str(prepared["transaction_base64"]),
|
|
5536
5681
|
request_id=str(prepared["request_id"]),
|
|
@@ -5583,12 +5728,132 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5583
5728
|
"source": prepared.get("swap_provider") or "jupiter-metis",
|
|
5584
5729
|
}
|
|
5585
5730
|
|
|
5731
|
+
async def execute_swap_from_preview(
|
|
5732
|
+
self,
|
|
5733
|
+
preview: dict[str, Any],
|
|
5734
|
+
) -> dict[str, Any]:
|
|
5735
|
+
prepared = await self.prepare_swap_from_preview(preview)
|
|
5736
|
+
return await self._submit_prepared_swap(prepared)
|
|
5737
|
+
|
|
5738
|
+
async def execute_swap_intent(
|
|
5739
|
+
self,
|
|
5740
|
+
*,
|
|
5741
|
+
input_mint: str,
|
|
5742
|
+
output_mint: str,
|
|
5743
|
+
amount_ui: float,
|
|
5744
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5745
|
+
minimum_output_amount_raw: int | None = None,
|
|
5746
|
+
max_fee_lamports: int | None = None,
|
|
5747
|
+
valid_until_epoch_seconds: int | None = None,
|
|
5748
|
+
max_attempts: int = 3,
|
|
5749
|
+
) -> dict[str, Any]:
|
|
5750
|
+
if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
|
|
5751
|
+
raise WalletBackendError("Approved swap intent has expired. Create a fresh intent preview.")
|
|
5752
|
+
if max_attempts <= 0 or max_attempts > 5:
|
|
5753
|
+
raise WalletBackendError("max_attempts must be between 1 and 5.")
|
|
5754
|
+
slippage_bps = max(int(slippage_bps), SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS)
|
|
5755
|
+
max_attempts = max(int(max_attempts), 3)
|
|
5756
|
+
|
|
5757
|
+
attempts: list[dict[str, Any]] = []
|
|
5758
|
+
last_error: str | None = None
|
|
5759
|
+
for attempt_index in range(max_attempts):
|
|
5760
|
+
if valid_until_epoch_seconds is not None and int(time.time()) > int(valid_until_epoch_seconds):
|
|
5761
|
+
break
|
|
5762
|
+
try:
|
|
5763
|
+
exclude_routers = ["jupiterz"] if attempt_index > 0 else None
|
|
5764
|
+
preview = await self.preview_swap(
|
|
5765
|
+
input_mint=input_mint,
|
|
5766
|
+
output_mint=output_mint,
|
|
5767
|
+
amount_ui=amount_ui,
|
|
5768
|
+
slippage_bps=slippage_bps,
|
|
5769
|
+
exclude_routers=exclude_routers,
|
|
5770
|
+
)
|
|
5771
|
+
estimated_output_raw = int(preview.get("estimated_output_amount_raw") or 0)
|
|
5772
|
+
if (
|
|
5773
|
+
minimum_output_amount_raw is not None
|
|
5774
|
+
and estimated_output_raw < int(minimum_output_amount_raw)
|
|
5775
|
+
):
|
|
5776
|
+
attempts.append(
|
|
5777
|
+
{
|
|
5778
|
+
"attempt": attempt_index + 1,
|
|
5779
|
+
"swap_provider": preview.get("swap_provider"),
|
|
5780
|
+
"rejected": "quote_below_minimum_output",
|
|
5781
|
+
"estimated_output_amount_raw": estimated_output_raw,
|
|
5782
|
+
"minimum_output_amount_raw": int(minimum_output_amount_raw),
|
|
5783
|
+
}
|
|
5784
|
+
)
|
|
5785
|
+
last_error = "Fresh swap quote is below the approved minimum output."
|
|
5786
|
+
continue
|
|
5787
|
+
|
|
5788
|
+
prepared = await self.prepare_swap_from_preview(preview)
|
|
5789
|
+
prepared_fee = self._swap_fee_lamports(prepared)
|
|
5790
|
+
if (
|
|
5791
|
+
max_fee_lamports is not None
|
|
5792
|
+
and prepared_fee is not None
|
|
5793
|
+
and prepared_fee > int(max_fee_lamports)
|
|
5794
|
+
):
|
|
5795
|
+
attempts.append(
|
|
5796
|
+
{
|
|
5797
|
+
"attempt": attempt_index + 1,
|
|
5798
|
+
"swap_provider": prepared.get("swap_provider"),
|
|
5799
|
+
"rejected": "fee_above_limit",
|
|
5800
|
+
"fee_lamports": prepared_fee,
|
|
5801
|
+
"max_fee_lamports": int(max_fee_lamports),
|
|
5802
|
+
}
|
|
5803
|
+
)
|
|
5804
|
+
last_error = "Fresh swap fee exceeds the approved fee limit."
|
|
5805
|
+
continue
|
|
5806
|
+
|
|
5807
|
+
result = await self._submit_prepared_swap(prepared)
|
|
5808
|
+
result["intent_execution"] = {
|
|
5809
|
+
"approved_minimum_output_amount_raw": minimum_output_amount_raw,
|
|
5810
|
+
"approved_max_fee_lamports": max_fee_lamports,
|
|
5811
|
+
"fresh_quote_used": True,
|
|
5812
|
+
"attempt_count": attempt_index + 1,
|
|
5813
|
+
"max_attempts": max_attempts,
|
|
5814
|
+
"attempts": attempts
|
|
5815
|
+
+ [
|
|
5816
|
+
{
|
|
5817
|
+
"attempt": attempt_index + 1,
|
|
5818
|
+
"swap_provider": prepared.get("swap_provider"),
|
|
5819
|
+
"status": "submitted",
|
|
5820
|
+
}
|
|
5821
|
+
],
|
|
5822
|
+
}
|
|
5823
|
+
return result
|
|
5824
|
+
except (WalletBackendError, ProviderError) as exc:
|
|
5825
|
+
last_error = str(exc)
|
|
5826
|
+
attempts.append(
|
|
5827
|
+
{
|
|
5828
|
+
"attempt": attempt_index + 1,
|
|
5829
|
+
"rejected": "execution_error",
|
|
5830
|
+
"error": str(exc),
|
|
5831
|
+
}
|
|
5832
|
+
)
|
|
5833
|
+
if "sign-only mode" in str(exc).lower():
|
|
5834
|
+
break
|
|
5835
|
+
if attempt_index + 1 < max_attempts:
|
|
5836
|
+
await asyncio.sleep(min(0.5 * (attempt_index + 1), 1.5))
|
|
5837
|
+
|
|
5838
|
+
reason_suffix = f" Last reason: {last_error}" if last_error else ""
|
|
5839
|
+
raise WalletBackendError(
|
|
5840
|
+
"Solana swap intent execution failed within the approved limits. Funds were not moved."
|
|
5841
|
+
+ reason_suffix,
|
|
5842
|
+
details={
|
|
5843
|
+
"reason": last_error,
|
|
5844
|
+
"attempts": attempts,
|
|
5845
|
+
"minimum_output_amount_raw": minimum_output_amount_raw,
|
|
5846
|
+
"max_fee_lamports": max_fee_lamports,
|
|
5847
|
+
"max_attempts": max_attempts,
|
|
5848
|
+
},
|
|
5849
|
+
)
|
|
5850
|
+
|
|
5586
5851
|
async def prepare_swap(
|
|
5587
5852
|
self,
|
|
5588
5853
|
input_mint: str,
|
|
5589
5854
|
output_mint: str,
|
|
5590
5855
|
amount_ui: float,
|
|
5591
|
-
slippage_bps: int =
|
|
5856
|
+
slippage_bps: int = SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS,
|
|
5592
5857
|
) -> dict[str, Any]:
|
|
5593
5858
|
preview = await self.preview_swap(
|
|
5594
5859
|
input_mint=input_mint,
|
|
@@ -5629,13 +5894,23 @@ class SolanaWalletBackend(AgentWalletBackend):
|
|
|
5629
5894
|
|
|
5630
5895
|
swap_provider = str(preview.get("swap_provider") or "jupiter-metis")
|
|
5631
5896
|
request_id = None
|
|
5632
|
-
if swap_provider
|
|
5897
|
+
if swap_provider in {"jupiter-v2-order", "jupiter-ultra"}:
|
|
5633
5898
|
swap_build = preview["quote_response"]
|
|
5634
5899
|
unsigned_transaction = VersionedTransaction.from_bytes(
|
|
5635
5900
|
base64.b64decode(str(swap_build["transaction"]))
|
|
5636
5901
|
)
|
|
5637
5902
|
request_id = swap_build.get("requestId")
|
|
5638
|
-
|
|
5903
|
+
blockhash_metadata = swap_build.get("blockhashWithMetadata")
|
|
5904
|
+
last_valid_block_height = (
|
|
5905
|
+
blockhash_metadata.get("lastValidBlockHeight")
|
|
5906
|
+
if isinstance(blockhash_metadata, dict)
|
|
5907
|
+
else None
|
|
5908
|
+
)
|
|
5909
|
+
if last_valid_block_height is None:
|
|
5910
|
+
last_valid_block_height = (
|
|
5911
|
+
swap_build.get("lastValidBlockHeight")
|
|
5912
|
+
or swap_build.get("expireAt")
|
|
5913
|
+
)
|
|
5639
5914
|
prioritization_fee_lamports = swap_build.get("prioritizationFeeLamports")
|
|
5640
5915
|
compute_unit_limit = swap_build.get("computeUnitLimit")
|
|
5641
5916
|
else:
|
|
@@ -12,7 +12,7 @@ Use this skill before calling OpenClaw wallet tools. It is the routing guide for
|
|
|
12
12
|
1. Start with `get_wallet_capabilities` when the active chain, signing support, or available tools are unclear.
|
|
13
13
|
2. Use `get_wallet_address` before asking for deposits or confirming a recipient/source wallet.
|
|
14
14
|
3. Use `get_wallet_balance` before spending, swapping, bridging, staking, lending, or claiming.
|
|
15
|
-
4. Use `preview` first for every write action. Use `prepare` only after explicit user intent. In OpenClaw, use `execute` only after the user explicitly confirms the shown summary in chat; do not ask the user for `/approve`, buttons, popups, or a manual token.
|
|
15
|
+
4. Use `preview` first for every write action. For Solana Jupiter swaps, prefer `intent_preview` then `intent_execute` after explicit chat confirmation so execution can refresh the quote inside approved limits. Solana swap intents are normalized by the backend to at least 300 bps slippage, 120 seconds validity, and 3 fresh execution attempts; do not pass a hand-tightened `minimum_output_amount_raw` unless the user explicitly set that floor. Use `prepare` only after explicit user intent. In OpenClaw, use `execute` only after the user explicitly confirms the shown summary in chat; do not ask the user for `/approve`, buttons, popups, or a manual token.
|
|
16
16
|
5. `prepare` returns an execution plan only; it must not return signed transaction bytes.
|
|
17
17
|
6. On mainnet, restate the network and material terms before `execute`; the OpenClaw plugin handles the internal execution authorization after chat confirmation.
|
|
18
18
|
7. If backend is `sign_only`, do not execute; use `prepare` and state that nothing was broadcast.
|
|
@@ -76,7 +76,11 @@ Use this skill before calling OpenClaw wallet tools. It is the routing guide for
|
|
|
76
76
|
## Swap Commands
|
|
77
77
|
|
|
78
78
|
- Solana same-chain Jupiter swap: `swap_solana_tokens`
|
|
79
|
-
- Params: `input_mint`, `output_mint`, `amount` in UI units, optional `slippage_bps`, `mode`, `purpose`.
|
|
79
|
+
- Params: `input_mint`, `output_mint`, `amount` in UI units, optional `slippage_bps`, `minimum_output_amount_raw`, `max_fee_lamports`, `valid_for_seconds`, `max_attempts`, `mode`, `purpose`.
|
|
80
|
+
- Prefer `mode=intent_preview`, show the intent limits to the user, then after chat confirmation call `mode=intent_execute` with the same semantic params. This confirms risk limits, not a stale quote fingerprint.
|
|
81
|
+
- Default Solana swap slippage is 300 bps (3%). The backend computes the approved minimum output from the indicative output and slippage, not from a strict RFQ threshold.
|
|
82
|
+
- The primary execution path uses Jupiter Swap API V2 `/order` + `/execute`; if a JupiterZ/RFQ route fails, the backend retries with a non-JupiterZ route when possible.
|
|
83
|
+
- Do not use legacy `execute` for Solana Jupiter swaps in OpenClaw; exact quote-bound approval is too fragile for active markets.
|
|
80
84
|
- Use for SOL<->SPL or SPL<->SPL on Solana. Do not use LI.FI for Solana-only swaps.
|
|
81
85
|
- EVM same-chain Velora swap: `swap_evm_tokens`
|
|
82
86
|
- Params: `token_in`, `token_out`, `amount_in_raw` base-unit string, `mode`, `purpose`, optional `network`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlayer.tech/wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -74,4 +74,4 @@
|
|
|
74
74
|
"evm"
|
|
75
75
|
],
|
|
76
76
|
"license": "SEE LICENSE IN LICENSE"
|
|
77
|
-
}
|
|
77
|
+
}
|