@agentlayer.tech/wallet 0.1.27 → 0.1.30

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 (56) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +4 -5
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +31 -31
  3. package/.openclaw/extensions/agent-wallet/index.ts +31 -31
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/CHANGELOG.md +52 -0
  7. package/README.md +9 -0
  8. package/agent-wallet/README.md +18 -22
  9. package/agent-wallet/agent_wallet/bootstrap.py +28 -12
  10. package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
  11. package/agent-wallet/agent_wallet/config.py +99 -22
  12. package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
  13. package/agent-wallet/agent_wallet/openclaw_adapter.py +72 -108
  14. package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
  15. package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
  16. package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
  17. package/agent-wallet/agent_wallet/providers/x402.py +198 -18
  18. package/agent-wallet/agent_wallet/user_wallets.py +4 -3
  19. package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
  20. package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
  21. package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
  22. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
  23. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +13 -13
  24. package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
  25. package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
  26. package/agent-wallet/openclaw.plugin.json +1 -1
  27. package/agent-wallet/pyproject.toml +2 -1
  28. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
  29. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
  30. package/agent-wallet/scripts/build_release_bundle.py +1 -0
  31. package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
  32. package/agent-wallet/scripts/install_agent_wallet.py +1 -0
  33. package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
  34. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
  35. package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
  36. package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
  37. package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
  38. package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
  39. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
  40. package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
  41. package/bin/openclaw-agent-wallet.mjs +289 -0
  42. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
  43. package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
  44. package/claude-code/plugins/agent-wallet/README.md +65 -0
  45. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
  46. package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  47. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
  48. package/codex/plugins/agent-wallet/.mcp.json +15 -0
  49. package/codex/plugins/agent-wallet/README.md +39 -0
  50. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
  51. package/codex/plugins/agent-wallet/server.py +1077 -0
  52. package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
  53. package/hermes/plugins/agent_wallet/schemas.py +2 -2
  54. package/hermes/plugins/agent_wallet/tools.py +17 -3
  55. package/package.json +6 -1
  56. package/setup.sh +2 -0
@@ -11,6 +11,7 @@ import time
11
11
  from decimal import Decimal, InvalidOperation
12
12
  from typing import Any
13
13
 
14
+ from agent_wallet.config import normalize_solana_network
14
15
  from agent_wallet.models import AgentWalletCapabilities, SolanaWalletState
15
16
  from agent_wallet.providers import bags, flash, flash_sdk_bridge, houdini, jupiter, kamino, lifi, solana_rpc
16
17
  from agent_wallet.solana_stake import (
@@ -51,6 +52,7 @@ STAKE_PROGRAM_ID = "Stake11111111111111111111111111111111111111"
51
52
  HOUDINI_PRIVATE_OUTPUT_DRIFT_BPS = 600
52
53
  SOLANA_SWAP_DEFAULT_SLIPPAGE_BPS = 300
53
54
  SOLANA_SWAP_INTENT_DEFAULT_MAX_FEE_LAMPORTS = 6_000_000
55
+ KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY = 6
54
56
 
55
57
 
56
58
  def _load_signing_key():
@@ -275,7 +277,7 @@ class SolanaWalletBackend(AgentWalletBackend):
275
277
  self.rpc_urls = rpc_url if isinstance(rpc_url, list) else [rpc_url]
276
278
  self.rpc_url = self.rpc_urls[0]
277
279
  self.commitment = commitment
278
- self.network = network
280
+ self.network = normalize_solana_network(network)
279
281
  self.signer = signer
280
282
  self.address = final_address
281
283
  self.sign_only = sign_only
@@ -3217,6 +3219,440 @@ class SolanaWalletBackend(AgentWalletBackend):
3217
3219
  "source": "kamino",
3218
3220
  }
3219
3221
 
3222
+ async def get_kamino_open_positions(self, user: str | None = None) -> dict[str, Any]:
3223
+ self._require_mainnet_kamino("Kamino lending")
3224
+ wallet_address = user or self.address
3225
+ if not wallet_address:
3226
+ raise WalletBackendError("A wallet address is required for Kamino position lookup.")
3227
+ wallet_address = validate_solana_address(wallet_address)
3228
+
3229
+ markets_snapshot = await self.get_kamino_lend_markets()
3230
+ markets = markets_snapshot.get("markets")
3231
+ if not isinstance(markets, list):
3232
+ markets = []
3233
+
3234
+ lookup_errors: list[dict[str, Any]] = []
3235
+ semaphore = asyncio.Semaphore(KAMINO_OPEN_POSITIONS_SCAN_CONCURRENCY)
3236
+
3237
+ def _market_address(entry: Any) -> str:
3238
+ return _kamino_entry_address(entry, "lendingMarket", "market", "address")
3239
+
3240
+ def _market_name(entry: Any) -> str | None:
3241
+ if isinstance(entry, dict):
3242
+ value = entry.get("name")
3243
+ if isinstance(value, str) and value.strip():
3244
+ return value.strip()
3245
+ return None
3246
+
3247
+ async def _fetch_market_obligations(
3248
+ market_entry: dict[str, Any],
3249
+ ) -> tuple[dict[str, Any], dict[str, Any]] | None:
3250
+ market_address = _market_address(market_entry)
3251
+ if not market_address:
3252
+ return None
3253
+ try:
3254
+ async with semaphore:
3255
+ obligations_snapshot = await self.get_kamino_lend_user_obligations(
3256
+ market=market_address,
3257
+ user=wallet_address,
3258
+ )
3259
+ except (ProviderError, WalletBackendError) as exc:
3260
+ lookup_errors.append(
3261
+ {
3262
+ "stage": "market_obligations",
3263
+ "market": market_address,
3264
+ "market_name": _market_name(market_entry),
3265
+ "error": str(exc),
3266
+ }
3267
+ )
3268
+ return None
3269
+ if int(obligations_snapshot.get("obligation_count") or 0) <= 0:
3270
+ return None
3271
+ return market_entry, obligations_snapshot
3272
+
3273
+ market_results = await asyncio.gather(
3274
+ *[
3275
+ _fetch_market_obligations(market_entry)
3276
+ for market_entry in markets
3277
+ if isinstance(market_entry, dict)
3278
+ ]
3279
+ )
3280
+ active_markets = [result for result in market_results if result is not None]
3281
+ discovered_obligation_count = sum(
3282
+ int(obligations_snapshot.get("obligation_count") or 0)
3283
+ for _, obligations_snapshot in active_markets
3284
+ )
3285
+
3286
+ try:
3287
+ reward_snapshot = await self.get_kamino_lend_user_rewards(user=wallet_address)
3288
+ except (ProviderError, WalletBackendError) as exc:
3289
+ lookup_errors.append(
3290
+ {
3291
+ "stage": "rewards",
3292
+ "user": wallet_address,
3293
+ "error": str(exc),
3294
+ }
3295
+ )
3296
+ reward_snapshot = {
3297
+ "chain": "solana",
3298
+ "network": self.network,
3299
+ "user": wallet_address,
3300
+ "reward_count": 0,
3301
+ "rewards": [],
3302
+ "avg_base_apy": None,
3303
+ "avg_boosted_apy": None,
3304
+ "avg_max_apy": None,
3305
+ "source": "kamino",
3306
+ }
3307
+ reward_items = reward_snapshot.get("rewards")
3308
+ if not isinstance(reward_items, list):
3309
+ reward_items = []
3310
+
3311
+ positions: list[dict[str, Any]] = []
3312
+ markets_with_positions: list[dict[str, Any]] = []
3313
+ total_collateral_value = Decimal("0")
3314
+ total_borrow_value = Decimal("0")
3315
+
3316
+ for market_entry, obligations_snapshot in active_markets:
3317
+ market_address = _market_address(market_entry)
3318
+ market_name = _market_name(market_entry)
3319
+ market_description = (
3320
+ market_entry.get("description")
3321
+ if isinstance(market_entry, dict) and isinstance(market_entry.get("description"), str)
3322
+ else None
3323
+ )
3324
+ markets_with_positions.append(
3325
+ {
3326
+ "market": market_address,
3327
+ "market_name": market_name,
3328
+ "obligation_count": int(obligations_snapshot.get("obligation_count") or 0),
3329
+ }
3330
+ )
3331
+
3332
+ try:
3333
+ reserve_snapshot = await self.get_kamino_lend_market_reserves(market=market_address)
3334
+ except (ProviderError, WalletBackendError) as exc:
3335
+ lookup_errors.append(
3336
+ {
3337
+ "stage": "market_reserves",
3338
+ "market": market_address,
3339
+ "market_name": market_name,
3340
+ "error": str(exc),
3341
+ }
3342
+ )
3343
+ reserve_snapshot = {
3344
+ "chain": "solana",
3345
+ "network": self.network,
3346
+ "market": market_address,
3347
+ "reserve_count": 0,
3348
+ "reserves": [],
3349
+ "source": "kamino",
3350
+ }
3351
+ reserves = reserve_snapshot.get("reserves")
3352
+ if not isinstance(reserves, list):
3353
+ reserves = []
3354
+ reserve_by_address = {
3355
+ address: reserve
3356
+ for reserve in reserves
3357
+ if isinstance(reserve, dict)
3358
+ and (address := _kamino_entry_address(reserve, "reserve"))
3359
+ }
3360
+ reserve_by_mint = {
3361
+ mint: reserve
3362
+ for reserve in reserves
3363
+ if isinstance(reserve, dict)
3364
+ and isinstance((mint := reserve.get("liquidityTokenMint")), str)
3365
+ and mint.strip()
3366
+ }
3367
+ reserve_by_symbol = {
3368
+ symbol.upper(): reserve
3369
+ for reserve in reserves
3370
+ if isinstance(reserve, dict)
3371
+ and isinstance((symbol := reserve.get("liquidityToken")), str)
3372
+ and symbol.strip()
3373
+ }
3374
+
3375
+ def _reward_metrics_for_reserve(
3376
+ *,
3377
+ reserve_address: str | None,
3378
+ side: str,
3379
+ ) -> list[dict[str, Any]]:
3380
+ if not reserve_address:
3381
+ return []
3382
+ reserve_key = "depositReserve" if side == "deposit" else "borrowReserve"
3383
+ metrics: list[dict[str, Any]] = []
3384
+ for reward in reward_items:
3385
+ if not isinstance(reward, dict):
3386
+ continue
3387
+ reward_market = _kamino_entry_address(reward, "market")
3388
+ if reward_market and reward_market != market_address:
3389
+ continue
3390
+ reward_reserve = _kamino_entry_address(reward, reserve_key)
3391
+ if reward_reserve != reserve_address:
3392
+ continue
3393
+ metrics.append(
3394
+ {
3395
+ "reward_mint": _kamino_entry_address(reward, "rewardMint", "rewardToken"),
3396
+ "tokens_earned": reward.get("tokensEarned"),
3397
+ "tokens_per_second": reward.get("tokensPerSecond"),
3398
+ "base_apy": reward.get("baseApy"),
3399
+ "boosted_apy": reward.get("boostedApy"),
3400
+ "max_apy": reward.get("maxApy"),
3401
+ "usd_amount": reward.get("usdAmount"),
3402
+ "usd_amount_boosted": reward.get("usdAmountBoosted"),
3403
+ "staking_boost": reward.get("stakingBoost"),
3404
+ "effective_staking_boost": reward.get("effectiveStakingBoost"),
3405
+ "last_calculated": reward.get("lastCalculated"),
3406
+ }
3407
+ )
3408
+ return metrics
3409
+
3410
+ obligations = obligations_snapshot.get("obligations")
3411
+ if not isinstance(obligations, list):
3412
+ obligations = []
3413
+ for obligation in obligations:
3414
+ if not isinstance(obligation, dict):
3415
+ continue
3416
+ obligation_address = _kamino_entry_address(
3417
+ obligation,
3418
+ "obligationAddress",
3419
+ "loanId",
3420
+ "address",
3421
+ )
3422
+ if not obligation_address:
3423
+ continue
3424
+ try:
3425
+ loan_data = await kamino.fetch_lend_loan_info(
3426
+ obligation=obligation_address,
3427
+ network=self.network,
3428
+ )
3429
+ except ProviderError as exc:
3430
+ lookup_errors.append(
3431
+ {
3432
+ "stage": "loan_info",
3433
+ "market": market_address,
3434
+ "market_name": market_name,
3435
+ "obligation_address": obligation_address,
3436
+ "error": str(exc),
3437
+ }
3438
+ )
3439
+ continue
3440
+
3441
+ loan_info = loan_data.get("loanInfo")
3442
+ if not isinstance(loan_info, dict):
3443
+ loan_info = {}
3444
+ collateral = loan_info.get("collateral")
3445
+ if not isinstance(collateral, dict):
3446
+ collateral = {}
3447
+ debt = loan_info.get("debt")
3448
+ if not isinstance(debt, dict):
3449
+ debt = {}
3450
+ deposit_entries = collateral.get("deposits")
3451
+ if not isinstance(deposit_entries, list):
3452
+ deposit_entries = []
3453
+ borrow_entries = debt.get("borrows")
3454
+ if not isinstance(borrow_entries, list):
3455
+ borrow_entries = []
3456
+
3457
+ state = obligation.get("state")
3458
+ if not isinstance(state, dict):
3459
+ state = {}
3460
+ state_deposits = [
3461
+ entry
3462
+ for entry in state.get("deposits", [])
3463
+ if isinstance(entry, dict)
3464
+ and (_coerce_decimal(entry.get("depositedAmount")) or Decimal("0")) > 0
3465
+ ]
3466
+ state_borrows = [
3467
+ entry
3468
+ for entry in state.get("borrows", [])
3469
+ if isinstance(entry, dict)
3470
+ and (
3471
+ (_coerce_decimal(entry.get("borrowedAmountSf")) or Decimal("0")) > 0
3472
+ or (_coerce_decimal(entry.get("marketValueSf")) or Decimal("0")) > 0
3473
+ )
3474
+ ]
3475
+
3476
+ def _match_reserve(
3477
+ *,
3478
+ token_mint: str | None,
3479
+ token_name: str | None,
3480
+ fallback_entry: Any,
3481
+ reserve_key: str,
3482
+ ) -> tuple[str | None, dict[str, Any] | None]:
3483
+ fallback_address = _kamino_entry_address(fallback_entry, reserve_key)
3484
+ if fallback_address and fallback_address in reserve_by_address:
3485
+ return fallback_address, reserve_by_address[fallback_address]
3486
+ if token_mint and token_mint in reserve_by_mint:
3487
+ reserve_entry = reserve_by_mint[token_mint]
3488
+ return _kamino_entry_address(reserve_entry, "reserve") or None, reserve_entry
3489
+ symbol = token_name.strip().upper() if isinstance(token_name, str) and token_name.strip() else None
3490
+ if symbol and symbol in reserve_by_symbol:
3491
+ reserve_entry = reserve_by_symbol[symbol]
3492
+ return _kamino_entry_address(reserve_entry, "reserve") or None, reserve_entry
3493
+ return fallback_address or None, None
3494
+
3495
+ def _enrich_position_entries(
3496
+ *,
3497
+ entries: list[dict[str, Any]],
3498
+ state_entries: list[dict[str, Any]],
3499
+ side: str,
3500
+ ) -> list[dict[str, Any]]:
3501
+ enriched: list[dict[str, Any]] = []
3502
+ reserve_key = "depositReserve" if side == "deposit" else "borrowReserve"
3503
+ for index, entry in enumerate(entries):
3504
+ if not isinstance(entry, dict):
3505
+ continue
3506
+ token_mint = entry.get("tokenMint")
3507
+ token_name = entry.get("tokenName")
3508
+ fallback_entry = state_entries[index] if index < len(state_entries) else None
3509
+ reserve_address, reserve_metrics = _match_reserve(
3510
+ token_mint=token_mint if isinstance(token_mint, str) else None,
3511
+ token_name=token_name if isinstance(token_name, str) else None,
3512
+ fallback_entry=fallback_entry,
3513
+ reserve_key=reserve_key,
3514
+ )
3515
+ reward_metrics = _reward_metrics_for_reserve(
3516
+ reserve_address=reserve_address,
3517
+ side=side,
3518
+ )
3519
+ enriched.append(
3520
+ {
3521
+ "reserve": reserve_address,
3522
+ "token_mint": token_mint,
3523
+ "token_name": token_name,
3524
+ "token_amount": entry.get("tokenAmount"),
3525
+ "token_value_usd": entry.get("tokenValue"),
3526
+ "token_price_usd": entry.get("tokenPrice"),
3527
+ "max_ltv": entry.get("maxLtv"),
3528
+ "liquidation_ltv": entry.get("liquidationLtv"),
3529
+ "max_withdrawable_amount": entry.get("maxWithdrawableAmount"),
3530
+ "max_withdrawable_value_usd": entry.get("maxWithdrawableValue"),
3531
+ "max_borrowable_amount": entry.get("maxBorrowableAmount"),
3532
+ "max_borrowable_value_usd": entry.get("maxBorrowableValue"),
3533
+ "borrow_factor": entry.get("borrowFactor"),
3534
+ "reserve_supply_apy": (
3535
+ reserve_metrics.get("supplyApy")
3536
+ if isinstance(reserve_metrics, dict)
3537
+ else None
3538
+ ),
3539
+ "reserve_borrow_apy": (
3540
+ reserve_metrics.get("borrowApy")
3541
+ if isinstance(reserve_metrics, dict)
3542
+ else None
3543
+ ),
3544
+ "reserve_max_ltv": (
3545
+ reserve_metrics.get("maxLtv")
3546
+ if isinstance(reserve_metrics, dict)
3547
+ else None
3548
+ ),
3549
+ "reward_metrics": reward_metrics,
3550
+ "reward_count": len(reward_metrics),
3551
+ }
3552
+ )
3553
+ return enriched
3554
+
3555
+ enriched_deposits = _enrich_position_entries(
3556
+ entries=deposit_entries,
3557
+ state_entries=state_deposits,
3558
+ side="deposit",
3559
+ )
3560
+ enriched_borrows = _enrich_position_entries(
3561
+ entries=borrow_entries,
3562
+ state_entries=state_borrows,
3563
+ side="borrow",
3564
+ )
3565
+
3566
+ collateral_value = sum(
3567
+ (
3568
+ _coerce_decimal(entry.get("token_value_usd")) or Decimal("0")
3569
+ for entry in enriched_deposits
3570
+ ),
3571
+ Decimal("0"),
3572
+ )
3573
+ borrow_value = sum(
3574
+ (
3575
+ _coerce_decimal(entry.get("token_value_usd")) or Decimal("0")
3576
+ for entry in enriched_borrows
3577
+ ),
3578
+ Decimal("0"),
3579
+ )
3580
+ total_collateral_value += collateral_value
3581
+ total_borrow_value += borrow_value
3582
+ refreshed_stats = obligation.get("refreshedStats")
3583
+ if not isinstance(refreshed_stats, dict):
3584
+ refreshed_stats = {}
3585
+ position_type = "borrow-lend"
3586
+ if enriched_deposits and not enriched_borrows:
3587
+ position_type = "lend"
3588
+ elif enriched_borrows and not enriched_deposits:
3589
+ position_type = "borrow"
3590
+
3591
+ positions.append(
3592
+ {
3593
+ "obligation_address": obligation_address,
3594
+ "market": market_address,
3595
+ "market_name": market_name,
3596
+ "market_description": market_description,
3597
+ "user": wallet_address,
3598
+ "position_type": position_type,
3599
+ "has_debt": bool(enriched_borrows),
3600
+ "timestamp": loan_data.get("timestamp"),
3601
+ "solana_slot": loan_data.get("solanaSlot"),
3602
+ "elevation_group": loan_data.get("elevationGroup"),
3603
+ "leverage": loan_data.get("leverage"),
3604
+ "collateral_value_usd": _format_decimal(collateral_value),
3605
+ "borrow_value_usd": _format_decimal(borrow_value),
3606
+ "net_value_usd": _format_decimal(collateral_value - borrow_value),
3607
+ "loan_info": {
3608
+ "current_ltv": loan_info.get("currentLtv"),
3609
+ "max_ltv": loan_info.get("maxLtv"),
3610
+ "liquidation_ltv": loan_info.get("liquidationLtv"),
3611
+ "close_factor": loan_info.get("closeFactor"),
3612
+ "collateral": {
3613
+ "deposit_count": len(enriched_deposits),
3614
+ "total_value_usd": _format_decimal(collateral_value),
3615
+ "deposits": enriched_deposits,
3616
+ },
3617
+ "debt": {
3618
+ "borrow_count": len(enriched_borrows),
3619
+ "total_value_usd": _format_decimal(borrow_value),
3620
+ "borrows": enriched_borrows,
3621
+ },
3622
+ },
3623
+ "refreshed_stats": {
3624
+ "borrow_limit": refreshed_stats.get("borrowLimit"),
3625
+ "borrow_liquidation_limit": refreshed_stats.get("borrowLiquidationLimit"),
3626
+ "borrow_utilization": refreshed_stats.get("borrowUtilization"),
3627
+ "net_account_value": refreshed_stats.get("netAccountValue"),
3628
+ },
3629
+ "source": "kamino+klend-loans",
3630
+ }
3631
+ )
3632
+
3633
+ return {
3634
+ "chain": "solana",
3635
+ "network": self.network,
3636
+ "user": wallet_address,
3637
+ "market_count_scanned": len(markets),
3638
+ "markets_with_positions_count": len(markets_with_positions),
3639
+ "markets_with_positions": markets_with_positions,
3640
+ "discovered_obligation_count": discovered_obligation_count,
3641
+ "position_count": len(positions),
3642
+ "positions": positions,
3643
+ "total_collateral_value_usd": _format_decimal(total_collateral_value),
3644
+ "total_borrow_value_usd": _format_decimal(total_borrow_value),
3645
+ "total_net_value_usd": _format_decimal(total_collateral_value - total_borrow_value),
3646
+ "reward_summary": {
3647
+ "reward_count": int(reward_snapshot.get("reward_count") or 0),
3648
+ "avg_base_apy": reward_snapshot.get("avg_base_apy"),
3649
+ "avg_boosted_apy": reward_snapshot.get("avg_boosted_apy"),
3650
+ "avg_max_apy": reward_snapshot.get("avg_max_apy"),
3651
+ },
3652
+ "lookup_errors": lookup_errors,
3653
+ "source": "kamino+klend-loans",
3654
+ }
3655
+
3220
3656
  async def get_state(self) -> SolanaWalletState:
3221
3657
  balance_native = None
3222
3658
  if self.address:
@@ -4809,49 +5245,6 @@ class SolanaWalletBackend(AgentWalletBackend):
4809
5245
  "source": "solana-rpc",
4810
5246
  }
4811
5247
 
4812
- async def request_testnet_airdrop(self, amount_native: float) -> dict[str, Any]:
4813
- if self.network not in {"devnet", "testnet"}:
4814
- raise WalletBackendError("Airdrop is only available on Solana devnet or testnet.")
4815
- if amount_native <= 0:
4816
- raise WalletBackendError("amount must be greater than zero.")
4817
-
4818
- address = await self.get_address()
4819
- if not address:
4820
- raise WalletBackendError(
4821
- "No Solana wallet address configured. Set SOLANA_AGENT_PUBLIC_KEY or a signer."
4822
- )
4823
-
4824
- lamports = int(round(amount_native * solana_rpc.LAMPORTS_PER_SOL))
4825
- submitted = await solana_rpc.request_airdrop(
4826
- address=address,
4827
- lamports=lamports,
4828
- rpc_url=self.rpc_urls,
4829
- commitment=self.commitment,
4830
- )
4831
- signature = submitted.get("signature")
4832
- status = None
4833
- confirmed = False
4834
- if isinstance(signature, str) and signature:
4835
- status = await solana_rpc.wait_for_confirmation(
4836
- signature=signature,
4837
- rpc_url=self.rpc_urls,
4838
- )
4839
- confirmed = status is not None
4840
-
4841
- return {
4842
- "chain": "solana",
4843
- "network": self.network,
4844
- "mode": "airdrop",
4845
- "address": address,
4846
- "amount_native": amount_native,
4847
- "amount_lamports": lamports,
4848
- "signature": signature,
4849
- "confirmed": confirmed,
4850
- "confirmation_status": status.get("confirmationStatus") if status else None,
4851
- "slot": status.get("slot") if status else None,
4852
- "source": "solana-rpc",
4853
- }
4854
-
4855
5248
  async def _resolve_mint_decimals(self, mint: str) -> int:
4856
5249
  if mint == NATIVE_SOL_MINT:
4857
5250
  return 9
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ from agent_wallet.config import normalize_btc_network
7
8
  from agent_wallet.providers.wdk_btc_local import WdkBtcLocalClient
8
9
  from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError, WalletCapabilities
9
10
 
@@ -13,14 +14,7 @@ def _sats_to_btc(value: Any) -> float:
13
14
 
14
15
 
15
16
  def _normalize_btc_network(value: str | None) -> str:
16
- network = str(value or "").strip().lower()
17
- aliases = {
18
- "mainnet": "bitcoin",
19
- }
20
- network = aliases.get(network, network)
21
- if network not in {"bitcoin", "testnet", "regtest"}:
22
- return "bitcoin"
23
- return network
17
+ return normalize_btc_network(value)
24
18
 
25
19
 
26
20
  class WdkBtcLocalWalletBackend(AgentWalletBackend):
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
6
6
  from typing import Any
7
7
 
8
+ from agent_wallet.config import normalize_evm_network
8
9
  from agent_wallet.providers.evm_portfolio import build_portfolio_snapshot
9
10
  from agent_wallet.providers import lifi
10
11
  from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
@@ -12,18 +13,7 @@ from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendErro
12
13
 
13
14
 
14
15
  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
16
+ return normalize_evm_network(value)
27
17
 
28
18
 
29
19
  def _lifi_chain_id_for_evm_network(network: str) -> str:
@@ -33,6 +23,16 @@ def _lifi_chain_id_for_evm_network(network: str) -> str:
33
23
  return "1"
34
24
 
35
25
 
26
+ def _normalize_x402_typed_data_value(value: Any) -> Any:
27
+ if isinstance(value, (bytes, bytearray)):
28
+ return "0x" + bytes(value).hex()
29
+ if isinstance(value, list):
30
+ return [_normalize_x402_typed_data_value(item) for item in value]
31
+ if isinstance(value, dict):
32
+ return {str(key): _normalize_x402_typed_data_value(item) for key, item in value.items()}
33
+ return value
34
+
35
+
36
36
  def _extract_fee_wei(payload: dict[str, Any]) -> str | None:
37
37
  for key in ("fee", "maxFee", "totalFee", "gasCost", "cost"):
38
38
  value = payload.get(key)
@@ -457,7 +457,7 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
457
457
  "domain": domain,
458
458
  "types": types,
459
459
  "primaryType": primary_type,
460
- "message": message,
460
+ "message": _normalize_x402_typed_data_value(message),
461
461
  },
462
462
  )
463
463
  signature = str(data.get("signature") or "").strip()
@@ -13,7 +13,7 @@ def main() -> None:
13
13
  context = onboard_openclaw_user_wallet(
14
14
  user_id,
15
15
  sign_only=False,
16
- network="devnet",
16
+ network="mainnet",
17
17
  )
18
18
 
19
19
  print("Session metadata:")
@@ -16,7 +16,7 @@ async def main() -> None:
16
16
  backend = create_wallet_backend_for_user(
17
17
  user_id,
18
18
  sign_only=False,
19
- network="devnet",
19
+ network="mainnet",
20
20
  )
21
21
  adapter = OpenClawWalletAdapter(backend)
22
22
 
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "network": {
41
41
  "type": "string",
42
- "description": "Backend network selector. Solana uses mainnet/devnet/testnet. BTC uses bitcoin/testnet/regtest. EVM uses ethereum/sepolia/base/base-sepolia."
42
+ "description": "Backend network selector. Solana uses mainnet. BTC uses bitcoin. EVM uses ethereum/base."
43
43
  },
44
44
  "wdkBtcServiceUrl": {
45
45
  "type": "string",
@@ -4,10 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.27"
7
+ version = "0.1.30"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
11
+ "fastmcp>=2.0.0",
11
12
  "httpx>=0.27.0",
12
13
  "pydantic>=2.0.0",
13
14
  "pydantic-settings>=2.0.0",
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
  sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
18
18
 
19
19
  from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
20
+ from agent_wallet.config import normalize_btc_network
20
21
 
21
22
 
22
23
  def _default_config_path() -> Path:
@@ -44,10 +45,7 @@ def _script_path(name: str) -> Path:
44
45
 
45
46
 
46
47
  def _normalize_network(value: str) -> str:
47
- network = str(value or "").strip().lower()
48
- if network == "mainnet":
49
- return "bitcoin"
50
- return network or "bitcoin"
48
+ return normalize_btc_network(value)
51
49
 
52
50
 
53
51
  def build_parser() -> argparse.ArgumentParser:
@@ -55,7 +53,7 @@ def build_parser() -> argparse.ArgumentParser:
55
53
  parser.add_argument("--config-path", default=str(_default_config_path()))
56
54
  parser.add_argument("--plugin-id", default="agent-wallet")
57
55
  parser.add_argument("--user-id", default=_default_user_id())
58
- parser.add_argument("--network", default="testnet")
56
+ parser.add_argument("--network", default="bitcoin")
59
57
  parser.add_argument("--service-url", default="http://127.0.0.1:8080")
60
58
  parser.add_argument("--wdk-wallet-root", default=str(_repo_root() / "wdk-btc-wallet"))
61
59
  parser.add_argument("--label", default="Agent BTC Wallet")