@agentlayer.tech/wallet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.openclaw/AGENTS.md +98 -0
  2. package/.openclaw/extensions/agent-wallet/README.md +127 -0
  3. package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
  5. package/.openclaw/extensions/agent-wallet/package.json +11 -0
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
  7. package/CHANGELOG.md +42 -0
  8. package/LICENSE +104 -0
  9. package/README.md +332 -0
  10. package/RELEASING.md +204 -0
  11. package/agent-wallet/.env.example +62 -0
  12. package/agent-wallet/AGENTS.md +129 -0
  13. package/agent-wallet/README.md +527 -0
  14. package/agent-wallet/agent_wallet/__init__.py +11 -0
  15. package/agent-wallet/agent_wallet/approval.py +161 -0
  16. package/agent-wallet/agent_wallet/bootstrap.py +178 -0
  17. package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
  18. package/agent-wallet/agent_wallet/config.py +382 -0
  19. package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
  20. package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
  21. package/agent-wallet/agent_wallet/exceptions.py +9 -0
  22. package/agent-wallet/agent_wallet/file_ops.py +34 -0
  23. package/agent-wallet/agent_wallet/http_client.py +25 -0
  24. package/agent-wallet/agent_wallet/models.py +66 -0
  25. package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
  26. package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
  27. package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
  28. package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
  29. package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
  30. package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
  31. package/agent-wallet/agent_wallet/providers/bags.py +259 -0
  32. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
  33. package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
  34. package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
  35. package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
  36. package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
  37. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
  38. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
  39. package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
  40. package/agent-wallet/agent_wallet/solana_stake.py +103 -0
  41. package/agent-wallet/agent_wallet/solana_tx.py +93 -0
  42. package/agent-wallet/agent_wallet/spending_limits.py +101 -0
  43. package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
  44. package/agent-wallet/agent_wallet/user_wallets.py +355 -0
  45. package/agent-wallet/agent_wallet/validation.py +31 -0
  46. package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
  47. package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
  48. package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
  49. package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
  50. package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
  51. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
  52. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
  53. package/agent-wallet/examples/bootstrap_wallet.py +21 -0
  54. package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
  55. package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
  56. package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
  57. package/agent-wallet/openclaw.plugin.json +138 -0
  58. package/agent-wallet/pyproject.toml +31 -0
  59. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
  60. package/agent-wallet/scripts/build_release_bundle.py +188 -0
  61. package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
  62. package/agent-wallet/scripts/install_agent_wallet.py +505 -0
  63. package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
  64. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
  65. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
  66. package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
  67. package/agent-wallet/scripts/security_utils.py +37 -0
  68. package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
  69. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
  70. package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
  71. package/bin/openclaw-agent-wallet.mjs +487 -0
  72. package/install-from-github.sh +134 -0
  73. package/package.json +61 -0
  74. package/setup.sh +40 -0
  75. package/wdk-btc-wallet/README.md +325 -0
  76. package/wdk-btc-wallet/bootstrap.sh +22 -0
  77. package/wdk-btc-wallet/package-lock.json +1839 -0
  78. package/wdk-btc-wallet/package.json +18 -0
  79. package/wdk-btc-wallet/run-local.sh +21 -0
  80. package/wdk-btc-wallet/src/config.js +160 -0
  81. package/wdk-btc-wallet/src/json.js +35 -0
  82. package/wdk-btc-wallet/src/local_vault.js +432 -0
  83. package/wdk-btc-wallet/src/network_state.js +84 -0
  84. package/wdk-btc-wallet/src/server.js +257 -0
  85. package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
  86. package/wdk-evm-wallet/README.md +183 -0
  87. package/wdk-evm-wallet/bootstrap.sh +8 -0
  88. package/wdk-evm-wallet/package-lock.json +2340 -0
  89. package/wdk-evm-wallet/package.json +23 -0
  90. package/wdk-evm-wallet/run-local.sh +12 -0
  91. package/wdk-evm-wallet/src/config.js +274 -0
  92. package/wdk-evm-wallet/src/json.js +35 -0
  93. package/wdk-evm-wallet/src/local_vault.js +430 -0
  94. package/wdk-evm-wallet/src/network_state.js +92 -0
  95. package/wdk-evm-wallet/src/server.js +575 -0
  96. package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
@@ -0,0 +1,1628 @@
1
+ """Local EVM backend backed by the wdk-evm-wallet service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
6
+ from typing import Any
7
+
8
+ from agent_wallet.providers.evm_portfolio import build_portfolio_snapshot
9
+ from agent_wallet.providers import lifi
10
+ from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
11
+ from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError, WalletCapabilities
12
+
13
+
14
+ def _normalize_evm_network(value: str | None) -> str:
15
+ network = str(value or "").strip().lower()
16
+ aliases = {
17
+ "mainnet": "ethereum",
18
+ "eth": "ethereum",
19
+ "eth-mainnet": "ethereum",
20
+ "base-mainnet": "base",
21
+ "base_sepolia": "base-sepolia",
22
+ }
23
+ network = aliases.get(network, network)
24
+ if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
25
+ return "ethereum"
26
+ return network
27
+
28
+
29
+ def _lifi_chain_id_for_evm_network(network: str) -> str:
30
+ normalized = _normalize_evm_network(network)
31
+ if normalized == "base":
32
+ return "8453"
33
+ return "1"
34
+
35
+
36
+ def _extract_fee_wei(payload: dict[str, Any]) -> str | None:
37
+ for key in ("fee", "maxFee", "totalFee", "gasCost", "cost"):
38
+ value = payload.get(key)
39
+ if value is None:
40
+ continue
41
+ text = str(value).strip()
42
+ if text:
43
+ return text
44
+ return None
45
+
46
+
47
+ def _normalize_swap_route(quote: dict[str, Any]) -> Any:
48
+ for key in ("routePlan", "route", "priceRoute"):
49
+ value = quote.get(key)
50
+ if value is not None:
51
+ return value
52
+ return None
53
+
54
+
55
+ def _normalize_token_metadata(payload: Any, token_address: str | None = None) -> dict[str, Any] | None:
56
+ if not isinstance(payload, dict):
57
+ return None
58
+ address = str(payload.get("address") or token_address or "").strip()
59
+ name = payload.get("name")
60
+ symbol = payload.get("symbol")
61
+ decimals = payload.get("decimals")
62
+ normalized: dict[str, Any] = {
63
+ "address": address,
64
+ "name": str(name) if name is not None else None,
65
+ "symbol": str(symbol) if symbol is not None else None,
66
+ "decimals": int(decimals) if decimals is not None else None,
67
+ "verified": bool(payload.get("verified")),
68
+ "source": str(payload.get("source") or "erc20-rpc"),
69
+ }
70
+ return normalized
71
+
72
+
73
+ def _normalize_swap_allowance(payload: Any) -> dict[str, Any] | None:
74
+ if not isinstance(payload, dict):
75
+ return None
76
+ return {
77
+ "spender": str(payload.get("spender") or "").strip() or None,
78
+ "current_allowance_raw": str(payload.get("currentAllowance") or "0"),
79
+ "required_allowance_raw": str(payload.get("requiredAllowance") or "0"),
80
+ "approval_required": bool(payload.get("approvalRequired")),
81
+ "approval_sequence": list(payload.get("approvalSequence") or []),
82
+ }
83
+
84
+
85
+ def _normalize_swap_simulation(payload: Any) -> dict[str, Any] | None:
86
+ if not isinstance(payload, dict):
87
+ return None
88
+ return {
89
+ "ok": payload.get("ok"),
90
+ "skipped": bool(payload.get("skipped")),
91
+ "reason": str(payload.get("reason") or "").strip() or None,
92
+ "message": str(payload.get("message") or "").strip() or None,
93
+ "details": dict(payload.get("details") or {}) if isinstance(payload.get("details"), dict) else None,
94
+ }
95
+
96
+
97
+ def _normalize_aave_operation(value: str) -> str:
98
+ operation = str(value or "").strip().lower()
99
+ if operation not in {"supply", "withdraw", "borrow", "repay"}:
100
+ raise WalletBackendError("Aave operation must be one of: supply, withdraw, borrow, repay.")
101
+ return operation
102
+
103
+
104
+ def _normalize_lido_operation(value: str) -> str:
105
+ operation = str(value or "").strip().lower()
106
+ if operation not in {"stake_eth_for_wsteth", "wrap_steth", "unwrap_wsteth"}:
107
+ raise WalletBackendError(
108
+ "Lido operation must be one of: stake_eth_for_wsteth, wrap_steth, unwrap_wsteth."
109
+ )
110
+ return operation
111
+
112
+
113
+ def _normalize_lido_withdrawal_operation(value: str) -> str:
114
+ operation = str(value or "").strip().lower()
115
+ if operation not in {"request_withdrawal_steth", "request_withdrawal_wsteth", "claim_withdrawal"}:
116
+ raise WalletBackendError(
117
+ "Lido withdrawal operation must be one of: request_withdrawal_steth, request_withdrawal_wsteth, claim_withdrawal."
118
+ )
119
+ return operation
120
+
121
+
122
+ def _normalize_aave_payload(
123
+ *,
124
+ chain: str,
125
+ network: str,
126
+ wallet_id: str,
127
+ address: str,
128
+ operation: str,
129
+ token_address: str,
130
+ amount_raw: str,
131
+ data: dict[str, Any],
132
+ sign_only: bool,
133
+ ) -> dict[str, Any]:
134
+ result = dict(data.get("result") or {})
135
+ request = dict(data.get("operationRequest") or {})
136
+ token = str(request.get("token") or token_address)
137
+ amount = str(request.get("amount") or amount_raw)
138
+ return {
139
+ "chain": chain,
140
+ "network": network,
141
+ "asset_type": "evm-aave-v3",
142
+ "asset": "ERC20",
143
+ "wallet": wallet_id,
144
+ "from_address": str(data.get("address") or address),
145
+ "protocol": str(data.get("protocol") or "aave-v3"),
146
+ "operation": str(data.get("operation") or operation),
147
+ "token_address": token,
148
+ "amount_raw": amount,
149
+ "amount_ui": str(data.get("amountFormatted")) if data.get("amountFormatted") is not None else None,
150
+ "estimated_fee_wei": str(data.get("estimatedFeeWei")) if data.get("estimatedFeeWei") is not None else None,
151
+ "estimated_operation_fee_wei": (
152
+ str(data.get("estimatedOperationFeeWei"))
153
+ if data.get("estimatedOperationFeeWei") is not None
154
+ else None
155
+ ),
156
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or "0"),
157
+ "fee_estimate_available": bool(data.get("feeEstimateAvailable", True)),
158
+ "fee_estimate_error": data.get("feeEstimateError"),
159
+ "execution_supported": not sign_only,
160
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
161
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
162
+ "token_metadata": _normalize_token_metadata(data.get("tokenMetadata"), token),
163
+ "hash": result.get("hash"),
164
+ "approve_hash": result.get("approveHash"),
165
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
166
+ "result": result,
167
+ "chain_id": int(data.get("chainId") or 0),
168
+ "source": "wdk-evm-wallet",
169
+ }
170
+
171
+
172
+ def _normalize_lido_payload(
173
+ *,
174
+ chain: str,
175
+ network: str,
176
+ wallet_id: str,
177
+ address: str,
178
+ operation: str,
179
+ amount_raw: str,
180
+ data: dict[str, Any],
181
+ sign_only: bool,
182
+ ) -> dict[str, Any]:
183
+ result = dict(data.get("result") or {})
184
+ request = dict(data.get("operationRequest") or {})
185
+ amount = str(request.get("amount") or amount_raw)
186
+ return {
187
+ "chain": chain,
188
+ "network": network,
189
+ "asset_type": "evm-lido-staking",
190
+ "asset": "ETH",
191
+ "wallet": wallet_id,
192
+ "from_address": str(data.get("address") or address),
193
+ "protocol": str(data.get("protocol") or "lido"),
194
+ "operation": str(data.get("operation") or operation),
195
+ "amount_raw": amount,
196
+ "amount_ui": str(data.get("amountFormatted")) if data.get("amountFormatted") is not None else None,
197
+ "expected_output_amount_raw": str(data.get("expectedOutputAmountRaw") or "0"),
198
+ "expected_output_amount_ui": (
199
+ str(data.get("expectedOutputAmountFormatted"))
200
+ if data.get("expectedOutputAmountFormatted") is not None
201
+ else None
202
+ ),
203
+ "estimated_fee_wei": str(data.get("estimatedFeeWei")) if data.get("estimatedFeeWei") is not None else None,
204
+ "estimated_operation_fee_wei": (
205
+ str(data.get("estimatedOperationFeeWei"))
206
+ if data.get("estimatedOperationFeeWei") is not None
207
+ else None
208
+ ),
209
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or "0"),
210
+ "fee_estimate_available": bool(data.get("feeEstimateAvailable", True)),
211
+ "fee_estimate_error": data.get("feeEstimateError"),
212
+ "execution_supported": not sign_only,
213
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
214
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
215
+ "input_asset": _normalize_token_metadata(data.get("inputAsset")),
216
+ "output_asset": _normalize_token_metadata(data.get("outputAsset")),
217
+ "contracts": dict(data.get("contracts") or {}),
218
+ "referral_address": str(data.get("referralAddress") or "").strip() or None,
219
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
220
+ "hash": result.get("hash"),
221
+ "approve_hash": result.get("approveHash"),
222
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
223
+ "result": result,
224
+ "chain_id": int(data.get("chainId") or 0),
225
+ "source": "wdk-evm-wallet",
226
+ }
227
+
228
+
229
+ def _normalize_lido_withdrawal_payload(
230
+ *,
231
+ chain: str,
232
+ network: str,
233
+ wallet_id: str,
234
+ address: str,
235
+ operation: str,
236
+ amount_raw: str | None,
237
+ request_id: str | None,
238
+ data: dict[str, Any],
239
+ sign_only: bool,
240
+ ) -> dict[str, Any]:
241
+ result = dict(data.get("result") or {})
242
+ request = dict(data.get("operationRequest") or {})
243
+ resolved_amount = (
244
+ str(request.get("amount"))
245
+ if request.get("amount") is not None
246
+ else (str(amount_raw) if amount_raw is not None else None)
247
+ )
248
+ resolved_request_id = (
249
+ str(request.get("requestId"))
250
+ if request.get("requestId") is not None
251
+ else (str(request_id) if request_id is not None else None)
252
+ )
253
+ return {
254
+ "chain": chain,
255
+ "network": network,
256
+ "asset_type": "evm-lido-withdrawal-queue",
257
+ "asset": "ETH",
258
+ "wallet": wallet_id,
259
+ "from_address": str(data.get("address") or address),
260
+ "protocol": str(data.get("protocol") or "lido"),
261
+ "operation": str(data.get("operation") or operation),
262
+ "amount_raw": resolved_amount,
263
+ "amount_ui": str(data.get("amountFormatted")) if data.get("amountFormatted") is not None else None,
264
+ "request_id": resolved_request_id,
265
+ "queued_steth_amount_raw": (
266
+ str(data.get("queuedStEthAmountRaw")) if data.get("queuedStEthAmountRaw") is not None else None
267
+ ),
268
+ "queued_steth_amount_ui": (
269
+ str(data.get("queuedStEthAmountFormatted"))
270
+ if data.get("queuedStEthAmountFormatted") is not None
271
+ else None
272
+ ),
273
+ "estimated_fee_wei": str(data.get("estimatedFeeWei")) if data.get("estimatedFeeWei") is not None else None,
274
+ "estimated_operation_fee_wei": (
275
+ str(data.get("estimatedOperationFeeWei"))
276
+ if data.get("estimatedOperationFeeWei") is not None
277
+ else None
278
+ ),
279
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or "0"),
280
+ "fee_estimate_available": bool(data.get("feeEstimateAvailable", True)),
281
+ "fee_estimate_error": data.get("feeEstimateError"),
282
+ "execution_supported": not sign_only,
283
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
284
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
285
+ "input_asset": _normalize_token_metadata(data.get("inputAsset")),
286
+ "queue_asset": _normalize_token_metadata(data.get("queueAsset")),
287
+ "withdrawal_queue": str(data.get("withdrawalQueue") or "").strip() or None,
288
+ "withdrawal_request": dict(data.get("withdrawalRequest") or {}),
289
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
290
+ "hash": result.get("hash"),
291
+ "approve_hash": result.get("approveHash"),
292
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
293
+ "result": result,
294
+ "chain_id": int(data.get("chainId") or 0),
295
+ "source": "wdk-evm-wallet",
296
+ }
297
+
298
+
299
+ def _normalize_lifi_cross_chain_payload(
300
+ *,
301
+ chain: str,
302
+ network: str,
303
+ wallet_id: str,
304
+ data: dict[str, Any],
305
+ token_in: str,
306
+ destination_chain: str,
307
+ output_token: str,
308
+ destination_address: str,
309
+ amount_in_raw: str,
310
+ slippage: float | int | None,
311
+ sign_only: bool,
312
+ ) -> dict[str, Any]:
313
+ quote = dict(data.get("quote") or {})
314
+ estimate = dict(quote.get("estimate") or {})
315
+ return {
316
+ "chain": chain,
317
+ "network": network,
318
+ "asset_type": "evm-lifi-cross-chain-swap",
319
+ "asset": "EVM",
320
+ "wallet": wallet_id,
321
+ "from_address": str(data.get("address") or ""),
322
+ "source_chain": str(data.get("sourceChain") or network),
323
+ "destination_chain": str(destination_chain or data.get("destinationChain")),
324
+ "destination_chain_id": str(data.get("destinationChainId") or destination_chain),
325
+ "token_in": str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
326
+ "output_token": str((data.get("swapRequest") or {}).get("outputToken") or output_token),
327
+ "destination_address": str(
328
+ (data.get("swapRequest") or {}).get("destinationAddress") or destination_address
329
+ ),
330
+ "input_amount_raw": str((data.get("swapRequest") or {}).get("tokenInAmount") or amount_in_raw),
331
+ "input_amount_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
332
+ "estimated_output_amount_raw": str(estimate.get("toAmount") or data.get("minimumOutputAmountRaw") or "0"),
333
+ "estimated_output_amount_ui": (
334
+ str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None
335
+ ),
336
+ "estimated_fee_wei": str(data.get("estimatedFeeWei")) if data.get("estimatedFeeWei") is not None else None,
337
+ "estimated_swap_fee_wei": str(data.get("estimatedSwapFeeWei")) if data.get("estimatedSwapFeeWei") is not None else None,
338
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or "0"),
339
+ "fee_estimate_available": bool(data.get("feeEstimateAvailable", True)),
340
+ "fee_estimate_error": data.get("feeEstimateError"),
341
+ "slippage": data.get("slippage") if data.get("slippage") is not None else slippage,
342
+ "minimum_output_amount_raw": str(data.get("minimumOutputAmountRaw") or estimate.get("toAmountMin") or "0"),
343
+ "swap_provider": str(data.get("protocol") or "lifi"),
344
+ "execution_supported": bool(data.get("executionSupported")) and not sign_only,
345
+ "route_plan": quote,
346
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
347
+ "router": str(data.get("router") or "").strip() or None,
348
+ "quote_type": str(data.get("quoteType") or quote.get("type") or "").strip() or None,
349
+ "quote_id": str(data.get("quoteId") or quote.get("id") or "").strip() or None,
350
+ "tool": str(data.get("tool") or quote.get("tool") or "").strip() or None,
351
+ "tool_details": data.get("toolDetails") or quote.get("toolDetails"),
352
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
353
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
354
+ "swap_transaction": {
355
+ "to": str((data.get("swapTransaction") or {}).get("to") or "").strip() or None,
356
+ "value": str((data.get("swapTransaction") or {}).get("value") or "0"),
357
+ "data_hash": str((data.get("swapTransaction") or {}).get("dataHash") or "").strip() or None,
358
+ },
359
+ "token_in_metadata": _normalize_token_metadata(
360
+ data.get("tokenInMetadata"),
361
+ str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
362
+ ),
363
+ "output_token_metadata": _normalize_token_metadata(
364
+ data.get("outputTokenMetadata"),
365
+ str((data.get("swapRequest") or {}).get("outputToken") or output_token),
366
+ ),
367
+ "quote": quote,
368
+ "chain_id": int(data.get("chainId") or 0),
369
+ "source": "wdk-evm-wallet",
370
+ }
371
+
372
+
373
+ def _sanitize_provider_url(value: Any) -> str | None:
374
+ raw = str(value or "").strip()
375
+ if not raw:
376
+ return None
377
+ try:
378
+ split = urlsplit(raw)
379
+ except Exception:
380
+ return raw
381
+ query = []
382
+ for key, item in parse_qsl(split.query, keep_blank_values=True):
383
+ if key.lower() in {"token", "apikey", "api_key"}:
384
+ query.append((key, "***"))
385
+ else:
386
+ query.append((key, item))
387
+ return urlunsplit((split.scheme, split.netloc, split.path, urlencode(query), split.fragment))
388
+
389
+
390
+ class WdkEvmLocalWalletBackend(AgentWalletBackend):
391
+ """EVM backend that delegates signing and execution to a local WDK service."""
392
+
393
+ name = "wdk_evm_local"
394
+
395
+ def __init__(
396
+ self,
397
+ *,
398
+ service_url: str,
399
+ wallet_id: str,
400
+ network: str,
401
+ account_index: int = 0,
402
+ sign_only: bool = False,
403
+ address: str | None = None,
404
+ ):
405
+ self.client = WdkEvmLocalClient(service_url)
406
+ self.wallet_id = str(wallet_id or "").strip()
407
+ if not self.wallet_id:
408
+ raise WalletBackendError("WDK EVM wallet id is not configured.")
409
+ self.network = _normalize_evm_network(network)
410
+ self.account_index = int(account_index)
411
+ self.sign_only = bool(sign_only)
412
+ self.address = address.strip() if isinstance(address, str) and address.strip() else None
413
+ self.chain = "evm"
414
+ self.custody_model = "local_service_vault"
415
+
416
+ def with_network(self, network: str) -> "WdkEvmLocalWalletBackend":
417
+ return WdkEvmLocalWalletBackend(
418
+ service_url=self.client.base_url,
419
+ wallet_id=self.wallet_id,
420
+ network=_normalize_evm_network(network),
421
+ account_index=self.account_index,
422
+ sign_only=self.sign_only,
423
+ address=self.address,
424
+ )
425
+
426
+ async def get_address(self) -> str | None:
427
+ if self.address:
428
+ return self.address
429
+ data = await self.client.post(
430
+ "/v1/evm/address/resolve",
431
+ {
432
+ "walletId": self.wallet_id,
433
+ "accountIndex": self.account_index,
434
+ "network": self.network,
435
+ },
436
+ )
437
+ address = str(data.get("address") or "").strip()
438
+ if not address:
439
+ raise WalletBackendError("wdk-evm-wallet did not return an address.")
440
+ self.address = address
441
+ return address
442
+
443
+ async def get_balance(self, address: str | None = None) -> dict[str, Any]:
444
+ resolved_address = await self.get_address()
445
+ if address is not None and address.strip() and address.strip() != resolved_address:
446
+ raise WalletBackendError(
447
+ "wdk_evm_local only supports the configured default EVM account address."
448
+ )
449
+ data = await self.client.post(
450
+ "/v1/evm/balance/get",
451
+ {
452
+ "walletId": self.wallet_id,
453
+ "address": resolved_address,
454
+ "accountIndex": self.account_index,
455
+ "network": self.network,
456
+ },
457
+ )
458
+ result = {
459
+ "chain": self.chain,
460
+ "network": self.network,
461
+ "address": str(data.get("address") or resolved_address or ""),
462
+ "balance_wei": str(data.get("balance") or "0"),
463
+ "balance_native": str(data.get("balanceFormatted") or "0"),
464
+ "asset": str(data.get("nativeSymbol") or "ETH"),
465
+ "chain_id": int(data.get("chainId") or 0),
466
+ "source": "wdk-evm-wallet",
467
+ }
468
+ try:
469
+ portfolio = await build_portfolio_snapshot(
470
+ address=result["address"],
471
+ network=self.network,
472
+ native_symbol=result["asset"],
473
+ native_balance_wei=result["balance_wei"],
474
+ native_balance=result["balance_native"],
475
+ )
476
+ except Exception as exc:
477
+ result["portfolio_error"] = str(exc)
478
+ result["tokens"] = []
479
+ result["token_count"] = 0
480
+ result["assets"] = [
481
+ {
482
+ "asset_type": "native",
483
+ "symbol": result["asset"],
484
+ "amount_raw": result["balance_wei"],
485
+ "amount_ui": result["balance_native"],
486
+ "price_usd": None,
487
+ "value_usd": None,
488
+ "pricing_source": None,
489
+ }
490
+ ]
491
+ result["asset_count"] = 1
492
+ result["priced_asset_count"] = 0
493
+ result["balance_usd"] = None
494
+ result["total_value_usd"] = None
495
+ result["native_price_usd"] = None
496
+ result["native_value_usd"] = None
497
+ return result
498
+
499
+ result.update(
500
+ {
501
+ "native_price_usd": portfolio.get("native_price_usd"),
502
+ "native_value_usd": portfolio.get("native_value_usd"),
503
+ "tokens": list(portfolio.get("tokens") or []),
504
+ "token_count": int(portfolio.get("token_count") or 0),
505
+ "assets": list(portfolio.get("assets") or []),
506
+ "asset_count": int(portfolio.get("asset_count") or 0),
507
+ "priced_asset_count": int(portfolio.get("priced_asset_count") or 0),
508
+ "balance_usd": portfolio.get("balance_usd"),
509
+ "total_value_usd": portfolio.get("total_value_usd"),
510
+ "pricing_source": portfolio.get("pricing_source"),
511
+ "token_discovery_source": portfolio.get("token_discovery_source"),
512
+ }
513
+ )
514
+ return result
515
+
516
+ async def get_evm_network_info(self) -> dict[str, Any]:
517
+ data = await self.client.get("/v1/evm/network")
518
+ profiles = data.get("profiles") or {}
519
+ return {
520
+ "chain": self.chain,
521
+ "network": self.network,
522
+ "configured_network": self.network,
523
+ "service_active_network": str(data.get("activeNetwork") or "").strip() or None,
524
+ "available_networks": sorted(str(key) for key in profiles.keys()),
525
+ "agent_selectable_networks": ["ethereum", "base"],
526
+ "swap_supported_networks": ["ethereum", "base"],
527
+ "network_profiles": {
528
+ str(network): {
529
+ **dict(profile),
530
+ "providerUrl": _sanitize_provider_url((profile or {}).get("providerUrl")),
531
+ }
532
+ for network, profile in profiles.items()
533
+ if isinstance(profile, dict)
534
+ },
535
+ "selected_profile": {
536
+ **dict(data.get("selectedProfile") or {}),
537
+ "providerUrl": _sanitize_provider_url((data.get("selectedProfile") or {}).get("providerUrl")),
538
+ }
539
+ if isinstance(data.get("selectedProfile"), dict)
540
+ else data.get("selectedProfile"),
541
+ "source": "wdk-evm-wallet",
542
+ }
543
+
544
+ async def get_lifi_supported_chains(self) -> dict[str, Any]:
545
+ chains = await lifi.fetch_supported_chains()
546
+ supported = lifi.format_openclaw_supported_chains(chains)
547
+ return {
548
+ "provider": "lifi",
549
+ "chain": "cross-chain",
550
+ "network": "mainnet",
551
+ "supported_by_openclaw": lifi.OPENCLAW_SUPPORTED_CHAINS,
552
+ "chain_count": len(supported),
553
+ "chains": supported,
554
+ "source": "lifi",
555
+ }
556
+
557
+ async def get_lifi_quote(
558
+ self,
559
+ *,
560
+ from_chain: str,
561
+ to_chain: str,
562
+ from_token: str,
563
+ to_token: str,
564
+ amount_in_raw: str,
565
+ from_address: str | None = None,
566
+ to_address: str | None = None,
567
+ slippage: float | int | None = None,
568
+ allow_bridges: list[str] | None = None,
569
+ deny_bridges: list[str] | None = None,
570
+ prefer_bridges: list[str] | None = None,
571
+ ) -> dict[str, Any]:
572
+ from_chain_id = lifi.normalize_chain_id(from_chain, field_name="from_chain")
573
+ to_chain_id = lifi.normalize_chain_id(to_chain, field_name="to_chain")
574
+ current_chain_id = _lifi_chain_id_for_evm_network(self.network)
575
+ resolved_from_address = str(from_address or "").strip()
576
+ resolved_to_address = str(to_address or "").strip()
577
+ wallet_address: str | None = None
578
+ if from_chain_id == current_chain_id and not resolved_from_address:
579
+ wallet_address = await self.get_address()
580
+ resolved_from_address = str(wallet_address or "").strip()
581
+ if to_chain_id == current_chain_id and not resolved_to_address:
582
+ wallet_address = wallet_address or await self.get_address()
583
+ resolved_to_address = str(wallet_address or "").strip()
584
+ if not resolved_from_address:
585
+ raise WalletBackendError("from_address is required when the LI.FI source chain is not the active EVM network.")
586
+ if not resolved_to_address:
587
+ raise WalletBackendError("to_address is required when the LI.FI destination chain is not the active EVM network.")
588
+
589
+ payload = await lifi.fetch_quote(
590
+ from_chain=from_chain_id,
591
+ to_chain=to_chain_id,
592
+ from_token=from_token,
593
+ to_token=to_token,
594
+ amount_in_raw=amount_in_raw,
595
+ from_address=resolved_from_address,
596
+ to_address=resolved_to_address,
597
+ slippage=slippage,
598
+ allow_bridges=allow_bridges,
599
+ deny_bridges=deny_bridges,
600
+ prefer_bridges=prefer_bridges,
601
+ )
602
+ return {
603
+ "provider": "lifi",
604
+ "chain": "cross-chain",
605
+ "network": "mainnet",
606
+ "active_evm_network": self.network,
607
+ "from_chain": lifi.chain_name_for_id(from_chain_id),
608
+ "to_chain": lifi.chain_name_for_id(to_chain_id),
609
+ "from_chain_id": from_chain_id,
610
+ "to_chain_id": to_chain_id,
611
+ "from_token": lifi.normalize_token_address(from_token, chain_id=from_chain_id),
612
+ "to_token": lifi.normalize_token_address(to_token, chain_id=to_chain_id),
613
+ "amount_in_raw": amount_in_raw,
614
+ "from_address": resolved_from_address,
615
+ "to_address": resolved_to_address,
616
+ "slippage": slippage,
617
+ "allow_bridges": allow_bridges,
618
+ "deny_bridges": deny_bridges,
619
+ "prefer_bridges": prefer_bridges,
620
+ "tool": payload.get("tool"),
621
+ "tool_details": payload.get("toolDetails"),
622
+ "action": payload.get("action"),
623
+ "estimate": payload.get("estimate"),
624
+ "included_steps": payload.get("includedSteps"),
625
+ "transaction_request": payload.get("transactionRequest"),
626
+ "quote": payload,
627
+ "source": "lifi",
628
+ }
629
+
630
+ async def get_lifi_transfer_status(
631
+ self,
632
+ *,
633
+ tx_hash: str,
634
+ bridge: str | None = None,
635
+ from_chain: str | None = None,
636
+ to_chain: str | None = None,
637
+ ) -> dict[str, Any]:
638
+ payload = await lifi.fetch_transfer_status(
639
+ tx_hash=tx_hash,
640
+ bridge=bridge,
641
+ from_chain=from_chain,
642
+ to_chain=to_chain,
643
+ )
644
+ return {
645
+ "provider": "lifi",
646
+ "chain": "cross-chain",
647
+ "network": "mainnet",
648
+ "tx_hash": tx_hash,
649
+ "bridge": bridge,
650
+ "from_chain": from_chain,
651
+ "to_chain": to_chain,
652
+ "status": payload.get("status"),
653
+ "substatus": payload.get("substatus"),
654
+ "sending": payload.get("sending"),
655
+ "receiving": payload.get("receiving"),
656
+ "transfer": payload,
657
+ "source": "lifi",
658
+ }
659
+
660
+ async def get_evm_token_balance(self, token_address: str) -> dict[str, Any]:
661
+ resolved_address = await self.get_address()
662
+ data = await self.client.post(
663
+ "/v1/evm/token-balance/get",
664
+ {
665
+ "walletId": self.wallet_id,
666
+ "address": resolved_address,
667
+ "accountIndex": self.account_index,
668
+ "network": self.network,
669
+ "tokenAddress": token_address,
670
+ },
671
+ )
672
+ return {
673
+ "chain": self.chain,
674
+ "network": self.network,
675
+ "address": str(data.get("address") or resolved_address or ""),
676
+ "token_address": str(data.get("tokenAddress") or token_address),
677
+ "balance_raw": str(data.get("balance") or "0"),
678
+ "balance_ui": str(data.get("balanceFormatted")) if data.get("balanceFormatted") is not None else None,
679
+ "token_metadata": _normalize_token_metadata(
680
+ data.get("tokenMetadata"),
681
+ str(data.get("tokenAddress") or token_address),
682
+ ),
683
+ "chain_id": int(data.get("chainId") or 0),
684
+ "source": "wdk-evm-wallet",
685
+ }
686
+
687
+ async def get_evm_token_metadata(self, token_address: str) -> dict[str, Any]:
688
+ data = await self.client.post(
689
+ "/v1/evm/token-metadata/get",
690
+ {
691
+ "network": self.network,
692
+ "tokenAddress": token_address,
693
+ },
694
+ )
695
+ resolved = _normalize_token_metadata(
696
+ data.get("tokenMetadata"),
697
+ str(data.get("tokenAddress") or token_address),
698
+ )
699
+ return {
700
+ "chain": self.chain,
701
+ "network": self.network,
702
+ "token_address": str(data.get("tokenAddress") or token_address),
703
+ "token_metadata": resolved,
704
+ "chain_id": int(data.get("chainId") or 0),
705
+ "source": "wdk-evm-wallet",
706
+ }
707
+
708
+ async def get_evm_fee_rates(self) -> dict[str, Any]:
709
+ data = await self.client.post(
710
+ "/v1/evm/fee-rates/get",
711
+ {"network": self.network},
712
+ )
713
+ return {
714
+ "chain": self.chain,
715
+ "network": self.network,
716
+ "chain_id": int(data.get("chainId") or 0),
717
+ "gas_price_wei": str(data.get("gasPrice") or "0"),
718
+ "fee_rates": data.get("feeRates") or {},
719
+ "source": "wdk-evm-wallet",
720
+ }
721
+
722
+ async def get_evm_transaction_receipt(self, tx_hash: str) -> dict[str, Any]:
723
+ data = await self.client.post(
724
+ "/v1/evm/transaction/receipt/get",
725
+ {
726
+ "network": self.network,
727
+ "txHash": tx_hash,
728
+ },
729
+ )
730
+ return {
731
+ "chain": self.chain,
732
+ "network": self.network,
733
+ "chain_id": int(data.get("chainId") or 0),
734
+ "tx_hash": tx_hash,
735
+ "found": bool(data.get("found")),
736
+ "receipt": data.get("receipt"),
737
+ "source": "wdk-evm-wallet",
738
+ }
739
+
740
+ async def get_evm_aave_account(self) -> dict[str, Any]:
741
+ resolved_address = await self.get_address()
742
+ data = await self.client.post(
743
+ "/v1/evm/aave/account/get",
744
+ {
745
+ "walletId": self.wallet_id,
746
+ "address": resolved_address,
747
+ "accountIndex": self.account_index,
748
+ "network": self.network,
749
+ },
750
+ )
751
+ return {
752
+ "chain": self.chain,
753
+ "network": self.network,
754
+ "address": str(data.get("address") or resolved_address or ""),
755
+ "protocol": str(data.get("protocol") or "aave-v3"),
756
+ "account_data": dict(data.get("accountData") or {}),
757
+ "chain_id": int(data.get("chainId") or 0),
758
+ "source": "wdk-evm-wallet",
759
+ }
760
+
761
+ async def get_evm_aave_reserves(self) -> dict[str, Any]:
762
+ data = await self.client.post(
763
+ "/v1/evm/aave/reserves/get",
764
+ {
765
+ "walletId": self.wallet_id,
766
+ "accountIndex": self.account_index,
767
+ "network": self.network,
768
+ },
769
+ )
770
+ return {
771
+ "chain": self.chain,
772
+ "network": self.network,
773
+ "protocol": str(data.get("protocol") or "aave-v3"),
774
+ "chain_id": int(data.get("chainId") or 0),
775
+ "pool": str(data.get("pool") or "").strip() or None,
776
+ "pool_addresses_provider": str(data.get("poolAddressesProvider") or "").strip() or None,
777
+ "ui_pool_data_provider": str(data.get("uiPoolDataProvider") or "").strip() or None,
778
+ "price_oracle": str(data.get("priceOracle") or "").strip() or None,
779
+ "base_currency_info": dict(data.get("baseCurrencyInfo") or {}),
780
+ "reserve_count": int(data.get("reserveCount") or 0),
781
+ "reserves": list(data.get("reserves") or []),
782
+ "source": "wdk-evm-wallet",
783
+ }
784
+
785
+ async def get_evm_aave_positions(self) -> dict[str, Any]:
786
+ resolved_address = await self.get_address()
787
+ data = await self.client.post(
788
+ "/v1/evm/aave/positions/get",
789
+ {
790
+ "walletId": self.wallet_id,
791
+ "address": resolved_address,
792
+ "accountIndex": self.account_index,
793
+ "network": self.network,
794
+ },
795
+ )
796
+ return {
797
+ "chain": self.chain,
798
+ "network": self.network,
799
+ "address": str(data.get("address") or resolved_address or ""),
800
+ "protocol": str(data.get("protocol") or "aave-v3"),
801
+ "chain_id": int(data.get("chainId") or 0),
802
+ "emode_category_id": str(data.get("eModeCategoryId") or "0"),
803
+ "account_data": dict(data.get("accountData") or {}),
804
+ "base_currency_info": dict(data.get("baseCurrencyInfo") or {}),
805
+ "position_count": int(data.get("positionCount") or 0),
806
+ "positions": list(data.get("positions") or []),
807
+ "source": "wdk-evm-wallet",
808
+ }
809
+
810
+ async def get_evm_lido_overview(self) -> dict[str, Any]:
811
+ resolved_address = await self.get_address()
812
+ data = await self.client.post(
813
+ "/v1/evm/lido/overview/get",
814
+ {
815
+ "walletId": self.wallet_id,
816
+ "address": resolved_address,
817
+ "accountIndex": self.account_index,
818
+ "network": self.network,
819
+ },
820
+ )
821
+ return {
822
+ "chain": self.chain,
823
+ "network": self.network,
824
+ "protocol": str(data.get("protocol") or "lido"),
825
+ "preferred_position_token": str(data.get("preferredPositionToken") or "wstETH"),
826
+ "chain_id": int(data.get("chainId") or 0),
827
+ "staking_asset": dict(data.get("stakingAsset") or {}),
828
+ "referral_address": str(data.get("referralAddress") or "").strip() or None,
829
+ "contracts": dict(data.get("contracts") or {}),
830
+ "steth_metadata": dict(data.get("stEthMetadata") or {}),
831
+ "wsteth_metadata": dict(data.get("wstEthMetadata") or {}),
832
+ "sample_rates": dict(data.get("sampleRates") or {}),
833
+ "staking_apr": dict(data.get("stakingApr") or {}) if isinstance(data.get("stakingApr"), dict) else None,
834
+ "staking_apr_error": (
835
+ dict(data.get("stakingAprError") or {})
836
+ if isinstance(data.get("stakingAprError"), dict)
837
+ else None
838
+ ),
839
+ "source": "wdk-evm-wallet",
840
+ }
841
+
842
+ async def get_evm_lido_positions(self) -> dict[str, Any]:
843
+ resolved_address = await self.get_address()
844
+ data = await self.client.post(
845
+ "/v1/evm/lido/positions/get",
846
+ {
847
+ "walletId": self.wallet_id,
848
+ "address": resolved_address,
849
+ "accountIndex": self.account_index,
850
+ "network": self.network,
851
+ },
852
+ )
853
+ return {
854
+ "chain": self.chain,
855
+ "network": self.network,
856
+ "address": str(data.get("address") or resolved_address or ""),
857
+ "protocol": str(data.get("protocol") or "lido"),
858
+ "preferred_position_token": str(data.get("preferredPositionToken") or "wstETH"),
859
+ "chain_id": int(data.get("chainId") or 0),
860
+ "contracts": dict(data.get("contracts") or {}),
861
+ "native_balance_wei": str(data.get("nativeBalanceWei") or "0"),
862
+ "native_balance_ui": str(data.get("nativeBalanceFormatted") or "0"),
863
+ "steth_equivalent_total_raw": str(data.get("stEthEquivalentTotalRaw") or "0"),
864
+ "steth_equivalent_total_ui": str(data.get("stEthEquivalentTotalFormatted") or "0"),
865
+ "position_count": int(data.get("positionCount") or 0),
866
+ "positions": list(data.get("positions") or []),
867
+ "source": "wdk-evm-wallet",
868
+ }
869
+
870
+ async def get_evm_lido_withdrawal_requests(self) -> dict[str, Any]:
871
+ resolved_address = await self.get_address()
872
+ data = await self.client.post(
873
+ "/v1/evm/lido/withdrawals/get",
874
+ {
875
+ "walletId": self.wallet_id,
876
+ "address": resolved_address,
877
+ "accountIndex": self.account_index,
878
+ "network": self.network,
879
+ },
880
+ )
881
+ return {
882
+ "chain": self.chain,
883
+ "network": self.network,
884
+ "address": str(data.get("address") or resolved_address or ""),
885
+ "protocol": str(data.get("protocol") or "lido"),
886
+ "chain_id": int(data.get("chainId") or 0),
887
+ "withdrawal_queue": str(data.get("withdrawalQueue") or "").strip() or None,
888
+ "request_count": int(data.get("requestCount") or 0),
889
+ "claimable_count": int(data.get("claimableCount") or 0),
890
+ "requests": list(data.get("requests") or []),
891
+ "source": "wdk-evm-wallet",
892
+ }
893
+
894
+ async def preview_evm_aave_operation(
895
+ self,
896
+ *,
897
+ operation: str,
898
+ token_address: str,
899
+ amount_raw: str,
900
+ ) -> dict[str, Any]:
901
+ normalized_operation = _normalize_aave_operation(operation)
902
+ resolved_address = await self.get_address()
903
+ data = await self.client.post(
904
+ f"/v1/evm/aave/{normalized_operation}/quote",
905
+ {
906
+ "walletId": self.wallet_id,
907
+ "address": resolved_address,
908
+ "accountIndex": self.account_index,
909
+ "network": self.network,
910
+ "tokenAddress": token_address,
911
+ "amount": amount_raw,
912
+ },
913
+ )
914
+ return _normalize_aave_payload(
915
+ chain=self.chain,
916
+ network=self.network,
917
+ wallet_id=self.wallet_id,
918
+ address=resolved_address,
919
+ operation=normalized_operation,
920
+ token_address=token_address,
921
+ amount_raw=amount_raw,
922
+ data=data,
923
+ sign_only=self.sign_only,
924
+ )
925
+
926
+ async def send_evm_aave_operation(
927
+ self,
928
+ *,
929
+ operation: str,
930
+ token_address: str,
931
+ amount_raw: str,
932
+ expected_quote_fingerprint: str | None = None,
933
+ ) -> dict[str, Any]:
934
+ if self.sign_only:
935
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
936
+ normalized_operation = _normalize_aave_operation(operation)
937
+ data = await self.client.post(
938
+ f"/v1/evm/aave/{normalized_operation}/send",
939
+ {
940
+ "walletId": self.wallet_id,
941
+ "accountIndex": self.account_index,
942
+ "network": self.network,
943
+ "tokenAddress": token_address,
944
+ "amount": amount_raw,
945
+ **(
946
+ {"expectedQuoteFingerprint": expected_quote_fingerprint}
947
+ if isinstance(expected_quote_fingerprint, str) and expected_quote_fingerprint.strip()
948
+ else {}
949
+ ),
950
+ },
951
+ )
952
+ return {
953
+ **_normalize_aave_payload(
954
+ chain=self.chain,
955
+ network=self.network,
956
+ wallet_id=self.wallet_id,
957
+ address=await self.get_address(),
958
+ operation=normalized_operation,
959
+ token_address=token_address,
960
+ amount_raw=amount_raw,
961
+ data=data,
962
+ sign_only=self.sign_only,
963
+ ),
964
+ "broadcasted": True,
965
+ "confirmed": False,
966
+ }
967
+
968
+ async def preview_evm_lido_operation(
969
+ self,
970
+ *,
971
+ operation: str,
972
+ amount_raw: str,
973
+ ) -> dict[str, Any]:
974
+ normalized_operation = _normalize_lido_operation(operation)
975
+ resolved_address = await self.get_address()
976
+ data = await self.client.post(
977
+ f"/v1/evm/lido/{normalized_operation}/quote",
978
+ {
979
+ "walletId": self.wallet_id,
980
+ "address": resolved_address,
981
+ "accountIndex": self.account_index,
982
+ "network": self.network,
983
+ "amount": amount_raw,
984
+ },
985
+ )
986
+ return _normalize_lido_payload(
987
+ chain=self.chain,
988
+ network=self.network,
989
+ wallet_id=self.wallet_id,
990
+ address=resolved_address,
991
+ operation=normalized_operation,
992
+ amount_raw=amount_raw,
993
+ data=data,
994
+ sign_only=self.sign_only,
995
+ )
996
+
997
+ async def send_evm_lido_operation(
998
+ self,
999
+ *,
1000
+ operation: str,
1001
+ amount_raw: str,
1002
+ expected_quote_fingerprint: str | None = None,
1003
+ ) -> dict[str, Any]:
1004
+ if self.sign_only:
1005
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1006
+ normalized_operation = _normalize_lido_operation(operation)
1007
+ data = await self.client.post(
1008
+ f"/v1/evm/lido/{normalized_operation}/send",
1009
+ {
1010
+ "walletId": self.wallet_id,
1011
+ "accountIndex": self.account_index,
1012
+ "network": self.network,
1013
+ "amount": amount_raw,
1014
+ **(
1015
+ {"expectedQuoteFingerprint": expected_quote_fingerprint}
1016
+ if isinstance(expected_quote_fingerprint, str) and expected_quote_fingerprint.strip()
1017
+ else {}
1018
+ ),
1019
+ },
1020
+ )
1021
+ return {
1022
+ **_normalize_lido_payload(
1023
+ chain=self.chain,
1024
+ network=self.network,
1025
+ wallet_id=self.wallet_id,
1026
+ address=await self.get_address(),
1027
+ operation=normalized_operation,
1028
+ amount_raw=amount_raw,
1029
+ data=data,
1030
+ sign_only=self.sign_only,
1031
+ ),
1032
+ "broadcasted": True,
1033
+ "confirmed": False,
1034
+ }
1035
+
1036
+ async def preview_evm_lido_withdrawal(
1037
+ self,
1038
+ *,
1039
+ operation: str,
1040
+ amount_raw: str | None = None,
1041
+ request_id: str | None = None,
1042
+ ) -> dict[str, Any]:
1043
+ normalized_operation = _normalize_lido_withdrawal_operation(operation)
1044
+ resolved_address = await self.get_address()
1045
+ payload: dict[str, Any] = {
1046
+ "walletId": self.wallet_id,
1047
+ "address": resolved_address,
1048
+ "accountIndex": self.account_index,
1049
+ "network": self.network,
1050
+ }
1051
+ if amount_raw is not None:
1052
+ payload["amount"] = amount_raw
1053
+ if request_id is not None:
1054
+ payload["requestId"] = request_id
1055
+ data = await self.client.post(
1056
+ f"/v1/evm/lido/{normalized_operation}/quote",
1057
+ payload,
1058
+ )
1059
+ return _normalize_lido_withdrawal_payload(
1060
+ chain=self.chain,
1061
+ network=self.network,
1062
+ wallet_id=self.wallet_id,
1063
+ address=resolved_address,
1064
+ operation=normalized_operation,
1065
+ amount_raw=amount_raw,
1066
+ request_id=request_id,
1067
+ data=data,
1068
+ sign_only=self.sign_only,
1069
+ )
1070
+
1071
+ async def send_evm_lido_withdrawal(
1072
+ self,
1073
+ *,
1074
+ operation: str,
1075
+ amount_raw: str | None = None,
1076
+ request_id: str | None = None,
1077
+ expected_quote_fingerprint: str | None = None,
1078
+ ) -> dict[str, Any]:
1079
+ if self.sign_only:
1080
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1081
+ normalized_operation = _normalize_lido_withdrawal_operation(operation)
1082
+ payload: dict[str, Any] = {
1083
+ "walletId": self.wallet_id,
1084
+ "accountIndex": self.account_index,
1085
+ "network": self.network,
1086
+ }
1087
+ if amount_raw is not None:
1088
+ payload["amount"] = amount_raw
1089
+ if request_id is not None:
1090
+ payload["requestId"] = request_id
1091
+ if isinstance(expected_quote_fingerprint, str) and expected_quote_fingerprint.strip():
1092
+ payload["expectedQuoteFingerprint"] = expected_quote_fingerprint
1093
+ data = await self.client.post(
1094
+ f"/v1/evm/lido/{normalized_operation}/send",
1095
+ payload,
1096
+ )
1097
+ return {
1098
+ **_normalize_lido_withdrawal_payload(
1099
+ chain=self.chain,
1100
+ network=self.network,
1101
+ wallet_id=self.wallet_id,
1102
+ address=await self.get_address(),
1103
+ operation=normalized_operation,
1104
+ amount_raw=amount_raw,
1105
+ request_id=request_id,
1106
+ data=data,
1107
+ sign_only=self.sign_only,
1108
+ ),
1109
+ "broadcasted": True,
1110
+ "confirmed": False,
1111
+ }
1112
+
1113
+ async def get_evm_swap_quote(
1114
+ self,
1115
+ *,
1116
+ token_in: str,
1117
+ token_out: str,
1118
+ amount_in_raw: str,
1119
+ ) -> dict[str, Any]:
1120
+ resolved_address = await self.get_address()
1121
+ data = await self.client.post(
1122
+ "/v1/evm/swap/quote",
1123
+ {
1124
+ "walletId": self.wallet_id,
1125
+ "address": resolved_address,
1126
+ "accountIndex": self.account_index,
1127
+ "network": self.network,
1128
+ "tokenIn": token_in,
1129
+ "tokenOut": token_out,
1130
+ "tokenInAmount": amount_in_raw,
1131
+ },
1132
+ )
1133
+ quote = dict(data.get("quote") or {})
1134
+ return {
1135
+ "chain": self.chain,
1136
+ "network": self.network,
1137
+ "address": str(data.get("address") or resolved_address or ""),
1138
+ "token_in": str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
1139
+ "token_out": str((data.get("swapRequest") or {}).get("tokenOut") or token_out),
1140
+ "amount_in_raw": str((data.get("swapRequest") or {}).get("tokenInAmount") or amount_in_raw),
1141
+ "amount_in_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1142
+ "estimated_output_amount_raw": str(quote.get("tokenOutAmount") or "0"),
1143
+ "estimated_output_amount_ui": (
1144
+ str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None
1145
+ ),
1146
+ "quote": quote,
1147
+ "protocol": str(data.get("protocol") or "velora"),
1148
+ "execution_supported": bool(data.get("executionSupported")) and not self.sign_only,
1149
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1150
+ "router": str(data.get("router") or "").strip() or None,
1151
+ "estimated_fee_wei": (
1152
+ str(data.get("estimatedFeeWei"))
1153
+ if data.get("estimatedFeeWei") is not None
1154
+ else (_extract_fee_wei(quote) if _extract_fee_wei(quote) is not None else None)
1155
+ ),
1156
+ "estimated_swap_fee_wei": (
1157
+ str(data.get("estimatedSwapFeeWei"))
1158
+ if data.get("estimatedSwapFeeWei") is not None
1159
+ else (_extract_fee_wei(quote) if _extract_fee_wei(quote) is not None else None)
1160
+ ),
1161
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or "0"),
1162
+ "fee_estimate_available": bool(data.get("feeEstimateAvailable", True)),
1163
+ "fee_estimate_error": data.get("feeEstimateError"),
1164
+ "slippage_bps": int(data.get("slippageBps") or 0) if data.get("slippageBps") is not None else None,
1165
+ "minimum_output_amount_raw": (
1166
+ str(data.get("minimumOutputAmountRaw"))
1167
+ if data.get("minimumOutputAmountRaw") is not None
1168
+ else None
1169
+ ),
1170
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1171
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
1172
+ "swap_transaction": {
1173
+ "to": str((data.get("swapTransaction") or {}).get("to") or "").strip() or None,
1174
+ "value": str((data.get("swapTransaction") or {}).get("value") or "0"),
1175
+ "data_hash": str((data.get("swapTransaction") or {}).get("dataHash") or "").strip() or None,
1176
+ },
1177
+ "token_in_metadata": _normalize_token_metadata(
1178
+ data.get("tokenInMetadata"),
1179
+ str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
1180
+ ),
1181
+ "token_out_metadata": _normalize_token_metadata(
1182
+ data.get("tokenOutMetadata"),
1183
+ str((data.get("swapRequest") or {}).get("tokenOut") or token_out),
1184
+ ),
1185
+ "chain_id": int(data.get("chainId") or 0),
1186
+ "source": "wdk-evm-wallet",
1187
+ }
1188
+
1189
+ async def preview_evm_swap(
1190
+ self,
1191
+ *,
1192
+ token_in: str,
1193
+ token_out: str,
1194
+ amount_in_raw: str,
1195
+ ) -> dict[str, Any]:
1196
+ resolved_address = await self.get_address()
1197
+ data = await self.client.post(
1198
+ "/v1/evm/swap/quote",
1199
+ {
1200
+ "walletId": self.wallet_id,
1201
+ "address": resolved_address,
1202
+ "accountIndex": self.account_index,
1203
+ "network": self.network,
1204
+ "tokenIn": token_in,
1205
+ "tokenOut": token_out,
1206
+ "tokenInAmount": amount_in_raw,
1207
+ },
1208
+ )
1209
+ quote = dict(data.get("quote") or {})
1210
+ return {
1211
+ "chain": self.chain,
1212
+ "network": self.network,
1213
+ "asset_type": "evm-swap",
1214
+ "asset": "ERC20",
1215
+ "wallet": self.wallet_id,
1216
+ "from_address": resolved_address,
1217
+ "token_in": str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
1218
+ "token_out": str((data.get("swapRequest") or {}).get("tokenOut") or token_out),
1219
+ "input_amount_raw": str((data.get("swapRequest") or {}).get("tokenInAmount") or amount_in_raw),
1220
+ "input_amount_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1221
+ "estimated_output_amount_raw": str(quote.get("tokenOutAmount") or "0"),
1222
+ "estimated_output_amount_ui": (
1223
+ str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None
1224
+ ),
1225
+ "estimated_fee_wei": (
1226
+ str(data.get("estimatedFeeWei"))
1227
+ if data.get("estimatedFeeWei") is not None
1228
+ else (_extract_fee_wei(quote) if _extract_fee_wei(quote) is not None else None)
1229
+ ),
1230
+ "estimated_swap_fee_wei": (
1231
+ str(data.get("estimatedSwapFeeWei"))
1232
+ if data.get("estimatedSwapFeeWei") is not None
1233
+ else (_extract_fee_wei(quote) if _extract_fee_wei(quote) is not None else None)
1234
+ ),
1235
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or "0"),
1236
+ "fee_estimate_available": bool(data.get("feeEstimateAvailable", True)),
1237
+ "fee_estimate_error": data.get("feeEstimateError"),
1238
+ "slippage_bps": int(data.get("slippageBps") or 0) if data.get("slippageBps") is not None else None,
1239
+ "minimum_output_amount_raw": (
1240
+ str(data.get("minimumOutputAmountRaw"))
1241
+ if data.get("minimumOutputAmountRaw") is not None
1242
+ else None
1243
+ ),
1244
+ "swap_provider": str(data.get("protocol") or "velora"),
1245
+ "execution_supported": bool(data.get("executionSupported")) and not self.sign_only,
1246
+ "route_plan": _normalize_swap_route(quote),
1247
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1248
+ "router": str(data.get("router") or "").strip() or None,
1249
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1250
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
1251
+ "swap_transaction": {
1252
+ "to": str((data.get("swapTransaction") or {}).get("to") or "").strip() or None,
1253
+ "value": str((data.get("swapTransaction") or {}).get("value") or "0"),
1254
+ "data_hash": str((data.get("swapTransaction") or {}).get("dataHash") or "").strip() or None,
1255
+ },
1256
+ "token_in_metadata": _normalize_token_metadata(
1257
+ data.get("tokenInMetadata"),
1258
+ str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
1259
+ ),
1260
+ "token_out_metadata": _normalize_token_metadata(
1261
+ data.get("tokenOutMetadata"),
1262
+ str((data.get("swapRequest") or {}).get("tokenOut") or token_out),
1263
+ ),
1264
+ "quote": quote,
1265
+ "chain_id": int(data.get("chainId") or 0),
1266
+ "source": "wdk-evm-wallet",
1267
+ }
1268
+
1269
+ async def send_evm_swap(
1270
+ self,
1271
+ *,
1272
+ token_in: str,
1273
+ token_out: str,
1274
+ amount_in_raw: str,
1275
+ expected_quote_fingerprint: str | None = None,
1276
+ minimum_output_amount_raw: str | None = None,
1277
+ ) -> dict[str, Any]:
1278
+ if self.sign_only:
1279
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1280
+ data = await self.client.post(
1281
+ "/v1/evm/swap/send",
1282
+ {
1283
+ "walletId": self.wallet_id,
1284
+ "accountIndex": self.account_index,
1285
+ "network": self.network,
1286
+ "tokenIn": token_in,
1287
+ "tokenOut": token_out,
1288
+ "tokenInAmount": amount_in_raw,
1289
+ **(
1290
+ {"expectedQuoteFingerprint": expected_quote_fingerprint}
1291
+ if isinstance(expected_quote_fingerprint, str) and expected_quote_fingerprint.strip()
1292
+ else {}
1293
+ ),
1294
+ **(
1295
+ {"minimumTokenOutAmount": minimum_output_amount_raw}
1296
+ if isinstance(minimum_output_amount_raw, str) and minimum_output_amount_raw.strip()
1297
+ else {}
1298
+ ),
1299
+ },
1300
+ )
1301
+ result = dict(data.get("result") or {})
1302
+ return {
1303
+ "chain": self.chain,
1304
+ "network": self.network,
1305
+ "asset_type": "evm-swap",
1306
+ "asset": "ERC20",
1307
+ "wallet": self.wallet_id,
1308
+ "from_address": await self.get_address(),
1309
+ "token_in": str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
1310
+ "token_out": str((data.get("swapRequest") or {}).get("tokenOut") or token_out),
1311
+ "input_amount_raw": str((data.get("swapRequest") or {}).get("tokenInAmount") or amount_in_raw),
1312
+ "input_amount_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1313
+ "output_amount_raw": str(result.get("tokenOutAmount") or "0"),
1314
+ "estimated_output_amount_raw": str(result.get("tokenOutAmount") or "0"),
1315
+ "estimated_output_amount_ui": (
1316
+ str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None
1317
+ ),
1318
+ "estimated_fee_wei": str(data.get("estimatedFeeWei") or result.get("fee") or "0"),
1319
+ "estimated_swap_fee_wei": str(data.get("estimatedSwapFeeWei") or result.get("swapFee") or "0"),
1320
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or result.get("approvalFee") or "0"),
1321
+ "slippage_bps": int(data.get("slippageBps") or 0) if data.get("slippageBps") is not None else None,
1322
+ "minimum_output_amount_raw": (
1323
+ str(data.get("minimumOutputAmountRaw"))
1324
+ if data.get("minimumOutputAmountRaw") is not None
1325
+ else None
1326
+ ),
1327
+ "swap_provider": str(data.get("protocol") or "velora"),
1328
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1329
+ "router": str(data.get("router") or "").strip() or None,
1330
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1331
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
1332
+ "swap_transaction": {
1333
+ "to": str((data.get("swapTransaction") or {}).get("to") or "").strip() or None,
1334
+ "value": str((data.get("swapTransaction") or {}).get("value") or "0"),
1335
+ "data_hash": str((data.get("swapTransaction") or {}).get("dataHash") or "").strip() or None,
1336
+ },
1337
+ "token_in_metadata": _normalize_token_metadata(
1338
+ data.get("tokenInMetadata"),
1339
+ str((data.get("swapRequest") or {}).get("tokenIn") or token_in),
1340
+ ),
1341
+ "token_out_metadata": _normalize_token_metadata(
1342
+ data.get("tokenOutMetadata"),
1343
+ str((data.get("swapRequest") or {}).get("tokenOut") or token_out),
1344
+ ),
1345
+ "hash": result.get("hash"),
1346
+ "approve_hash": result.get("approveHash"),
1347
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
1348
+ "result": result,
1349
+ "chain_id": int(data.get("chainId") or 0),
1350
+ "broadcasted": True,
1351
+ "confirmed": False,
1352
+ "source": "wdk-evm-wallet",
1353
+ }
1354
+
1355
+ async def preview_evm_lifi_cross_chain_swap(
1356
+ self,
1357
+ *,
1358
+ token_in: str,
1359
+ destination_chain: str,
1360
+ output_token: str,
1361
+ destination_address: str,
1362
+ amount_in_raw: str,
1363
+ slippage: float | int | None = None,
1364
+ allow_bridges: list[str] | None = None,
1365
+ deny_bridges: list[str] | None = None,
1366
+ prefer_bridges: list[str] | None = None,
1367
+ ) -> dict[str, Any]:
1368
+ resolved_address = await self.get_address()
1369
+ data = await self.client.post(
1370
+ "/v1/evm/lifi/quote",
1371
+ {
1372
+ "walletId": self.wallet_id,
1373
+ "address": resolved_address,
1374
+ "accountIndex": self.account_index,
1375
+ "network": self.network,
1376
+ "tokenIn": token_in,
1377
+ "destinationChain": destination_chain,
1378
+ "outputToken": output_token,
1379
+ "destinationAddress": destination_address,
1380
+ "tokenInAmount": amount_in_raw,
1381
+ **({"slippage": slippage} if slippage is not None else {}),
1382
+ **({"allowBridges": allow_bridges} if allow_bridges is not None else {}),
1383
+ **({"denyBridges": deny_bridges} if deny_bridges is not None else {}),
1384
+ **({"preferBridges": prefer_bridges} if prefer_bridges is not None else {}),
1385
+ },
1386
+ )
1387
+ data.setdefault("address", resolved_address)
1388
+ return _normalize_lifi_cross_chain_payload(
1389
+ chain=self.chain,
1390
+ network=self.network,
1391
+ wallet_id=self.wallet_id,
1392
+ data=data,
1393
+ token_in=token_in,
1394
+ destination_chain=destination_chain,
1395
+ output_token=output_token,
1396
+ destination_address=destination_address,
1397
+ amount_in_raw=amount_in_raw,
1398
+ slippage=slippage,
1399
+ sign_only=self.sign_only,
1400
+ )
1401
+
1402
+ async def send_evm_lifi_cross_chain_swap(
1403
+ self,
1404
+ *,
1405
+ token_in: str,
1406
+ destination_chain: str,
1407
+ output_token: str,
1408
+ destination_address: str,
1409
+ amount_in_raw: str,
1410
+ slippage: float | int | None = None,
1411
+ allow_bridges: list[str] | None = None,
1412
+ deny_bridges: list[str] | None = None,
1413
+ prefer_bridges: list[str] | None = None,
1414
+ minimum_output_amount_raw: str | None = None,
1415
+ ) -> dict[str, Any]:
1416
+ if self.sign_only:
1417
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1418
+ data = await self.client.post(
1419
+ "/v1/evm/lifi/send",
1420
+ {
1421
+ "walletId": self.wallet_id,
1422
+ "accountIndex": self.account_index,
1423
+ "network": self.network,
1424
+ "tokenIn": token_in,
1425
+ "destinationChain": destination_chain,
1426
+ "outputToken": output_token,
1427
+ "destinationAddress": destination_address,
1428
+ "tokenInAmount": amount_in_raw,
1429
+ **({"slippage": slippage} if slippage is not None else {}),
1430
+ **({"allowBridges": allow_bridges} if allow_bridges is not None else {}),
1431
+ **({"denyBridges": deny_bridges} if deny_bridges is not None else {}),
1432
+ **({"preferBridges": prefer_bridges} if prefer_bridges is not None else {}),
1433
+ **(
1434
+ {"minimumTokenOutAmount": minimum_output_amount_raw}
1435
+ if isinstance(minimum_output_amount_raw, str) and minimum_output_amount_raw.strip()
1436
+ else {}
1437
+ ),
1438
+ },
1439
+ )
1440
+ result = dict(data.get("result") or {})
1441
+ data.setdefault("address", await self.get_address())
1442
+ shaped = _normalize_lifi_cross_chain_payload(
1443
+ chain=self.chain,
1444
+ network=self.network,
1445
+ wallet_id=self.wallet_id,
1446
+ data=data,
1447
+ token_in=token_in,
1448
+ destination_chain=destination_chain,
1449
+ output_token=output_token,
1450
+ destination_address=destination_address,
1451
+ amount_in_raw=amount_in_raw,
1452
+ slippage=slippage,
1453
+ sign_only=self.sign_only,
1454
+ )
1455
+ return {
1456
+ **shaped,
1457
+ "output_amount_raw": str(result.get("tokenOutAmount") or shaped.get("estimated_output_amount_raw") or "0"),
1458
+ "estimated_fee_wei": str(data.get("estimatedFeeWei") or result.get("fee") or shaped.get("estimated_fee_wei") or "0"),
1459
+ "estimated_swap_fee_wei": str(data.get("estimatedSwapFeeWei") or result.get("swapFee") or shaped.get("estimated_swap_fee_wei") or "0"),
1460
+ "estimated_approval_fee_wei": str(data.get("estimatedApprovalFeeWei") or result.get("approvalFee") or shaped.get("estimated_approval_fee_wei") or "0"),
1461
+ "hash": result.get("hash"),
1462
+ "approve_hash": result.get("approveHash"),
1463
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
1464
+ "result": result,
1465
+ "broadcasted": True,
1466
+ "confirmed": False,
1467
+ }
1468
+
1469
+ async def preview_evm_native_transfer(
1470
+ self,
1471
+ *,
1472
+ recipient: str,
1473
+ amount_wei: str,
1474
+ ) -> dict[str, Any]:
1475
+ data = await self.client.post(
1476
+ "/v1/evm/transfer/quote",
1477
+ {
1478
+ "walletId": self.wallet_id,
1479
+ "accountIndex": self.account_index,
1480
+ "network": self.network,
1481
+ "to": recipient,
1482
+ "value": amount_wei,
1483
+ },
1484
+ )
1485
+ quote = dict(data.get("quote") or {})
1486
+ return {
1487
+ "chain": self.chain,
1488
+ "network": self.network,
1489
+ "asset_type": "evm-native-transfer",
1490
+ "asset": "ETH",
1491
+ "wallet": self.wallet_id,
1492
+ "from_address": await self.get_address(),
1493
+ "recipient": recipient,
1494
+ "amount_wei": str(amount_wei),
1495
+ "estimated_fee_wei": _extract_fee_wei(quote),
1496
+ "quote": quote,
1497
+ "chain_id": int(data.get("chainId") or 0),
1498
+ "source": "wdk-evm-wallet",
1499
+ }
1500
+
1501
+ async def send_evm_native_transfer(
1502
+ self,
1503
+ *,
1504
+ recipient: str,
1505
+ amount_wei: str,
1506
+ ) -> dict[str, Any]:
1507
+ if self.sign_only:
1508
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1509
+ data = await self.client.post(
1510
+ "/v1/evm/transfer/send",
1511
+ {
1512
+ "walletId": self.wallet_id,
1513
+ "accountIndex": self.account_index,
1514
+ "network": self.network,
1515
+ "to": recipient,
1516
+ "value": amount_wei,
1517
+ },
1518
+ )
1519
+ result = dict(data.get("result") or {})
1520
+ return {
1521
+ "chain": self.chain,
1522
+ "network": self.network,
1523
+ "asset_type": "evm-native-transfer",
1524
+ "asset": "ETH",
1525
+ "wallet": self.wallet_id,
1526
+ "from_address": await self.get_address(),
1527
+ "recipient": recipient,
1528
+ "amount_wei": str(amount_wei),
1529
+ "estimated_fee_wei": _extract_fee_wei(result),
1530
+ "hash": result.get("hash"),
1531
+ "result": result,
1532
+ "chain_id": int(data.get("chainId") or 0),
1533
+ "broadcasted": True,
1534
+ "confirmed": False,
1535
+ "source": "wdk-evm-wallet",
1536
+ }
1537
+
1538
+ async def preview_evm_token_transfer(
1539
+ self,
1540
+ *,
1541
+ token_address: str,
1542
+ recipient: str,
1543
+ amount_raw: str,
1544
+ ) -> dict[str, Any]:
1545
+ data = await self.client.post(
1546
+ "/v1/evm/token-transfer/quote",
1547
+ {
1548
+ "walletId": self.wallet_id,
1549
+ "accountIndex": self.account_index,
1550
+ "network": self.network,
1551
+ "tokenAddress": token_address,
1552
+ "recipient": recipient,
1553
+ "amount": amount_raw,
1554
+ },
1555
+ )
1556
+ quote = dict(data.get("quote") or {})
1557
+ return {
1558
+ "chain": self.chain,
1559
+ "network": self.network,
1560
+ "asset_type": "evm-token-transfer",
1561
+ "wallet": self.wallet_id,
1562
+ "from_address": await self.get_address(),
1563
+ "recipient": recipient,
1564
+ "token_address": token_address,
1565
+ "amount_raw": str(amount_raw),
1566
+ "amount_ui": str(data.get("amountFormatted")) if data.get("amountFormatted") is not None else None,
1567
+ "estimated_fee_wei": _extract_fee_wei(quote),
1568
+ "token_metadata": _normalize_token_metadata(data.get("tokenMetadata"), token_address),
1569
+ "quote": quote,
1570
+ "chain_id": int(data.get("chainId") or 0),
1571
+ "source": "wdk-evm-wallet",
1572
+ }
1573
+
1574
+ async def send_evm_token_transfer(
1575
+ self,
1576
+ *,
1577
+ token_address: str,
1578
+ recipient: str,
1579
+ amount_raw: str,
1580
+ ) -> dict[str, Any]:
1581
+ if self.sign_only:
1582
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1583
+ data = await self.client.post(
1584
+ "/v1/evm/token-transfer/send",
1585
+ {
1586
+ "walletId": self.wallet_id,
1587
+ "accountIndex": self.account_index,
1588
+ "network": self.network,
1589
+ "tokenAddress": token_address,
1590
+ "recipient": recipient,
1591
+ "amount": amount_raw,
1592
+ },
1593
+ )
1594
+ result = dict(data.get("result") or {})
1595
+ return {
1596
+ "chain": self.chain,
1597
+ "network": self.network,
1598
+ "asset_type": "evm-token-transfer",
1599
+ "wallet": self.wallet_id,
1600
+ "from_address": await self.get_address(),
1601
+ "recipient": recipient,
1602
+ "token_address": token_address,
1603
+ "amount_raw": str(amount_raw),
1604
+ "amount_ui": str(data.get("amountFormatted")) if data.get("amountFormatted") is not None else None,
1605
+ "estimated_fee_wei": _extract_fee_wei(result),
1606
+ "token_metadata": _normalize_token_metadata(data.get("tokenMetadata"), token_address),
1607
+ "hash": result.get("hash"),
1608
+ "result": result,
1609
+ "chain_id": int(data.get("chainId") or 0),
1610
+ "broadcasted": True,
1611
+ "confirmed": False,
1612
+ "source": "wdk-evm-wallet",
1613
+ }
1614
+
1615
+ def get_capabilities(self) -> WalletCapabilities:
1616
+ return WalletCapabilities(
1617
+ backend=self.name,
1618
+ chain=self.chain,
1619
+ custody_model=self.custody_model,
1620
+ sign_only=self.sign_only,
1621
+ has_signer=True,
1622
+ can_get_address=True,
1623
+ can_get_balance=True,
1624
+ can_sign_message=False,
1625
+ can_sign_transaction=not self.sign_only,
1626
+ can_send_transaction=not self.sign_only,
1627
+ external_dependencies=["wdk-evm-wallet", "evm-rpc"],
1628
+ )