@agentlayer.tech/wallet 0.1.14 → 0.1.16
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 +20 -0
- package/.openclaw/extensions/agent-wallet/dist/index.js +2079 -0
- package/.openclaw/extensions/agent-wallet/index.ts +125 -2
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -0
- package/.openclaw/extensions/agent-wallet/package.json +44 -5
- package/.openclaw/extensions/pay-bridge/README.md +6 -0
- package/.openclaw/extensions/pay-bridge/dist/core.mjs +287 -0
- package/.openclaw/extensions/pay-bridge/dist/index.js +196 -0
- package/.openclaw/extensions/pay-bridge/package.json +43 -5
- package/CHANGELOG.md +33 -0
- package/README.md +32 -0
- package/RELEASING.md +167 -0
- package/agent-wallet/.env.example +5 -0
- package/agent-wallet/README.md +38 -0
- package/agent-wallet/agent_wallet/config.py +4 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +504 -0
- package/agent-wallet/agent_wallet/providers/flash.py +186 -0
- package/agent-wallet/agent_wallet/providers/flash_sdk_bridge.py +251 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +79 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +78 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +623 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/flash-sdk-bridge/README.md +33 -0
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1179 -0
- package/agent-wallet/scripts/flash-sdk-bridge/package-lock.json +2377 -0
- package/agent-wallet/scripts/flash-sdk-bridge/package.json +12 -0
- package/agent-wallet/scripts/install_agent_wallet.py +46 -11
- package/agent-wallet/scripts/install_openclaw_local_config.py +4 -0
- package/package.json +4 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
|
|
@@ -13,7 +14,12 @@ let selectedEvmNetwork = null;
|
|
|
13
14
|
let selectedBtcNetwork = null;
|
|
14
15
|
const PREVIEW_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
15
16
|
const PRIVATE_SWAP_CACHE_TTL_MS = 35 * 60 * 1000;
|
|
16
|
-
const PREVIEW_BOUND_SWAP_TOOLS = new Set([
|
|
17
|
+
const PREVIEW_BOUND_SWAP_TOOLS = new Set([
|
|
18
|
+
"swap_solana_tokens",
|
|
19
|
+
"swap_solana_privately",
|
|
20
|
+
"flash_trade_open_position",
|
|
21
|
+
"flash_trade_close_position",
|
|
22
|
+
]);
|
|
17
23
|
const PRIVATE_SWAP_APPROVAL_TOOL_NAME = "swap_solana_privately";
|
|
18
24
|
const approvalPreviewCache = new Map();
|
|
19
25
|
const privateSwapOrderCache = new Map();
|
|
@@ -390,10 +396,16 @@ function resolvePythonBin(config) {
|
|
|
390
396
|
return config.pythonBin || process.env.OPENCLAW_AGENT_WALLET_PYTHON || "python3";
|
|
391
397
|
}
|
|
392
398
|
|
|
399
|
+
function resolveOpenclawHome(config) {
|
|
400
|
+
return path.resolve(config.openclawHome || process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw"));
|
|
401
|
+
}
|
|
402
|
+
|
|
393
403
|
function resolvePackageRoot(config) {
|
|
404
|
+
const openclawHome = resolveOpenclawHome(config);
|
|
394
405
|
const candidates = [
|
|
395
406
|
config.packageRoot,
|
|
396
407
|
process.env.OPENCLAW_AGENT_WALLET_PACKAGE_ROOT,
|
|
408
|
+
path.join(openclawHome, "agent-wallet-runtime/current/agent-wallet"),
|
|
397
409
|
path.resolve(PLUGIN_ROOT, "../../../agent-wallet"),
|
|
398
410
|
path.resolve(process.cwd(), "agent-wallet"),
|
|
399
411
|
].filter(Boolean);
|
|
@@ -405,7 +417,7 @@ function resolvePackageRoot(config) {
|
|
|
405
417
|
}
|
|
406
418
|
}
|
|
407
419
|
throw new Error(
|
|
408
|
-
|
|
420
|
+
`Could not resolve agent-wallet package root. Checked ${path.join(openclawHome, "agent-wallet-runtime/current/agent-wallet")} and local workspace fallbacks. Set plugins.entries.agent-wallet.config.packageRoot if your runtime lives elsewhere.`
|
|
409
421
|
);
|
|
410
422
|
}
|
|
411
423
|
|
|
@@ -1019,6 +1031,38 @@ const solanaToolDefinitions = [
|
|
|
1019
1031
|
additionalProperties: false,
|
|
1020
1032
|
},
|
|
1021
1033
|
},
|
|
1034
|
+
{
|
|
1035
|
+
name: "get_flash_trade_markets",
|
|
1036
|
+
description: "List Flash Trade perpetual markets currently available on Solana mainnet.",
|
|
1037
|
+
parameters: {
|
|
1038
|
+
type: "object",
|
|
1039
|
+
properties: {
|
|
1040
|
+
pool_name: {
|
|
1041
|
+
type: "string",
|
|
1042
|
+
description: "Optional Flash pool identifier such as Crypto.1.",
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
additionalProperties: false,
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
name: "get_flash_trade_positions",
|
|
1050
|
+
description: "Get Flash Trade perpetual positions for a Solana wallet on mainnet.",
|
|
1051
|
+
parameters: {
|
|
1052
|
+
type: "object",
|
|
1053
|
+
properties: {
|
|
1054
|
+
owner: {
|
|
1055
|
+
type: "string",
|
|
1056
|
+
description: "Optional Solana wallet address override. If omitted, use the configured wallet.",
|
|
1057
|
+
},
|
|
1058
|
+
pool_name: {
|
|
1059
|
+
type: "string",
|
|
1060
|
+
description: "Optional Flash pool identifier such as Crypto.1.",
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
additionalProperties: false,
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1022
1066
|
{
|
|
1023
1067
|
name: "get_kamino_lend_markets",
|
|
1024
1068
|
description: "List Kamino lending markets currently available on Solana mainnet.",
|
|
@@ -1402,6 +1446,85 @@ const solanaToolDefinitions = [
|
|
|
1402
1446
|
additionalProperties: false,
|
|
1403
1447
|
},
|
|
1404
1448
|
},
|
|
1449
|
+
{
|
|
1450
|
+
name: "flash_trade_open_position",
|
|
1451
|
+
description: "Preview, prepare, or execute a Flash Trade same-collateral perpetual open on Solana mainnet.",
|
|
1452
|
+
optional: true,
|
|
1453
|
+
parameters: {
|
|
1454
|
+
type: "object",
|
|
1455
|
+
properties: {
|
|
1456
|
+
pool_name: {
|
|
1457
|
+
type: "string",
|
|
1458
|
+
description: "Flash pool identifier such as Crypto.1.",
|
|
1459
|
+
},
|
|
1460
|
+
market_symbol: {
|
|
1461
|
+
type: "string",
|
|
1462
|
+
description: "Flash market symbol such as SOL or BTC.",
|
|
1463
|
+
},
|
|
1464
|
+
collateral_symbol: {
|
|
1465
|
+
type: "string",
|
|
1466
|
+
description: "Collateral symbol. Current integration requires the same symbol as market_symbol.",
|
|
1467
|
+
},
|
|
1468
|
+
collateral_amount_raw: {
|
|
1469
|
+
type: "string",
|
|
1470
|
+
description: "Collateral amount in raw token units.",
|
|
1471
|
+
},
|
|
1472
|
+
leverage: {
|
|
1473
|
+
type: "string",
|
|
1474
|
+
description: "Requested leverage as a decimal string such as 5 or 7.5.",
|
|
1475
|
+
},
|
|
1476
|
+
side: {
|
|
1477
|
+
type: "string",
|
|
1478
|
+
enum: ["long", "short"],
|
|
1479
|
+
description: "Position direction.",
|
|
1480
|
+
},
|
|
1481
|
+
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1482
|
+
purpose: { type: "string" },
|
|
1483
|
+
user_intent: { type: "boolean" },
|
|
1484
|
+
approval_token: { type: "string" },
|
|
1485
|
+
},
|
|
1486
|
+
required: [
|
|
1487
|
+
"pool_name",
|
|
1488
|
+
"market_symbol",
|
|
1489
|
+
"collateral_symbol",
|
|
1490
|
+
"collateral_amount_raw",
|
|
1491
|
+
"leverage",
|
|
1492
|
+
"side",
|
|
1493
|
+
"mode",
|
|
1494
|
+
"purpose",
|
|
1495
|
+
],
|
|
1496
|
+
additionalProperties: false,
|
|
1497
|
+
},
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
name: "flash_trade_close_position",
|
|
1501
|
+
description: "Preview, prepare, or execute a Flash Trade same-collateral perpetual close on Solana mainnet.",
|
|
1502
|
+
optional: true,
|
|
1503
|
+
parameters: {
|
|
1504
|
+
type: "object",
|
|
1505
|
+
properties: {
|
|
1506
|
+
pool_name: {
|
|
1507
|
+
type: "string",
|
|
1508
|
+
description: "Flash pool identifier such as Crypto.1.",
|
|
1509
|
+
},
|
|
1510
|
+
market_symbol: {
|
|
1511
|
+
type: "string",
|
|
1512
|
+
description: "Flash market symbol such as SOL or BTC.",
|
|
1513
|
+
},
|
|
1514
|
+
side: {
|
|
1515
|
+
type: "string",
|
|
1516
|
+
enum: ["long", "short"],
|
|
1517
|
+
description: "Position direction to close.",
|
|
1518
|
+
},
|
|
1519
|
+
mode: { type: "string", enum: ["preview", "prepare", "execute"] },
|
|
1520
|
+
purpose: { type: "string" },
|
|
1521
|
+
user_intent: { type: "boolean" },
|
|
1522
|
+
approval_token: { type: "string" },
|
|
1523
|
+
},
|
|
1524
|
+
required: ["pool_name", "market_symbol", "side", "mode", "purpose"],
|
|
1525
|
+
additionalProperties: false,
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1405
1528
|
{
|
|
1406
1529
|
name: "close_empty_token_accounts",
|
|
1407
1530
|
description: "Preview or execute closing zero-balance token accounts. Execute requires a host-issued approval token bound to the previewed operation.",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"claim_bags_fees",
|
|
9
9
|
"close_empty_token_accounts",
|
|
10
10
|
"continue_solana_private_swap",
|
|
11
|
+
"flash_trade_close_position",
|
|
12
|
+
"flash_trade_open_position",
|
|
11
13
|
"get_active_wallet_backend",
|
|
12
14
|
"get_bags_claimable_positions",
|
|
13
15
|
"get_bags_fee_analytics",
|
|
@@ -26,6 +28,8 @@
|
|
|
26
28
|
"get_evm_token_balance",
|
|
27
29
|
"get_evm_token_metadata",
|
|
28
30
|
"get_evm_transaction_receipt",
|
|
31
|
+
"get_flash_trade_markets",
|
|
32
|
+
"get_flash_trade_positions",
|
|
29
33
|
"get_jupiter_earn_earnings",
|
|
30
34
|
"get_jupiter_earn_positions",
|
|
31
35
|
"get_jupiter_earn_tokens",
|
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "agent-wallet",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"
|
|
2
|
+
"name": "@agentlayertech/agent-wallet-plugin",
|
|
3
|
+
"version": "0.1.16",
|
|
4
|
+
"description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN ../../../LICENSE",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/lopushok9/Agent-Layer.git",
|
|
10
|
+
"directory": ".openclaw/extensions/agent-wallet"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/lopushok9/Agent-Layer/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/lopushok9/Agent-Layer/tree/main/.openclaw/extensions/agent-wallet#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.ts",
|
|
21
|
+
"dist/",
|
|
22
|
+
"openclaw.plugin.json",
|
|
23
|
+
"README.md",
|
|
24
|
+
"skills/"
|
|
25
|
+
],
|
|
6
26
|
"openclaw": {
|
|
7
27
|
"extensions": [
|
|
8
28
|
"./index.ts"
|
|
9
|
-
]
|
|
10
|
-
|
|
29
|
+
],
|
|
30
|
+
"runtimeExtensions": [
|
|
31
|
+
"./dist/index.js"
|
|
32
|
+
],
|
|
33
|
+
"compat": {
|
|
34
|
+
"pluginApi": ">=2026.3.24-beta.2",
|
|
35
|
+
"minGatewayVersion": "2026.3.24-beta.2"
|
|
36
|
+
},
|
|
37
|
+
"build": {
|
|
38
|
+
"openclawVersion": "2026.3.24-beta.2",
|
|
39
|
+
"pluginSdkVersion": "2026.3.24-beta.2"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"openclaw",
|
|
44
|
+
"plugin",
|
|
45
|
+
"wallet",
|
|
46
|
+
"solana",
|
|
47
|
+
"bitcoin",
|
|
48
|
+
"evm"
|
|
49
|
+
]
|
|
11
50
|
}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Thin OpenClaw bridge to the locally installed `pay` CLI.
|
|
4
4
|
|
|
5
|
+
External install path:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
5
11
|
This plugin is intentionally separate from `agent-wallet`:
|
|
6
12
|
|
|
7
13
|
- `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
6
|
+
|
|
7
|
+
function stripAnsi(text) {
|
|
8
|
+
return String(text || "").replace(ANSI_RE, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nonEmptyLines(text) {
|
|
12
|
+
return stripAnsi(text)
|
|
13
|
+
.split(/\r?\n/)
|
|
14
|
+
.map((line) => line.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractLastJsonValue(text) {
|
|
19
|
+
const lines = nonEmptyLines(text);
|
|
20
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
21
|
+
const candidate = lines[i];
|
|
22
|
+
if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(candidate);
|
|
25
|
+
} catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectStringLeaves(value, acc = new Set()) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
acc.add(value);
|
|
35
|
+
return acc;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
for (const item of value) collectStringLeaves(item, acc);
|
|
39
|
+
return acc;
|
|
40
|
+
}
|
|
41
|
+
if (value && typeof value === "object") {
|
|
42
|
+
for (const item of Object.values(value)) collectStringLeaves(item, acc);
|
|
43
|
+
}
|
|
44
|
+
return acc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function withAccountArgs(args, account) {
|
|
48
|
+
if (!account) return args;
|
|
49
|
+
return [...args, "--account", account];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolvePayBinary(config = {}) {
|
|
53
|
+
return (
|
|
54
|
+
config.payBinary ||
|
|
55
|
+
process.env.OPENCLAW_PAY_BINARY ||
|
|
56
|
+
"pay"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runPayCommand(payBinary, args, options = {}) {
|
|
61
|
+
const { cwd, input = null } = options;
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let stderr = "";
|
|
64
|
+
try {
|
|
65
|
+
const result = await execFileAsync(payBinary, args, {
|
|
66
|
+
cwd,
|
|
67
|
+
env: { ...process.env },
|
|
68
|
+
input,
|
|
69
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
70
|
+
});
|
|
71
|
+
stdout = result.stdout ?? "";
|
|
72
|
+
stderr = result.stderr ?? "";
|
|
73
|
+
} catch (error) {
|
|
74
|
+
stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
|
75
|
+
stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
|
76
|
+
const payload = extractLastJsonValue(stdout) || extractLastJsonValue(stderr);
|
|
77
|
+
const message =
|
|
78
|
+
payload?.error?.message ||
|
|
79
|
+
payload?.message ||
|
|
80
|
+
stripAnsi(stderr || stdout || error?.message || "pay command failed").trim() ||
|
|
81
|
+
"pay command failed";
|
|
82
|
+
const wrapped = new Error(message);
|
|
83
|
+
wrapped.stdout = stdout;
|
|
84
|
+
wrapped.stderr = stderr;
|
|
85
|
+
wrapped.details = payload && typeof payload === "object" ? payload : null;
|
|
86
|
+
throw wrapped;
|
|
87
|
+
}
|
|
88
|
+
return { stdout, stderr };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function parseWhoamiOutput(stdout) {
|
|
92
|
+
const lines = nonEmptyLines(stdout);
|
|
93
|
+
const systemUser = lines[0] || null;
|
|
94
|
+
const noAccount = lines.some((line) => /no mainnet account/i.test(line));
|
|
95
|
+
return {
|
|
96
|
+
system_user: systemUser,
|
|
97
|
+
has_mainnet_account: !noAccount,
|
|
98
|
+
raw_lines: lines,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseAccountListOutput(stdout) {
|
|
103
|
+
const lines = nonEmptyLines(stdout);
|
|
104
|
+
const noAccounts = lines.some((line) => /no accounts found/i.test(line));
|
|
105
|
+
return {
|
|
106
|
+
has_accounts: !noAccounts,
|
|
107
|
+
raw_lines: lines,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function getPayStatus(config = {}, options = {}) {
|
|
112
|
+
const payBinary = resolvePayBinary(config);
|
|
113
|
+
const versionResult = await runPayCommand(payBinary, ["--version"], options);
|
|
114
|
+
const whoamiResult = await runPayCommand(
|
|
115
|
+
payBinary,
|
|
116
|
+
withAccountArgs(["whoami"], config.defaultAccount),
|
|
117
|
+
options
|
|
118
|
+
);
|
|
119
|
+
const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
|
|
120
|
+
return {
|
|
121
|
+
installed: true,
|
|
122
|
+
pay_binary: payBinary,
|
|
123
|
+
version: stripAnsi(versionResult.stdout).trim() || null,
|
|
124
|
+
account_configured: parseWhoamiOutput(whoamiResult.stdout).has_mainnet_account,
|
|
125
|
+
has_any_accounts: parseAccountListOutput(accountListResult.stdout).has_accounts,
|
|
126
|
+
whoami: parseWhoamiOutput(whoamiResult.stdout),
|
|
127
|
+
accounts: parseAccountListOutput(accountListResult.stdout),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function getPayWalletInfo(config = {}, options = {}) {
|
|
132
|
+
const payBinary = resolvePayBinary(config);
|
|
133
|
+
const whoamiResult = await runPayCommand(
|
|
134
|
+
payBinary,
|
|
135
|
+
withAccountArgs(["whoami"], config.defaultAccount),
|
|
136
|
+
options
|
|
137
|
+
);
|
|
138
|
+
const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
|
|
139
|
+
return {
|
|
140
|
+
pay_binary: payBinary,
|
|
141
|
+
default_account: config.defaultAccount || null,
|
|
142
|
+
whoami: parseWhoamiOutput(whoamiResult.stdout),
|
|
143
|
+
accounts: parseAccountListOutput(accountListResult.stdout),
|
|
144
|
+
notes: [
|
|
145
|
+
"This wallet is managed by pay.sh and is separate from the AgentLayer execution wallet.",
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function searchPayServices(config = {}, params = {}, options = {}) {
|
|
151
|
+
const payBinary = resolvePayBinary(config);
|
|
152
|
+
const args = ["skills", "search"];
|
|
153
|
+
if (params.query) args.push(String(params.query));
|
|
154
|
+
if (params.category) args.push("--category", String(params.category));
|
|
155
|
+
args.push("--json");
|
|
156
|
+
const { stdout, stderr } = await runPayCommand(
|
|
157
|
+
payBinary,
|
|
158
|
+
withAccountArgs(args, params.account || config.defaultAccount),
|
|
159
|
+
options
|
|
160
|
+
);
|
|
161
|
+
const parsed = JSON.parse(stdout.trim() || "{}");
|
|
162
|
+
return {
|
|
163
|
+
query: params.query || "",
|
|
164
|
+
category: params.category || null,
|
|
165
|
+
results: parsed,
|
|
166
|
+
warnings: nonEmptyLines(stderr),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function getPayServiceEndpoints(config = {}, params = {}, options = {}) {
|
|
171
|
+
const payBinary = resolvePayBinary(config);
|
|
172
|
+
const args = [
|
|
173
|
+
"skills",
|
|
174
|
+
"endpoints",
|
|
175
|
+
String(params.service_fqn),
|
|
176
|
+
String(params.resource),
|
|
177
|
+
"--json",
|
|
178
|
+
];
|
|
179
|
+
const { stdout, stderr } = await runPayCommand(
|
|
180
|
+
payBinary,
|
|
181
|
+
withAccountArgs(args, params.account || config.defaultAccount),
|
|
182
|
+
options
|
|
183
|
+
);
|
|
184
|
+
const parsed = JSON.parse(stdout.trim() || "{}");
|
|
185
|
+
return {
|
|
186
|
+
service_fqn: String(params.service_fqn),
|
|
187
|
+
resource: String(params.resource),
|
|
188
|
+
endpoints: parsed,
|
|
189
|
+
warnings: nonEmptyLines(stderr),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ensureHttps(url, requireHttps = true) {
|
|
194
|
+
if (!requireHttps) return;
|
|
195
|
+
const parsed = new URL(url);
|
|
196
|
+
if (parsed.protocol !== "https:") {
|
|
197
|
+
throw new Error("pay_api_request only allows https URLs.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function appendQuery(url, query) {
|
|
202
|
+
const parsed = new URL(url);
|
|
203
|
+
if (query && typeof query === "object") {
|
|
204
|
+
for (const [key, value] of Object.entries(query)) {
|
|
205
|
+
if (value === undefined || value === null) continue;
|
|
206
|
+
parsed.searchParams.set(key, String(value));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return parsed.toString();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function endpointPayloadContainsUrl(endpointPayload, url) {
|
|
213
|
+
const strings = collectStringLeaves(endpointPayload);
|
|
214
|
+
return strings.has(url);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function executePayApiRequest(config = {}, params = {}, options = {}) {
|
|
218
|
+
if (params.user_confirmed !== true) {
|
|
219
|
+
throw new Error("pay_api_request requires user_confirmed=true.");
|
|
220
|
+
}
|
|
221
|
+
if (!params.purpose || !String(params.purpose).trim()) {
|
|
222
|
+
throw new Error("pay_api_request requires a non-empty purpose.");
|
|
223
|
+
}
|
|
224
|
+
if (!params.service_fqn || !params.resource || !params.url) {
|
|
225
|
+
throw new Error("pay_api_request requires service_fqn, resource, and url.");
|
|
226
|
+
}
|
|
227
|
+
if (params.json_body !== undefined && params.text_body !== undefined) {
|
|
228
|
+
throw new Error("Provide either json_body or text_body, not both.");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const endpointData = await getPayServiceEndpoints(config, {
|
|
232
|
+
account: params.account,
|
|
233
|
+
resource: params.resource,
|
|
234
|
+
service_fqn: params.service_fqn,
|
|
235
|
+
}, options);
|
|
236
|
+
|
|
237
|
+
const finalUrl = appendQuery(String(params.url), params.query);
|
|
238
|
+
ensureHttps(finalUrl, config.requireHttps !== false);
|
|
239
|
+
if (!endpointPayloadContainsUrl(endpointData.endpoints, String(params.url))) {
|
|
240
|
+
throw new Error("The requested URL is not present in pay_get_service_endpoints for this service/resource.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const payBinary = resolvePayBinary(config);
|
|
244
|
+
const method = String(params.method || "GET").toUpperCase();
|
|
245
|
+
const args = ["curl"];
|
|
246
|
+
if (params.account || config.defaultAccount) {
|
|
247
|
+
args.push("--account", String(params.account || config.defaultAccount));
|
|
248
|
+
}
|
|
249
|
+
args.push("--request", method);
|
|
250
|
+
|
|
251
|
+
const headers = params.headers && typeof params.headers === "object" ? params.headers : {};
|
|
252
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
253
|
+
args.push("--header", `${key}: ${String(value)}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (params.json_body !== undefined) {
|
|
257
|
+
const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
|
|
258
|
+
if (!hasContentType) {
|
|
259
|
+
args.push("--header", "content-type: application/json");
|
|
260
|
+
}
|
|
261
|
+
args.push("--data", JSON.stringify(params.json_body));
|
|
262
|
+
} else if (params.text_body !== undefined) {
|
|
263
|
+
args.push("--data", String(params.text_body));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
args.push(finalUrl);
|
|
267
|
+
const { stdout, stderr } = await runPayCommand(payBinary, args, options);
|
|
268
|
+
const trimmed = stdout.trim();
|
|
269
|
+
let responseBody = trimmed;
|
|
270
|
+
if (params.parse_json_response !== false) {
|
|
271
|
+
try {
|
|
272
|
+
responseBody = JSON.parse(trimmed);
|
|
273
|
+
} catch {
|
|
274
|
+
responseBody = trimmed;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
method,
|
|
279
|
+
purpose: String(params.purpose),
|
|
280
|
+
request_url: finalUrl,
|
|
281
|
+
service_fqn: String(params.service_fqn),
|
|
282
|
+
resource: String(params.resource),
|
|
283
|
+
response: responseBody,
|
|
284
|
+
raw_response_text: trimmed,
|
|
285
|
+
warnings: nonEmptyLines(stderr),
|
|
286
|
+
};
|
|
287
|
+
}
|