@agentlayer.tech/wallet 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openclaw/extensions/agent-wallet/README.md +1 -2
- package/.openclaw/extensions/agent-wallet/dist/index.js +6 -340
- package/.openclaw/extensions/agent-wallet/index.ts +6 -340
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +0 -43
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
- package/CHANGELOG.md +35 -0
- package/README.md +0 -5
- package/agent-wallet/.env.example +0 -12
- package/agent-wallet/README.md +0 -35
- package/agent-wallet/agent_wallet/btc_user_wallets.py +32 -1
- package/agent-wallet/agent_wallet/config.py +11 -7
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +1 -655
- package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
- package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
- package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
- package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -100
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1118
- package/agent-wallet/openclaw.plugin.json +0 -4
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +113 -6
- package/agent-wallet/scripts/install_openclaw_local_config.py +7 -5
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -5
- package/bin/openclaw-agent-wallet.mjs +103 -36
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +7 -2
- package/codex/plugins/agent-wallet/server.py +2 -118
- package/hermes/plugins/agent_wallet/tools.py +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/src/local_vault.js +45 -68
- package/wdk-btc-wallet/src/server.js +1 -0
- package/wdk-evm-wallet/README.md +4 -3
- package/wdk-evm-wallet/src/config.js +15 -0
- package/wdk-evm-wallet/src/local_vault.js +45 -68
- package/wdk-evm-wallet/src/server.js +1 -0
- package/agent-wallet/agent_wallet/providers/houdini.py +0 -539
|
@@ -117,10 +117,6 @@
|
|
|
117
117
|
"type": "string",
|
|
118
118
|
"description": "Optional Jupiter Portfolio API base URL for Jupiter-specific positions and staking data."
|
|
119
119
|
},
|
|
120
|
-
"jupiterLendBaseUrl": {
|
|
121
|
-
"type": "string",
|
|
122
|
-
"description": "Optional Jupiter Lend API base URL for Earn read and deposit/withdraw flows."
|
|
123
|
-
},
|
|
124
120
|
"jupiterApiKey": {
|
|
125
121
|
"type": "string",
|
|
126
122
|
"description": "Optional Jupiter API key if your deployment uses a keyed endpoint."
|
|
@@ -182,6 +182,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
182
182
|
parser.add_argument("--extension-path", default=str(_extension_path()))
|
|
183
183
|
parser.add_argument("--wdk-btc-root", default=str(_default_wdk_btc_root()))
|
|
184
184
|
parser.add_argument("--wdk-evm-root", default=str(_default_wdk_evm_root()))
|
|
185
|
+
parser.add_argument("--wdk-evm-service-url", default=EVM_DEFAULT_SERVICE_URL)
|
|
185
186
|
parser.add_argument("--runtime-root", default=str(_default_runtime_root()))
|
|
186
187
|
parser.add_argument("--npm-bin", default=_default_npm_bin())
|
|
187
188
|
parser.add_argument("--plugin-id", default="agent-wallet")
|
|
@@ -665,6 +666,9 @@ def _build_next_steps(
|
|
|
665
666
|
command.extend(["--extension-path", str(effective_extension_path)])
|
|
666
667
|
command.extend(["--package-root", str(effective_package_root)])
|
|
667
668
|
command.extend(["--python-bin", str(python_bin)])
|
|
669
|
+
if _is_evm_backend(args.backend):
|
|
670
|
+
service_url = str(getattr(args, "wdk_evm_service_url", "") or EVM_DEFAULT_SERVICE_URL).strip()
|
|
671
|
+
command.extend(["--wdk-evm-service-url", service_url or EVM_DEFAULT_SERVICE_URL])
|
|
668
672
|
return command
|
|
669
673
|
|
|
670
674
|
|
|
@@ -672,18 +676,51 @@ def _is_solana_backend(backend: str) -> bool:
|
|
|
672
676
|
return backend.strip().lower() in {"solana", "solana_local", "solana-local"}
|
|
673
677
|
|
|
674
678
|
|
|
679
|
+
def _is_evm_backend(backend: str) -> bool:
|
|
680
|
+
return backend.strip().lower() in {
|
|
681
|
+
"wdk_evm_local",
|
|
682
|
+
"wdk-evm-local",
|
|
683
|
+
"evm_local",
|
|
684
|
+
"evm-local",
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
EVM_DEFAULT_SERVICE_URL = "http://127.0.0.1:8081"
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _build_evm_onboard_config(args: argparse.Namespace) -> dict[str, object]:
|
|
692
|
+
# The EVM wallet is provisioned on every install (best-effort). Seed creation
|
|
693
|
+
# with a valid EVM network; ensure_user_evm_wallet_ready binds BOTH base and
|
|
694
|
+
# ethereum (one address), so the active --network only matters when EVM is the
|
|
695
|
+
# active backend.
|
|
696
|
+
network = args.network.strip().lower() if _is_evm_backend(args.backend) else "base"
|
|
697
|
+
if network not in {"base", "ethereum"}:
|
|
698
|
+
network = "base"
|
|
699
|
+
service_url = str(getattr(args, "wdk_evm_service_url", "") or EVM_DEFAULT_SERVICE_URL).strip()
|
|
700
|
+
return {
|
|
701
|
+
"backend": "wdk_evm_local",
|
|
702
|
+
"network": network,
|
|
703
|
+
"signOnly": bool(args.sign_only),
|
|
704
|
+
"wdkEvmServiceUrl": service_url or EVM_DEFAULT_SERVICE_URL,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
|
|
675
708
|
def _build_solana_onboard_config(args: argparse.Namespace) -> dict[str, object]:
|
|
709
|
+
# Solana is provisioned on every install (both wallets are created), so force
|
|
710
|
+
# the Solana backend/network here -- this must work even when the active
|
|
711
|
+
# backend chosen by the user is EVM or BTC.
|
|
712
|
+
solana_active = _is_solana_backend(args.backend)
|
|
676
713
|
config: dict[str, object] = {
|
|
677
|
-
"backend":
|
|
678
|
-
"network":
|
|
714
|
+
"backend": "solana_local",
|
|
715
|
+
"network": "mainnet",
|
|
679
716
|
"signOnly": bool(args.sign_only),
|
|
680
717
|
"encryptUserWallets": True,
|
|
681
718
|
"migratePlaintextUserWallets": True,
|
|
682
719
|
"refuseMainnetWalletRecreation": True,
|
|
683
720
|
}
|
|
684
|
-
if args.rpc_url.strip():
|
|
721
|
+
if solana_active and args.rpc_url.strip():
|
|
685
722
|
config["rpcUrl"] = args.rpc_url.strip()
|
|
686
|
-
if args.rpc_urls.strip():
|
|
723
|
+
if solana_active and args.rpc_urls.strip():
|
|
687
724
|
config["rpcUrls"] = [item.strip() for item in args.rpc_urls.split(",") if item.strip()]
|
|
688
725
|
return config
|
|
689
726
|
|
|
@@ -708,8 +745,6 @@ def _bootstrap_solana_wallet(
|
|
|
708
745
|
package_root: Path,
|
|
709
746
|
args: argparse.Namespace,
|
|
710
747
|
) -> dict[str, object] | None:
|
|
711
|
-
if not _is_solana_backend(args.backend):
|
|
712
|
-
return None
|
|
713
748
|
result = subprocess.run(
|
|
714
749
|
[
|
|
715
750
|
str(python_bin),
|
|
@@ -741,6 +776,60 @@ def _bootstrap_solana_wallet(
|
|
|
741
776
|
}
|
|
742
777
|
|
|
743
778
|
|
|
779
|
+
def _bootstrap_evm_wallet(
|
|
780
|
+
python_bin: Path,
|
|
781
|
+
package_root: Path,
|
|
782
|
+
args: argparse.Namespace,
|
|
783
|
+
wdk_evm_root: Path,
|
|
784
|
+
) -> dict[str, object]:
|
|
785
|
+
"""Provision the local EVM wallet (best-effort).
|
|
786
|
+
|
|
787
|
+
Mirrors _bootstrap_solana_wallet but for wdk_evm_local. Runs the onboard CLI,
|
|
788
|
+
which calls ensure_user_evm_wallet_ready: auto-starts the local Node service,
|
|
789
|
+
creates and seals the wallet password, and binds both base and ethereum.
|
|
790
|
+
Failures here never abort the install -- the lazy runtime path (ensure_ready on
|
|
791
|
+
first EVM use) remains the safety net.
|
|
792
|
+
"""
|
|
793
|
+
env = _runtime_env_for_onboard(package_root)
|
|
794
|
+
env["OPENCLAW_EVM_WDK_WALLET_ROOT"] = str(wdk_evm_root)
|
|
795
|
+
try:
|
|
796
|
+
result = subprocess.run(
|
|
797
|
+
[
|
|
798
|
+
str(python_bin),
|
|
799
|
+
"-m",
|
|
800
|
+
"agent_wallet.openclaw_cli",
|
|
801
|
+
"onboard",
|
|
802
|
+
"--user-id",
|
|
803
|
+
args.user_id,
|
|
804
|
+
"--config-json",
|
|
805
|
+
json.dumps(_build_evm_onboard_config(args)),
|
|
806
|
+
],
|
|
807
|
+
cwd=package_root,
|
|
808
|
+
capture_output=True,
|
|
809
|
+
text=True,
|
|
810
|
+
check=True,
|
|
811
|
+
env=env,
|
|
812
|
+
)
|
|
813
|
+
except subprocess.CalledProcessError as exc:
|
|
814
|
+
detail = (exc.stderr or exc.stdout or str(exc)).strip()
|
|
815
|
+
return {"ok": False, "error": detail[-2000:]}
|
|
816
|
+
try:
|
|
817
|
+
payload = json.loads(result.stdout)
|
|
818
|
+
except json.JSONDecodeError:
|
|
819
|
+
return {"ok": False, "error": "EVM onboard returned non-JSON output."}
|
|
820
|
+
session = dict(payload.get("session") or {})
|
|
821
|
+
wallet_path = str(session.get("wallet_path") or "")
|
|
822
|
+
wallet_id = wallet_path.split("walletId=", 1)[-1] if "walletId=" in wallet_path else None
|
|
823
|
+
return {
|
|
824
|
+
"ok": True,
|
|
825
|
+
"user_id": session.get("user_id") or args.user_id,
|
|
826
|
+
"address": session.get("address"),
|
|
827
|
+
"wallet_id": wallet_id,
|
|
828
|
+
"networks": ["base", "ethereum"],
|
|
829
|
+
"backend": session.get("backend"),
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
|
|
744
833
|
def main() -> None:
|
|
745
834
|
args = build_parser().parse_args()
|
|
746
835
|
source_package_root = Path(args.package_root).expanduser().resolve()
|
|
@@ -873,6 +962,7 @@ def main() -> None:
|
|
|
873
962
|
configured = False
|
|
874
963
|
configure_stdout = ""
|
|
875
964
|
solana_onboard_result: dict[str, object] | None = None
|
|
965
|
+
evm_onboard_result: dict[str, object] | None = None
|
|
876
966
|
if backend_enabled and not pending_env and not args.dry_run:
|
|
877
967
|
result = subprocess.run(
|
|
878
968
|
_build_next_steps(
|
|
@@ -893,6 +983,22 @@ def main() -> None:
|
|
|
893
983
|
package_root,
|
|
894
984
|
args,
|
|
895
985
|
)
|
|
986
|
+
# Both wallets are provisioned on every install. EVM provisioning is
|
|
987
|
+
# best-effort: a failure here must not abort the install, since the lazy
|
|
988
|
+
# runtime path will create the wallet on first EVM use.
|
|
989
|
+
evm_onboard_result = _bootstrap_evm_wallet(
|
|
990
|
+
python_bin,
|
|
991
|
+
package_root,
|
|
992
|
+
args,
|
|
993
|
+
wdk_evm_root,
|
|
994
|
+
)
|
|
995
|
+
if isinstance(evm_onboard_result, dict) and not evm_onboard_result.get("ok"):
|
|
996
|
+
print(
|
|
997
|
+
"warning: the EVM wallet was not provisioned during install; it will "
|
|
998
|
+
"be created automatically on first EVM use. Details: "
|
|
999
|
+
+ str(evm_onboard_result.get("error") or "unknown"),
|
|
1000
|
+
file=sys.stderr,
|
|
1001
|
+
)
|
|
896
1002
|
|
|
897
1003
|
print(
|
|
898
1004
|
json.dumps(
|
|
@@ -918,6 +1024,7 @@ def main() -> None:
|
|
|
918
1024
|
"configured": configured,
|
|
919
1025
|
"pending_env": pending_env,
|
|
920
1026
|
"solana_wallet": solana_onboard_result,
|
|
1027
|
+
"evm_wallet": evm_onboard_result,
|
|
921
1028
|
"next_configure_command": _build_next_steps(
|
|
922
1029
|
python_bin,
|
|
923
1030
|
install_config_script,
|
|
@@ -13,7 +13,11 @@ from pathlib import Path
|
|
|
13
13
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
14
14
|
|
|
15
15
|
from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
|
|
16
|
-
from agent_wallet.config import
|
|
16
|
+
from agent_wallet.config import (
|
|
17
|
+
normalize_btc_network,
|
|
18
|
+
normalize_evm_network,
|
|
19
|
+
normalize_solana_network,
|
|
20
|
+
)
|
|
17
21
|
from agent_wallet.sealed_keys import resolve_sealed_keys_path, seal_keys, unseal_keys
|
|
18
22
|
from security_utils import write_redacted_backup
|
|
19
23
|
|
|
@@ -25,10 +29,6 @@ LEGACY_ALLOWLIST_TOOLS = [
|
|
|
25
29
|
"set_wallet_backend",
|
|
26
30
|
"get_wallet_portfolio",
|
|
27
31
|
"get_solana_token_prices",
|
|
28
|
-
"swap_solana_privately",
|
|
29
|
-
"continue_solana_private_swap",
|
|
30
|
-
"list_pending_solana_private_swaps",
|
|
31
|
-
"get_solana_private_swap_status",
|
|
32
32
|
"get_kamino_lend_markets",
|
|
33
33
|
"get_kamino_lend_market_reserves",
|
|
34
34
|
"get_kamino_lend_user_obligations",
|
|
@@ -182,6 +182,8 @@ def _normalize_network(backend: str, network: str) -> str:
|
|
|
182
182
|
normalized = network.strip().lower()
|
|
183
183
|
if backend_name in {"wdk_btc_local", "wdk-btc-local", "btc_local", "btc-local"}:
|
|
184
184
|
return normalize_btc_network(normalized or "bitcoin")
|
|
185
|
+
if backend_name in {"wdk_evm_local", "wdk-evm-local", "evm_local", "evm-local"}:
|
|
186
|
+
return normalize_evm_network(normalized or "ethereum")
|
|
185
187
|
return normalize_solana_network(normalized or "mainnet")
|
|
186
188
|
|
|
187
189
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallet-operator
|
|
3
|
-
description: Use when operating OpenClaw wallet tools: balances, transfers, swaps, LI.FI cross-chain swaps, Jupiter swaps, Velora EVM swaps, BTC transfers, staking,
|
|
3
|
+
description: Use when operating OpenClaw wallet tools: balances, transfers, swaps, LI.FI cross-chain swaps, Jupiter swaps, Velora EVM swaps, BTC transfers, staking, Kamino lending, Bags claims/launches, and wallet execution safety.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Wallet Operator
|
|
@@ -29,7 +29,6 @@ Use this skill before calling OpenClaw wallet tools. It is the routing guide for
|
|
|
29
29
|
- EVM transfers: `transfer_evm_native`, `transfer_evm_token`.
|
|
30
30
|
- BTC transfer: `transfer_btc`.
|
|
31
31
|
- Solana staking: `stake_sol_native`, `deactivate_solana_stake`, `withdraw_solana_stake`.
|
|
32
|
-
- Jupiter Earn: `jupiter_earn_deposit`, `jupiter_earn_withdraw`.
|
|
33
32
|
- Kamino: `kamino_lend_deposit`, `kamino_lend_withdraw`, `kamino_lend_borrow`, `kamino_lend_repay`.
|
|
34
33
|
- Bags: `claim_bags_fees`, `launch_bags_token`.
|
|
35
34
|
|
|
@@ -107,9 +106,6 @@ Use this skill before calling OpenClaw wallet tools. It is the routing guide for
|
|
|
107
106
|
- `stake_sol_native`: `vote_account`, `amount` in SOL, `mode`, `purpose`.
|
|
108
107
|
- `deactivate_solana_stake`: `stake_account`, `mode`, `purpose`.
|
|
109
108
|
- `withdraw_solana_stake`: `stake_account`, `amount` in SOL, optional `recipient`, `mode`, `purpose`.
|
|
110
|
-
- Before Jupiter Earn writes, use `get_jupiter_earn_tokens`, `get_jupiter_earn_positions`, and `get_jupiter_earn_earnings`.
|
|
111
|
-
- `jupiter_earn_deposit`: `asset` mint, `amount_raw`, `mode`, `purpose`.
|
|
112
|
-
- `jupiter_earn_withdraw`: `asset` mint, `amount_raw`, `mode`, `purpose`.
|
|
113
109
|
- Before Kamino writes, use `get_kamino_lend_markets`, `get_kamino_lend_market_reserves`, `get_kamino_lend_user_obligations`, and `get_kamino_lend_user_rewards`.
|
|
114
110
|
- Kamino write params: `market`, `reserve`, `amount_ui` decimal string, `mode`, `purpose`.
|
|
115
111
|
- Bags reads: `get_bags_claimable_positions`, `get_bags_fee_analytics`.
|
|
@@ -1268,71 +1268,138 @@ function runCodexInstall(args) {
|
|
|
1268
1268
|
return add.skipped || add.ok ? 0 : 1;
|
|
1269
1269
|
}
|
|
1270
1270
|
|
|
1271
|
-
|
|
1272
|
-
const pluginSource = resolveClaudeCodePluginSource();
|
|
1273
|
-
const force = hasFlag(args, "--force");
|
|
1274
|
-
const skipEnable = hasFlag(args, "--skip-enable");
|
|
1275
|
-
const claudeBin = commandPath("claude");
|
|
1271
|
+
const CLAUDE_CODE_MARKETPLACE_NAME = "agentlayer-local";
|
|
1276
1272
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1273
|
+
function resolveClaudeCodeMarketplaceDir(env = process.env) {
|
|
1274
|
+
return path.resolve(
|
|
1275
|
+
expandHome(env.AGENT_WALLET_CLAUDE_CODE_MARKETPLACE_DIR || "~/.claude/agentlayer-local"),
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function ensureClaudeCodeMarketplace(marketplaceDir, pluginSource, force) {
|
|
1280
|
+
const pluginsDir = path.join(marketplaceDir, "plugins");
|
|
1281
|
+
const pluginLink = path.join(pluginsDir, "agent-wallet");
|
|
1282
|
+
const manifestDir = path.join(marketplaceDir, ".claude-plugin");
|
|
1283
|
+
const manifestPath = path.join(manifestDir, "marketplace.json");
|
|
1280
1284
|
|
|
1281
|
-
fs.mkdirSync(
|
|
1285
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
1286
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
1287
|
+
|
|
1288
|
+
// Symlink plugin source into marketplace plugins dir.
|
|
1282
1289
|
try {
|
|
1283
|
-
const existing = fs.lstatSync(
|
|
1290
|
+
const existing = fs.lstatSync(pluginLink);
|
|
1284
1291
|
if (!existing.isSymbolicLink()) {
|
|
1285
1292
|
if (!force) {
|
|
1286
|
-
throw new Error(
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`${pluginLink} exists and is not a symlink. Pass --force to replace it.`,
|
|
1295
|
+
);
|
|
1287
1296
|
}
|
|
1288
|
-
fs.rmSync(
|
|
1297
|
+
fs.rmSync(pluginLink, { recursive: true, force: true });
|
|
1289
1298
|
} else {
|
|
1290
|
-
fs.unlinkSync(
|
|
1299
|
+
fs.unlinkSync(pluginLink);
|
|
1291
1300
|
}
|
|
1292
1301
|
} catch (error) {
|
|
1293
1302
|
if (error?.code !== "ENOENT") throw error;
|
|
1294
1303
|
}
|
|
1295
|
-
fs.symlinkSync(pluginSource,
|
|
1304
|
+
fs.symlinkSync(pluginSource, pluginLink, "dir");
|
|
1305
|
+
|
|
1306
|
+
// Write marketplace manifest (Claude Code requires owner + plugins[].source as relative path).
|
|
1307
|
+
const manifest = {
|
|
1308
|
+
name: CLAUDE_CODE_MARKETPLACE_NAME,
|
|
1309
|
+
description: "Local AgentLayer plugins",
|
|
1310
|
+
owner: { name: "AgentLayer" },
|
|
1311
|
+
plugins: [
|
|
1312
|
+
{
|
|
1313
|
+
name: "agent-wallet",
|
|
1314
|
+
displayName: "Agent Wallet",
|
|
1315
|
+
description:
|
|
1316
|
+
"Bridge to the existing local AgentLayer wallet runtime (Solana, Bitcoin, EVM).",
|
|
1317
|
+
category: "development",
|
|
1318
|
+
source: "./plugins/agent-wallet",
|
|
1319
|
+
},
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
writeJsonFile(manifestPath, manifest);
|
|
1323
|
+
return { pluginLink, manifestPath };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function runClaudeCodeInstall(args) {
|
|
1327
|
+
const pluginSource = resolveClaudeCodePluginSource();
|
|
1328
|
+
const force = hasFlag(args, "--force");
|
|
1329
|
+
const skipEnable = hasFlag(args, "--skip-enable");
|
|
1330
|
+
const claudeBin = commandPath("claude");
|
|
1331
|
+
const marketplaceDir = resolveClaudeCodeMarketplaceDir();
|
|
1332
|
+
|
|
1333
|
+
const { pluginLink } = ensureClaudeCodeMarketplace(marketplaceDir, pluginSource, force);
|
|
1334
|
+
|
|
1335
|
+
const pluginDirFlag = `claude --plugin-dir ${pluginLink}`;
|
|
1296
1336
|
|
|
1297
|
-
//
|
|
1337
|
+
// Without the Claude CLI we can only set up the files; the user must register manually.
|
|
1338
|
+
let marketplaceAdd = { attempted: false, ok: false, skipped: skipEnable, error: "" };
|
|
1298
1339
|
let enable = { attempted: false, ok: false, skipped: skipEnable, error: "" };
|
|
1340
|
+
|
|
1299
1341
|
if (!skipEnable) {
|
|
1300
1342
|
if (!claudeBin) {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
error:
|
|
1306
|
-
"Claude Code CLI was not found on PATH. Load the plugin manually with: claude --plugin-dir " +
|
|
1307
|
-
pluginTarget,
|
|
1308
|
-
};
|
|
1343
|
+
const msg =
|
|
1344
|
+
"Claude Code CLI was not found on PATH. Load the plugin manually with: " + pluginDirFlag;
|
|
1345
|
+
marketplaceAdd = { attempted: false, ok: false, skipped: false, error: msg };
|
|
1346
|
+
enable = { attempted: false, ok: false, skipped: false, error: msg };
|
|
1309
1347
|
} else {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1348
|
+
// Register the local marketplace (idempotent — safe to re-run).
|
|
1349
|
+
const addResult = spawnSync(
|
|
1350
|
+
claudeBin,
|
|
1351
|
+
["plugin", "marketplace", "add", marketplaceDir, "--scope", "user"],
|
|
1352
|
+
{ encoding: "utf8", stdio: "pipe" },
|
|
1353
|
+
);
|
|
1354
|
+
marketplaceAdd = {
|
|
1315
1355
|
attempted: true,
|
|
1316
|
-
ok:
|
|
1356
|
+
ok: addResult.status === 0,
|
|
1317
1357
|
skipped: false,
|
|
1318
|
-
error:
|
|
1358
|
+
error: addResult.status === 0 ? "" : (addResult.stderr || addResult.stdout || "").trim(),
|
|
1319
1359
|
};
|
|
1360
|
+
|
|
1361
|
+
if (marketplaceAdd.ok) {
|
|
1362
|
+
// Install plugin from the now-registered local marketplace.
|
|
1363
|
+
const installResult = spawnSync(
|
|
1364
|
+
claudeBin,
|
|
1365
|
+
["plugin", "install", `agent-wallet@${CLAUDE_CODE_MARKETPLACE_NAME}`, "--scope", "user"],
|
|
1366
|
+
{ encoding: "utf8", stdio: "pipe" },
|
|
1367
|
+
);
|
|
1368
|
+
enable = {
|
|
1369
|
+
attempted: true,
|
|
1370
|
+
ok: installResult.status === 0,
|
|
1371
|
+
skipped: false,
|
|
1372
|
+
error:
|
|
1373
|
+
installResult.status === 0
|
|
1374
|
+
? ""
|
|
1375
|
+
: (installResult.stderr || installResult.stdout || "").trim(),
|
|
1376
|
+
};
|
|
1377
|
+
} else {
|
|
1378
|
+
enable = {
|
|
1379
|
+
attempted: false,
|
|
1380
|
+
ok: false,
|
|
1381
|
+
skipped: false,
|
|
1382
|
+
error: "Skipped plugin install because marketplace registration failed.",
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1320
1385
|
}
|
|
1321
1386
|
}
|
|
1322
1387
|
|
|
1323
|
-
const
|
|
1388
|
+
const ok = enable.skipped || enable.ok;
|
|
1389
|
+
const pluginDirFlagFull = `claude --plugin-dir ${pluginSource}`;
|
|
1324
1390
|
console.log(
|
|
1325
1391
|
JSON.stringify(
|
|
1326
1392
|
{
|
|
1327
|
-
ok
|
|
1393
|
+
ok,
|
|
1328
1394
|
plugin_source: pluginSource,
|
|
1329
|
-
|
|
1395
|
+
marketplace_dir: marketplaceDir,
|
|
1396
|
+
marketplace_add: marketplaceAdd,
|
|
1330
1397
|
claude_code_install: enable,
|
|
1331
|
-
manual_load:
|
|
1398
|
+
manual_load: pluginDirFlagFull,
|
|
1332
1399
|
restart_required: true,
|
|
1333
|
-
note:
|
|
1400
|
+
note: ok
|
|
1334
1401
|
? "Plugin registered. Restart Claude Code to activate."
|
|
1335
|
-
: `If automatic registration failed, load the plugin with: ${
|
|
1402
|
+
: `If automatic registration failed, load the plugin with: ${pluginDirFlagFull}`,
|
|
1336
1403
|
},
|
|
1337
1404
|
null,
|
|
1338
1405
|
2,
|
|
@@ -6,14 +6,19 @@ PLUGIN_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
|
|
6
6
|
OPENCLAW_HOME=${OPENCLAW_HOME:-"$HOME/.openclaw"}
|
|
7
7
|
PACKAGE_ROOT=${AGENT_WALLET_PACKAGE_ROOT:-${OPENCLAW_AGENT_WALLET_PACKAGE_ROOT:-"$OPENCLAW_HOME/agent-wallet-runtime/current/agent-wallet"}}
|
|
8
8
|
|
|
9
|
-
# Resolve server.py
|
|
10
|
-
|
|
9
|
+
# Resolve server.py. When Claude Code copies this plugin into its cache, the
|
|
10
|
+
# relative sibling paths below no longer resolve, so fall back to the codex
|
|
11
|
+
# plugin copy inside the installed runtime package, which is always present.
|
|
11
12
|
LOCAL_SERVER="$PLUGIN_ROOT/server.py"
|
|
13
|
+
CODEX_SERVER="$PLUGIN_ROOT/../../codex/plugins/agent-wallet/server.py"
|
|
14
|
+
RUNTIME_CODEX_DIR="$OPENCLAW_HOME/agent-wallet-runtime/current/codex/plugins/agent-wallet"
|
|
12
15
|
|
|
13
16
|
if [ -f "$LOCAL_SERVER" ]; then
|
|
14
17
|
SERVER_PY="$LOCAL_SERVER"
|
|
15
18
|
elif [ -f "$CODEX_SERVER" ]; then
|
|
16
19
|
SERVER_PY=$(CDPATH= cd -- "$PLUGIN_ROOT/../../codex/plugins/agent-wallet" && pwd)/server.py
|
|
20
|
+
elif [ -f "$RUNTIME_CODEX_DIR/server.py" ]; then
|
|
21
|
+
SERVER_PY=$(CDPATH= cd -- "$RUNTIME_CODEX_DIR" && pwd)/server.py
|
|
17
22
|
else
|
|
18
23
|
printf '{"error":"agent-wallet server.py not found. Run: npx @agentlayer.tech/wallet install --yes"}\n' >&2
|
|
19
24
|
exit 1
|
|
@@ -46,27 +46,17 @@ HOST_DEFAULT_CONFIG_KEYS = {
|
|
|
46
46
|
"jupiterUltraBaseUrl",
|
|
47
47
|
"jupiterPriceBaseUrl",
|
|
48
48
|
"jupiterPortfolioBaseUrl",
|
|
49
|
-
"jupiterLendBaseUrl",
|
|
50
49
|
"jupiterApiKey",
|
|
51
|
-
"houdiniBaseUrl",
|
|
52
|
-
"houdiniApiKey",
|
|
53
|
-
"houdiniApiSecret",
|
|
54
|
-
"houdiniUserIp",
|
|
55
|
-
"houdiniUserAgent",
|
|
56
|
-
"houdiniUserTimezone",
|
|
57
50
|
"kaminoBaseUrl",
|
|
58
51
|
"kaminoProgramId",
|
|
59
52
|
}
|
|
60
53
|
BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
|
|
61
54
|
PREVIEW_CACHE_TTL_SECONDS = 15 * 60
|
|
62
|
-
PRIVATE_SWAP_CACHE_TTL_SECONDS = 35 * 60
|
|
63
55
|
PREVIEW_BOUND_SWAP_TOOLS = {
|
|
64
56
|
"swap_solana_tokens",
|
|
65
|
-
"swap_solana_privately",
|
|
66
57
|
"flash_trade_open_position",
|
|
67
58
|
"flash_trade_close_position",
|
|
68
59
|
}
|
|
69
|
-
PRIVATE_SWAP_APPROVAL_TOOL_NAME = "swap_solana_privately"
|
|
70
60
|
APPROVAL_PREVIEW_TOOL_ALIASES = {
|
|
71
61
|
"x402_pay_request": "x402_preview_request",
|
|
72
62
|
}
|
|
@@ -81,7 +71,6 @@ selected_solana_network: str | None = None
|
|
|
81
71
|
selected_evm_network: str | None = None
|
|
82
72
|
selected_btc_network: str | None = None
|
|
83
73
|
approval_preview_cache: dict[str, dict[str, Any]] = {}
|
|
84
|
-
private_swap_order_cache: dict[str, dict[str, Any]] = {}
|
|
85
74
|
|
|
86
75
|
|
|
87
76
|
class WalletCliError(RuntimeError):
|
|
@@ -245,17 +234,10 @@ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str,
|
|
|
245
234
|
_prune_approval_preview_cache()
|
|
246
235
|
approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
|
|
247
236
|
"digest": _preview_digest(data),
|
|
248
|
-
"expires_at": time.time()
|
|
249
|
-
+ (
|
|
250
|
-
PRIVATE_SWAP_CACHE_TTL_SECONDS
|
|
251
|
-
if cache_tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME
|
|
252
|
-
else PREVIEW_CACHE_TTL_SECONDS
|
|
253
|
-
),
|
|
237
|
+
"expires_at": time.time() + PREVIEW_CACHE_TTL_SECONDS,
|
|
254
238
|
"preview": data,
|
|
255
239
|
"summary": summary,
|
|
256
240
|
}
|
|
257
|
-
if cache_tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
258
|
-
private_swap_order_cache.pop(_approval_cache_key(user_id, cache_tool_name), None)
|
|
259
241
|
|
|
260
242
|
|
|
261
243
|
def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
|
|
@@ -290,66 +272,6 @@ def _cached_preview_for_token(user_id: str, tool_name: str, token: str) -> dict[
|
|
|
290
272
|
return preview if isinstance(preview, dict) else None
|
|
291
273
|
|
|
292
274
|
|
|
293
|
-
def _cache_pending_private_swap_order(
|
|
294
|
-
user_id: str,
|
|
295
|
-
tool_name: str,
|
|
296
|
-
preview: dict[str, Any],
|
|
297
|
-
details: dict[str, Any],
|
|
298
|
-
) -> None:
|
|
299
|
-
if tool_name != PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
300
|
-
return
|
|
301
|
-
houdini_id = str(details.get("houdini_id") or "").strip()
|
|
302
|
-
deposit_address = str(details.get("deposit_address") or "").strip()
|
|
303
|
-
if not houdini_id or not deposit_address:
|
|
304
|
-
return
|
|
305
|
-
private_swap_order_cache[_approval_cache_key(user_id, tool_name)] = {
|
|
306
|
-
"digest": _preview_digest(preview),
|
|
307
|
-
"expires_at": time.time() + PRIVATE_SWAP_CACHE_TTL_SECONDS,
|
|
308
|
-
"order": {
|
|
309
|
-
"multi_id": str(details.get("multi_id") or "").strip() or None,
|
|
310
|
-
"houdini_id": houdini_id,
|
|
311
|
-
"deposit_address": deposit_address,
|
|
312
|
-
"order": details.get("order") if isinstance(details.get("order"), dict) else {},
|
|
313
|
-
},
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _latest_pending_private_swap_order(
|
|
318
|
-
user_id: str,
|
|
319
|
-
tool_name: str,
|
|
320
|
-
preview: dict[str, Any],
|
|
321
|
-
) -> dict[str, Any] | None:
|
|
322
|
-
if tool_name != PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
323
|
-
return None
|
|
324
|
-
cached = private_swap_order_cache.get(_approval_cache_key(user_id, tool_name))
|
|
325
|
-
if not cached:
|
|
326
|
-
return None
|
|
327
|
-
if float(cached.get("expires_at") or 0) <= time.time():
|
|
328
|
-
private_swap_order_cache.pop(_approval_cache_key(user_id, tool_name), None)
|
|
329
|
-
return None
|
|
330
|
-
if cached.get("digest") != _preview_digest(preview):
|
|
331
|
-
return None
|
|
332
|
-
order = cached.get("order")
|
|
333
|
-
return order if isinstance(order, dict) else None
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def _clear_pending_private_swap_order(user_id: str, tool_name: str) -> None:
|
|
337
|
-
if tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
338
|
-
private_swap_order_cache.pop(_approval_cache_key(user_id, tool_name), None)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _list_pending_private_swap_orders(user_id: str) -> list[dict[str, Any]]:
|
|
342
|
-
key = _approval_cache_key(user_id, PRIVATE_SWAP_APPROVAL_TOOL_NAME)
|
|
343
|
-
pending = private_swap_order_cache.get(key)
|
|
344
|
-
if not pending or float(pending.get("expires_at") or 0) <= time.time():
|
|
345
|
-
private_swap_order_cache.pop(key, None)
|
|
346
|
-
return []
|
|
347
|
-
order = pending.get("order")
|
|
348
|
-
if not isinstance(order, dict):
|
|
349
|
-
return []
|
|
350
|
-
return [{**order, "expires_at_ms": int(float(pending["expires_at"]) * 1000)}]
|
|
351
|
-
|
|
352
|
-
|
|
353
275
|
def _normalize_wallet_backend(value: Any) -> str:
|
|
354
276
|
normalized = str(value or "").strip().lower()
|
|
355
277
|
aliases = {
|
|
@@ -651,8 +573,6 @@ def _issue_approval_token(
|
|
|
651
573
|
]
|
|
652
574
|
if preview_payload.get("is_mainnet") is True:
|
|
653
575
|
extra_args.append("--mainnet-confirmed")
|
|
654
|
-
if tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
655
|
-
extra_args.extend(["--ttl-seconds", "1800"])
|
|
656
576
|
payload = _call_wallet_cli("issue-approval", extra_args)
|
|
657
577
|
token = str(payload.get("approval_token") or "").strip()
|
|
658
578
|
if not token:
|
|
@@ -966,9 +886,6 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
966
886
|
|
|
967
887
|
|
|
968
888
|
async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
969
|
-
if tool_name == "list_pending_solana_private_swaps":
|
|
970
|
-
return {"orders": _list_pending_private_swap_orders(_user_id())}
|
|
971
|
-
|
|
972
889
|
config = _base_config(params, tool_name=tool_name)
|
|
973
890
|
backend = _normalize_wallet_backend(config.get("backend"))
|
|
974
891
|
if backend == "wdk_evm_local" and params.get("network") is None and selected_evm_network:
|
|
@@ -976,24 +893,7 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
|
|
|
976
893
|
config["network"] = selected_evm_network
|
|
977
894
|
|
|
978
895
|
effective_params = dict(params)
|
|
979
|
-
|
|
980
|
-
_attach_approval_for_execute(tool_name, config, effective_params)
|
|
981
|
-
else:
|
|
982
|
-
cached = _latest_cached_preview(_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME)
|
|
983
|
-
if cached and isinstance(cached.get("preview"), dict):
|
|
984
|
-
effective_params["_approved_preview"] = cached["preview"]
|
|
985
|
-
effective_params["approval_token"] = _issue_approval_token(
|
|
986
|
-
PRIVATE_SWAP_APPROVAL_TOOL_NAME,
|
|
987
|
-
config,
|
|
988
|
-
cached["preview"],
|
|
989
|
-
)
|
|
990
|
-
pending = _latest_pending_private_swap_order(
|
|
991
|
-
_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME, cached["preview"]
|
|
992
|
-
)
|
|
993
|
-
if pending and effective_params.get("_resume_private_swap_order") is None:
|
|
994
|
-
effective_params["_resume_private_swap_order"] = pending
|
|
995
|
-
elif not effective_params.get("approval_token"):
|
|
996
|
-
raise RuntimeError(APPROVAL_CONTEXT_MISSING_MESSAGE)
|
|
896
|
+
_attach_approval_for_execute(tool_name, config, effective_params)
|
|
997
897
|
|
|
998
898
|
try:
|
|
999
899
|
payload = _invoke_tool(tool_name, effective_params, config)
|
|
@@ -1001,22 +901,6 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
|
|
|
1001
901
|
raise _normalize_approval_context_error(exc) from exc
|
|
1002
902
|
|
|
1003
903
|
_cache_preview_for_approval(_user_id(), tool_name, payload)
|
|
1004
|
-
if tool_name == "swap_solana_privately" and payload.get("ok") is True:
|
|
1005
|
-
data = payload.get("data")
|
|
1006
|
-
approved_preview = effective_params.get("_approved_preview")
|
|
1007
|
-
if (
|
|
1008
|
-
isinstance(data, dict)
|
|
1009
|
-
and data.get("execution_state") == "awaiting_deposit_funding"
|
|
1010
|
-
and isinstance(approved_preview, dict)
|
|
1011
|
-
):
|
|
1012
|
-
_cache_pending_private_swap_order(_user_id(), tool_name, approved_preview, data)
|
|
1013
|
-
elif isinstance(data, dict):
|
|
1014
|
-
_clear_pending_private_swap_order(_user_id(), tool_name)
|
|
1015
|
-
if tool_name == "continue_solana_private_swap" and payload.get("ok") is True:
|
|
1016
|
-
data = payload.get("data")
|
|
1017
|
-
if isinstance(data, dict) and data.get("execution_state") == "funding_submitted":
|
|
1018
|
-
_clear_pending_private_swap_order(_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME)
|
|
1019
|
-
|
|
1020
904
|
if payload.get("ok") is False:
|
|
1021
905
|
raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
|
|
1022
906
|
return payload.get("data", {})
|
|
@@ -15,7 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
|
|
17
17
|
BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
|
|
18
|
-
PREVIEW_BOUND_SWAP_TOOLS = {"swap_solana_tokens"
|
|
18
|
+
PREVIEW_BOUND_SWAP_TOOLS = {"swap_solana_tokens"}
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _json(data: dict[str, Any]) -> str:
|