@agentlayer.tech/wallet 0.1.28 → 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.
- package/.openclaw/extensions/agent-wallet/README.md +4 -5
- package/.openclaw/extensions/agent-wallet/dist/index.js +29 -20
- package/.openclaw/extensions/agent-wallet/index.ts +29 -20
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -2
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +38 -0
- package/README.md +9 -0
- package/agent-wallet/README.md +18 -22
- package/agent-wallet/agent_wallet/bootstrap.py +28 -12
- package/agent-wallet/agent_wallet/btc_user_wallets.py +2 -7
- package/agent-wallet/agent_wallet/config.py +99 -22
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -14
- package/agent-wallet/agent_wallet/openclaw_adapter.py +28 -32
- package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
- package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
- package/agent-wallet/agent_wallet/providers/x402.py +4 -9
- package/agent-wallet/agent_wallet/user_wallets.py +4 -3
- package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -3
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +437 -44
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +2 -12
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
- package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
- package/agent-wallet/scripts/install_agent_wallet.py +1 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +4 -6
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
- package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
- package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
- package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +0 -1
- package/bin/openclaw-agent-wallet.mjs +289 -0
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
- package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
- package/claude-code/plugins/agent-wallet/README.md +65 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +34 -0
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
- package/codex/plugins/agent-wallet/.mcp.json +15 -0
- package/codex/plugins/agent-wallet/README.md +39 -0
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
- package/codex/plugins/agent-wallet/server.py +1077 -0
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/hermes/plugins/agent_wallet/schemas.py +2 -2
- package/hermes/plugins/agent_wallet/tools.py +17 -3
- package/package.json +6 -1
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"network": {
|
|
41
41
|
"type": "string",
|
|
42
|
-
"description": "Backend network selector. Solana uses mainnet
|
|
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.
|
|
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
|
-
|
|
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="
|
|
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")
|
|
@@ -17,6 +17,7 @@ from urllib.request import urlopen
|
|
|
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_evm_network
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _default_config_path() -> Path:
|
|
@@ -44,18 +45,7 @@ def _script_path(name: str) -> Path:
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def _normalize_network(value: str) -> str:
|
|
47
|
-
|
|
48
|
-
aliases = {
|
|
49
|
-
"mainnet": "ethereum",
|
|
50
|
-
"eth": "ethereum",
|
|
51
|
-
"eth-mainnet": "ethereum",
|
|
52
|
-
"base-mainnet": "base",
|
|
53
|
-
"base_sepolia": "base-sepolia",
|
|
54
|
-
}
|
|
55
|
-
network = aliases.get(network, network)
|
|
56
|
-
if network not in {"ethereum", "sepolia", "base", "base-sepolia"}:
|
|
57
|
-
return "ethereum"
|
|
58
|
-
return network
|
|
48
|
+
return normalize_evm_network(value)
|
|
59
49
|
|
|
60
50
|
|
|
61
51
|
def build_parser() -> argparse.ArgumentParser:
|