@stellar-agent/cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3606 @@
1
+ #!/usr/bin/env node
2
+ import { EXIT_CODES, StellarAgentError, configSchema, createDefaultConfig, fail, formatStroops, ok, parseAmount, paymentRequestSchema, redactSensitive, resolvePath, serializeError } from "@stellar-agent/core";
3
+ import { latestLedger, lookupTransaction, parseStellarCliTransactionHash, resolveNetworkProfile } from "@stellar-agent/stellar";
4
+ import { appendEvent, latestReceipt, listReceipts, readReceipt, spendHistoryFromReceipts, verifyReceipt, writeReceipt } from "@stellar-agent/ledger-logger";
5
+ import { DEFAULT_MAINNET_POLICY, DEFAULT_TESTNET_POLICY, defaultPolicyForNetwork, evaluateDefiBlendRequest, evaluateMarketLiquidityRequest, evaluatePaymentRequest, parsePolicyYaml, policyToYaml } from "@stellar-agent/policy";
6
+ import { createTestnetHarness, ensureWallet, importPublicWallet, initTestnetWorkspace, listWalletPublicViews, loadWallet, loadWalletPublic, walletBalances, walletTrustlines } from "@stellar-agent/testnet-suite";
7
+ import { Command } from "commander";
8
+ import { realpathSync } from "node:fs";
9
+ import { createRequire } from "node:module";
10
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
14
+ const require = createRequire(import.meta.url);
15
+ const { version: VERSION } = require("../package.json");
16
+ export function buildProgram() {
17
+ const program = new Command();
18
+ program
19
+ .name("stellar-agent")
20
+ .description("Stellar Agent Bridge\n\nSafe agentic payments on Stellar from your terminal.")
21
+ .version(VERSION)
22
+ .option("--profile <name>", "Network profile to use: testnet, mainnet, local")
23
+ .option("--config <path>", "Path to config file")
24
+ .option("--policy <path>", "Path to policy file")
25
+ .option("--json", "Print machine-readable JSON")
26
+ .option("--no-color", "Disable colored output")
27
+ .option("--verbose", "Print additional diagnostics")
28
+ .option("--quiet", "Suppress nonessential output")
29
+ .option("--no-cache", "Disable session caches for network lookups")
30
+ .addHelpText("after", `
31
+
32
+ Quick start:
33
+ stellar-agent testnet init
34
+ stellar-agent testnet smoke-test
35
+ stellar-agent testnet scenario basic-payment
36
+ stellar-agent receipts latest
37
+
38
+ Common commands:
39
+ stellar-agent testnet doctor
40
+ stellar-agent wallet balance
41
+ stellar-agent wallet trustline add --asset USD:G... --account merchant
42
+ stellar-agent pay quote --to G... --amount 1 --asset XLM --fee-strategy medium
43
+ stellar-agent pay send --to G... --amount 1 --asset XLM
44
+ stellar-agent pay batch --file ./payments.json
45
+ stellar-agent tx submit-approval appr_...
46
+ stellar-agent claimable create --to G... --amount 1
47
+ stellar-agent cache inspect
48
+ stellar-agent contract invoke --id C... --source agent --fn hello --arg to=world
49
+ stellar-agent market pools list --asset-a XLM --asset-b USDC:G... --json
50
+ stellar-agent market lp preflight --pool 0123... --max-a 1 --max-b 1 --min-price 0.9 --max-price 1.1 --json
51
+ stellar-agent policy explain --request ./payment-request.json`);
52
+ addProfileCommands(program);
53
+ addTestnetCommands(program);
54
+ addWalletCommands(program);
55
+ addApprovalCommands(program);
56
+ addTransactionCommands(program);
57
+ addPayCommands(program);
58
+ addClaimableCommands(program);
59
+ addContractCommands(program);
60
+ addMarketCommands(program);
61
+ addStrategyCommands(program);
62
+ addDefiCommands(program);
63
+ addPolicyCommands(program);
64
+ addLedgerCommands(program);
65
+ addReceiptCommands(program);
66
+ addCacheCommands(program);
67
+ addMainnetCommands(program);
68
+ return program;
69
+ }
70
+ function addCacheCommands(program) {
71
+ const cache = program.command("cache").description("Inspect and clear in-process session caches.");
72
+ cache
73
+ .command("inspect")
74
+ .description("Show cached Horizon and tool-readiness lookups for this CLI process.")
75
+ .action(withContext(async () => {
76
+ const { stellarSessionCacheSnapshot } = await import("@stellar-agent/stellar");
77
+ return { entries: stellarSessionCacheSnapshot() };
78
+ }, "Session cache inspected."));
79
+ cache
80
+ .command("clear")
81
+ .description("Clear in-process session caches.")
82
+ .action(withContext(async () => {
83
+ const { clearStellarSessionCache } = await import("@stellar-agent/stellar");
84
+ clearStellarSessionCache();
85
+ return { cleared: true };
86
+ }, "Session cache cleared."));
87
+ }
88
+ function addProfileCommands(program) {
89
+ const profile = program.command("profile").description("Inspect and manage network profiles.");
90
+ profile
91
+ .command("list")
92
+ .description("List configured profiles.")
93
+ .action(withContext(async (context) => context.config.profiles, "Configured profiles listed."));
94
+ profile
95
+ .command("inspect")
96
+ .description("Inspect a network profile.")
97
+ .argument("[name]", "Profile name")
98
+ .action(withContext(async (context, name) => {
99
+ const target = name ?? context.profileName;
100
+ return resolveNetworkProfile(target, context.config.profiles);
101
+ }, "Profile inspected."));
102
+ profile
103
+ .command("use")
104
+ .description("Set the active profile.")
105
+ .argument("<name>", "Profile name")
106
+ .action(withContext(async (context, name) => {
107
+ const target = resolveNetworkProfile(name, context.config.profiles);
108
+ context.config.activeProfile = target.name;
109
+ await writeConfig(context.config, context.options);
110
+ return { activeProfile: target.name, realFunds: target.realFunds };
111
+ }, "Active profile updated."));
112
+ }
113
+ function addTestnetCommands(program) {
114
+ const testnet = program.command("testnet").description("Run safe Testnet workflows.");
115
+ testnet
116
+ .command("doctor")
117
+ .description("Check local Testnet readiness.")
118
+ .option("--live", "Also check network endpoints")
119
+ .action(withContext(async (context, options) => {
120
+ const checks = [
121
+ { name: "node", ok: Number(process.versions.node.split(".")[0]) >= 22, detail: process.version },
122
+ { name: "profile", ok: Boolean(context.config.profiles.testnet), detail: "testnet profile configured" },
123
+ { name: "storage", ok: Boolean(context.config.storage.rootDir), detail: context.config.storage.rootDir },
124
+ {
125
+ name: "network",
126
+ ok: options.live ? Boolean(context.config.profiles.testnet?.horizonUrl) : true,
127
+ detail: options.live ? context.config.profiles.testnet?.horizonUrl : "skipped"
128
+ },
129
+ {
130
+ name: "stellar-cli",
131
+ ok: true,
132
+ detail: await import("@stellar-agent/stellar").then(({ checkStellarCli }) => checkStellarCli())
133
+ }
134
+ ];
135
+ return { checks, ok: checks.every((check) => check.ok) };
136
+ }, "Doctor checks complete."));
137
+ testnet
138
+ .command("init")
139
+ .description("Create local Testnet wallets, policy, config, logs, and receipts directories.")
140
+ .option("--no-fund", "Create local state without calling Friendbot")
141
+ .option("--overwrite-policy", "Overwrite the default Testnet policy")
142
+ .action(withContext(async (context, options) => {
143
+ const result = await initTestnetWorkspace(context.config, {
144
+ ...(options.fund === undefined ? {} : { fund: options.fund }),
145
+ ...(options.overwritePolicy === undefined ? {} : { overwritePolicy: options.overwritePolicy })
146
+ });
147
+ if (context.options.config)
148
+ await writeConfig(context.config, context.options);
149
+ return {
150
+ ...result,
151
+ configPath: context.options.config ? resolvePath(context.options.config) : result.configPath
152
+ };
153
+ }, "Testnet workspace initialized."));
154
+ testnet
155
+ .command("reset")
156
+ .description("Plan a reset of generated Testnet state.")
157
+ .option("--soft", "Preserve wallets and policies")
158
+ .option("--wallets", "Also reset generated Testnet wallets")
159
+ .option("--yes", "Confirm destructive reset")
160
+ .action(withContext(async (_context, options) => {
161
+ if (options.wallets && !options.yes) {
162
+ throw new StellarAgentError({
163
+ code: "INVALID_INPUT",
164
+ message: "Resetting wallets requires --yes.",
165
+ docs: "docs/quickstart-testnet.md#reset"
166
+ });
167
+ }
168
+ return { planned: true, soft: Boolean(options.soft), wallets: Boolean(options.wallets) };
169
+ }, "Reset plan generated."));
170
+ const friendbot = testnet
171
+ .command("friendbot")
172
+ .alias("fund")
173
+ .description("Fund a Testnet account with Friendbot. Alias: testnet fund.")
174
+ .option("--account <name>", "Local wallet name", "agent")
175
+ .option("--address <address>", "Raw Stellar public key");
176
+ friendbot.action(withContext(async (context, options) => {
177
+ const { fundWithFriendbot } = await import("@stellar-agent/stellar");
178
+ const address = options.address ?? (await loadWallet(context.config, options.account)).publicKey;
179
+ const result = await fundWithFriendbot(address, context.config.profiles.testnet);
180
+ return { address, ...result };
181
+ }, "Friendbot funding complete."));
182
+ testnet
183
+ .command("create-pair")
184
+ .description("Create local agent and merchant Testnet wallets if missing.")
185
+ .action(withContext(async (context) => ({
186
+ agent: await ensureWallet(context.config, "agent"),
187
+ merchant: await ensureWallet(context.config, "merchant")
188
+ }), "Testnet pair ready."));
189
+ testnet
190
+ .command("smoke-test")
191
+ .description("Run the install-to-Testnet-payment workflow.")
192
+ .option("--dry-run", "Evaluate locally without submitting a transaction")
193
+ .action(withContext(async (context, options) => {
194
+ const harness = createTestnetHarness(context.config);
195
+ return harness.runBasicPayment(options.dryRun === undefined ? {} : { dryRun: options.dryRun });
196
+ }, "Smoke test complete."));
197
+ const scenario = testnet.command("scenario").description("Run canned Testnet scenarios.");
198
+ scenario
199
+ .command("basic-payment")
200
+ .description("Run a canned Testnet XLM payment scenario.")
201
+ .option("--dry-run", "Evaluate locally without submitting a transaction")
202
+ .action(withContext(async (context, options) => {
203
+ return createTestnetHarness(context.config).runBasicPayment({
204
+ amount: "1",
205
+ memo: "basic-payment scenario",
206
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
207
+ });
208
+ }, "Basic payment scenario complete."));
209
+ scenario
210
+ .command("issued-asset-payment")
211
+ .description("Run a canned issued-asset trustline and payment scenario.")
212
+ .option("--issuer <name>", "Local issuer wallet name", "issuer")
213
+ .option("--recipient <name>", "Local recipient wallet name", "merchant")
214
+ .option("--asset-code <code>", "Issued asset code; generated when omitted")
215
+ .option("--amount <amount>", "Issued asset amount", "0.0000001")
216
+ .option("--memo <memo>", "Payment memo", "issued-asset scenario")
217
+ .option("--no-fund", "Do not call Friendbot before submitting")
218
+ .option("--dry-run", "Plan the scenario without funding, trustline, or payment submission")
219
+ .action(withContext(async (context, options) => {
220
+ return createTestnetHarness(context.config).runIssuedAssetPaymentScenario({
221
+ issuer: options.issuer,
222
+ recipient: options.recipient,
223
+ amount: options.amount,
224
+ memo: options.memo,
225
+ ...(options.assetCode === undefined ? {} : { assetCode: options.assetCode }),
226
+ ...(options.fund === undefined ? {} : { fund: options.fund }),
227
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
228
+ });
229
+ }, "Issued-asset scenario complete."));
230
+ scenario
231
+ .command("contract-asset-smoke")
232
+ .description("Run a Stellar CLI-backed asset contract smoke scenario.")
233
+ .option("--source <source>", "Local wallet name, Stellar CLI identity, raw public key, or Testnet secret key", "agent")
234
+ .option("--asset <asset>", "Asset as native or CODE:G... issuer", "native")
235
+ .option("--alias <alias>", "Stellar CLI alias for deployed asset contract")
236
+ .option("--ledgers-to-extend <n>", "Number of ledgers to extend", parseLedgersToExtendOption, 535679)
237
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
238
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
239
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
240
+ .option("--no-fund", "Do not Friendbot-fund a local Testnet source wallet")
241
+ .option("--dry-run", "Plan the scenario without submitting contract transactions")
242
+ .action(withContext(async (context, options) => createTestnetHarness(context.config).runContractAssetSmokeScenario({
243
+ source: options.source,
244
+ asset: options.asset,
245
+ ...(options.alias === undefined ? {} : { alias: options.alias }),
246
+ ledgersToExtend: options.ledgersToExtend,
247
+ stellarBinary: options.stellarBinary,
248
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
249
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache }),
250
+ ...(options.fund === undefined ? {} : { fund: options.fund }),
251
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
252
+ }), "Contract asset smoke scenario complete."));
253
+ scenario
254
+ .command("policy-denied")
255
+ .description("Verify a policy denial without signing or submitting.")
256
+ .action(withContext(async (context) => createTestnetHarness(context.config).runPolicyDeniedScenario(), "Policy-denied scenario complete."));
257
+ scenario
258
+ .command("approval-required")
259
+ .description("Verify approval-required behavior.")
260
+ .option("--auto-deny", "Complete without signing")
261
+ .option("--approve-testnet", "Proceed on Testnet after approval")
262
+ .action(withContext(async (context, options) => {
263
+ return createTestnetHarness(context.config).runApprovalRequiredScenario(options);
264
+ }, "Approval-required scenario complete."));
265
+ scenario
266
+ .command("x402-payment")
267
+ .description("Run a local x402-style Testnet payment scenario.")
268
+ .option("--dry-run", "Evaluate the 402 requirement without submitting payment")
269
+ .action(withContext(async (context, options) => {
270
+ const { runX402Payment, startPaidApiDemo } = await import("@stellar-agent/x402-client");
271
+ const source = await loadWallet(context.config, "agent");
272
+ const merchant = await loadWallet(context.config, "merchant");
273
+ const demo = await startPaidApiDemo({ recipient: merchant.publicKey });
274
+ try {
275
+ const policy = enableX402ForUrl(DEFAULT_TESTNET_POLICY, demo.url);
276
+ return await runX402Payment({
277
+ url: demo.url,
278
+ source,
279
+ policy,
280
+ profile: context.config.profiles.testnet,
281
+ receiptsDir: context.config.storage.receiptsDir,
282
+ eventLog: join(context.config.storage.logsDir, "events.jsonl"),
283
+ command: "testnet scenario x402-payment",
284
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
285
+ });
286
+ }
287
+ finally {
288
+ await demo.close();
289
+ }
290
+ }, "x402 payment scenario complete."));
291
+ testnet
292
+ .command("export-report")
293
+ .description("Export a local Testnet report with wallets, receipts, and event-log paths.")
294
+ .option("--output <path>", "Output JSON path")
295
+ .action(withContext(async (context, options) => {
296
+ return writeLocalReport(context, options.output ?? join(context.config.storage.ledgersDir, "testnet-report.json"));
297
+ }, "Testnet report exported."));
298
+ }
299
+ function addWalletCommands(program) {
300
+ const wallet = program.command("wallet").description("Manage local and connected wallets.");
301
+ wallet
302
+ .command("create")
303
+ .description("Create a Testnet wallet. Mainnet secret storage is not implemented.")
304
+ .option("--fund", "Fund the Testnet wallet with Friendbot after creation")
305
+ .action(withContext(async (context, options) => createTestnetWalletResult(context, "agent", Boolean(options.fund)), "Wallet created."));
306
+ wallet
307
+ .command("create-testnet")
308
+ .description("Create a local Testnet wallet.")
309
+ .argument("[name]", "Wallet name", "agent")
310
+ .option("--fund", "Fund the Testnet wallet with Friendbot after creation")
311
+ .action(withContext(async (context, name, options) => createTestnetWalletResult(context, name, Boolean(options.fund)), "Testnet wallet created."));
312
+ wallet
313
+ .command("balance")
314
+ .description("Show wallet balances.")
315
+ .option("--account <name>", "Local wallet name", "agent")
316
+ .action(withContext(async (context, options) => ({
317
+ account: options.account,
318
+ balances: await walletBalances(context.config, options.account)
319
+ }), "Wallet balance loaded."));
320
+ wallet
321
+ .command("address")
322
+ .description("Show a local wallet public address.")
323
+ .option("--account <name>", "Local wallet name", "agent")
324
+ .action(withContext(async (context, options) => {
325
+ const wallet = await loadWalletPublic(context.config, options.account);
326
+ return { account: options.account, publicKey: wallet.publicKey, network: wallet.network, hasSecret: wallet.hasSecret };
327
+ }, "Wallet address loaded."));
328
+ wallet
329
+ .command("export-public")
330
+ .description("Export public wallet metadata.")
331
+ .option("--account <name>", "Local wallet name", "agent")
332
+ .action(withContext(async (context, options) => {
333
+ const wallet = await loadWalletPublic(context.config, options.account);
334
+ return redactSensitive(wallet);
335
+ }, "Public wallet metadata exported."));
336
+ wallet
337
+ .command("import-public")
338
+ .description("Import a watch-only public wallet for balance and ledger inspection.")
339
+ .requiredOption("--name <name>", "Local watch-only wallet name")
340
+ .requiredOption("--address <address>", "Stellar public key")
341
+ .option("--network <network>", "Network name: testnet, mainnet, local", "mainnet")
342
+ .action(withContext(async (context, options) => {
343
+ const wallet = await importPublicWallet({
344
+ config: context.config,
345
+ name: options.name,
346
+ publicKey: options.address,
347
+ network: options.network
348
+ });
349
+ return { ...wallet, hasSecret: false, realFunds: options.network === "mainnet" };
350
+ }, "Watch-only wallet imported."));
351
+ wallet
352
+ .command("use-env")
353
+ .description("Report whether STELLAR_SECRET_KEY is configured without printing it.")
354
+ .action(withContext(async () => ({ configured: Boolean(process.env.STELLAR_SECRET_KEY), secretPrinted: false }), "Environment wallet checked."));
355
+ wallet
356
+ .command("status")
357
+ .description("Show local wallet status.")
358
+ .action(withContext(async (context) => {
359
+ const wallets = await listWalletPublicViews(context.config);
360
+ return { wallets };
361
+ }, "Wallet status loaded."));
362
+ wallet
363
+ .command("connect-freighter")
364
+ .description("Show local approval bridge details for Freighter-backed approval flows.")
365
+ .action(withContext(async (context) => {
366
+ const { listApprovalRequests } = await import("@stellar-agent/freighter-bridge");
367
+ const requests = await listApprovalRequests(context.config.storage.approvalsDir);
368
+ return {
369
+ implemented: true,
370
+ mode: "local_approval_bridge",
371
+ approvalsDir: context.config.storage.approvalsDir,
372
+ pendingRequests: requests.filter((request) => request.status === "pending").length,
373
+ startServer: "stellar-agent approval serve",
374
+ ui: "Open the approval bridge URL in a browser with Freighter installed.",
375
+ secretsHandled: false
376
+ };
377
+ }, "Freighter approval bridge details loaded."));
378
+ const trustline = wallet.command("trustline").description("List, add, or remove issued-asset trustlines.");
379
+ trustline
380
+ .command("list")
381
+ .description("List issued-asset trustlines for an account.")
382
+ .option("--account <name>", "Local wallet name", "merchant")
383
+ .action(withContext(async (context, options) => {
384
+ const wallet = await loadWalletPublic(context.config, options.account);
385
+ return {
386
+ account: options.account,
387
+ publicKey: wallet.publicKey,
388
+ trustlines: await walletTrustlines(context.config, options.account)
389
+ };
390
+ }, "Trustlines listed."));
391
+ trustline
392
+ .command("add")
393
+ .description("Add or update a trustline for an issued asset.")
394
+ .requiredOption("--asset <asset>", "Issued asset as CODE:G... issuer")
395
+ .option("--account <name>", "Local wallet name", "merchant")
396
+ .option("--limit <amount>", "Trustline limit", "922337203685.4775807")
397
+ .action(withContext(async (context, options) => {
398
+ const { changeTrustline } = await import("@stellar-agent/stellar");
399
+ const source = await loadWallet(context.config, options.account);
400
+ const transaction = await changeTrustline({
401
+ source,
402
+ asset: options.asset,
403
+ limit: options.limit,
404
+ profile: context.config.profiles.testnet
405
+ });
406
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
407
+ event: "transaction_confirmed",
408
+ status: "trustline_added",
409
+ command: "wallet trustline add",
410
+ profile: "testnet",
411
+ data: { account: options.account, asset: options.asset, transaction }
412
+ });
413
+ const receiptPath = await writeOperationReceipt(context, {
414
+ command: "wallet trustline add",
415
+ operation: {
416
+ type: "trustline.add",
417
+ account: options.account,
418
+ source: source.publicKey,
419
+ asset: options.asset,
420
+ limit: transaction.limit
421
+ },
422
+ transaction
423
+ });
424
+ return { account: options.account, publicKey: source.publicKey, transaction, receiptPath };
425
+ }, "Trustline added."));
426
+ trustline
427
+ .command("remove")
428
+ .description("Remove a trustline by setting its limit to 0.")
429
+ .requiredOption("--asset <asset>", "Issued asset as CODE:G... issuer")
430
+ .option("--account <name>", "Local wallet name", "merchant")
431
+ .action(withContext(async (context, options) => {
432
+ const { removeTrustline } = await import("@stellar-agent/stellar");
433
+ const source = await loadWallet(context.config, options.account);
434
+ const transaction = await removeTrustline({
435
+ source,
436
+ asset: options.asset,
437
+ profile: context.config.profiles.testnet
438
+ });
439
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
440
+ event: "transaction_confirmed",
441
+ status: "trustline_removed",
442
+ command: "wallet trustline remove",
443
+ profile: "testnet",
444
+ data: { account: options.account, asset: options.asset, transaction }
445
+ });
446
+ const receiptPath = await writeOperationReceipt(context, {
447
+ command: "wallet trustline remove",
448
+ operation: {
449
+ type: "trustline.remove",
450
+ account: options.account,
451
+ source: source.publicKey,
452
+ asset: options.asset,
453
+ limit: transaction.limit
454
+ },
455
+ transaction
456
+ });
457
+ return { account: options.account, publicKey: source.publicKey, transaction, receiptPath };
458
+ }, "Trustline removed."));
459
+ }
460
+ function addApprovalCommands(program) {
461
+ const approval = program.command("approval").description("Create, inspect, decide, and serve local approval requests.");
462
+ approval
463
+ .command("create-payment")
464
+ .description("Create a local payment approval request.")
465
+ .requiredOption("--to <address>", "Destination public key")
466
+ .requiredOption("--amount <amount>", "Payment amount")
467
+ .option("--asset <asset>", "Asset", "XLM")
468
+ .option("--from <account>", "Local source wallet name", "agent")
469
+ .option("--memo <memo>", "Memo")
470
+ .action(withContext(async (context, options) => {
471
+ const { createPaymentApprovalRequest } = await import("@stellar-agent/freighter-bridge");
472
+ const source = await loadWalletPublic(context.config, options.from);
473
+ const request = paymentRequestSchema.parse({
474
+ source: source.publicKey,
475
+ destination: options.to,
476
+ amount: options.amount,
477
+ asset: options.asset,
478
+ memo: options.memo,
479
+ network: context.profileName
480
+ });
481
+ const approval = await createPaymentApprovalRequest({
482
+ approvalsDir: context.config.storage.approvalsDir,
483
+ payment: request
484
+ });
485
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
486
+ event: "approval_requested",
487
+ status: "pending",
488
+ command: "approval create-payment",
489
+ profile: context.profileName,
490
+ requestId: approval.id,
491
+ data: approval
492
+ });
493
+ return approval;
494
+ }, "Approval request created."));
495
+ approval
496
+ .command("create-transaction")
497
+ .description("Create a local transaction-XDR approval request for browser-wallet signing.")
498
+ .requiredOption("--xdr <base64>", "Unsigned transaction XDR")
499
+ .option("--summary <summary>", "Human-readable signing summary", "Approve transaction XDR")
500
+ .option("--network <network>", "Network name: testnet, mainnet, local")
501
+ .action(withContext(async (context, options) => {
502
+ const { createTransactionXdrApprovalRequest } = await import("@stellar-agent/freighter-bridge");
503
+ const approval = await createTransactionXdrApprovalRequest({
504
+ approvalsDir: context.config.storage.approvalsDir,
505
+ network: options.network ?? context.profileName,
506
+ transactionXdr: options.xdr,
507
+ summary: options.summary
508
+ });
509
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
510
+ event: "approval_requested",
511
+ status: "pending",
512
+ command: "approval create-transaction",
513
+ profile: approval.network,
514
+ requestId: approval.id,
515
+ data: approval
516
+ });
517
+ return approval;
518
+ }, "Transaction approval request created."));
519
+ approval
520
+ .command("list")
521
+ .description("List local approval requests.")
522
+ .action(withContext(async (context) => {
523
+ const { listApprovalRequests } = await import("@stellar-agent/freighter-bridge");
524
+ return { approvals: await listApprovalRequests(context.config.storage.approvalsDir) };
525
+ }, "Approval requests listed."));
526
+ approval
527
+ .command("show")
528
+ .description("Show a local approval request.")
529
+ .argument("<id>", "Approval request id")
530
+ .action(withContext(async (context, id) => {
531
+ const { readApprovalRequest } = await import("@stellar-agent/freighter-bridge");
532
+ return readApprovalRequest(context.config.storage.approvalsDir, id);
533
+ }, "Approval request loaded."));
534
+ approval
535
+ .command("decide")
536
+ .description("Approve or deny a local approval request.")
537
+ .argument("<id>", "Approval request id")
538
+ .option("--approve", "Approve the request")
539
+ .option("--deny", "Deny the request")
540
+ .option("--reason <reason>", "Decision reason")
541
+ .option("--signer-public-key <address>", "Signer public key for signed transaction XDR")
542
+ .option("--signed-transaction-xdr <base64>", "Signed transaction XDR returned by Freighter")
543
+ .action(withContext(async (context, id, options) => {
544
+ const signing = Boolean(options.signedTransactionXdr);
545
+ if (!signing && Boolean(options.approve) === Boolean(options.deny)) {
546
+ throw new StellarAgentError({
547
+ code: "INVALID_INPUT",
548
+ message: "Pass exactly one of --approve or --deny.",
549
+ docs: "docs/mainnet-safety.md#local-approval-bridge"
550
+ });
551
+ }
552
+ const { decideApprovalRequest } = await import("@stellar-agent/freighter-bridge");
553
+ const decided = await decideApprovalRequest({
554
+ approvalsDir: context.config.storage.approvalsDir,
555
+ id,
556
+ approved: signing || Boolean(options.approve),
557
+ ...(options.reason === undefined ? {} : { reason: options.reason }),
558
+ ...(options.signerPublicKey === undefined ? {} : { signerPublicKey: options.signerPublicKey }),
559
+ ...(options.signedTransactionXdr === undefined ? {} : { signedTransactionXdr: options.signedTransactionXdr })
560
+ });
561
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
562
+ event: decided.status === "denied" ? "approval_denied" : "approval_granted",
563
+ status: decided.status,
564
+ command: "approval decide",
565
+ profile: context.profileName,
566
+ requestId: decided.id,
567
+ data: decided
568
+ });
569
+ return decided;
570
+ }, "Approval request decided."));
571
+ approval
572
+ .command("serve")
573
+ .description("Start the local approval bridge HTTP server.")
574
+ .option("--host <host>", "Host", "127.0.0.1")
575
+ .option("--port <port>", "Port", parsePortOption, 0)
576
+ .action(async (options, command) => {
577
+ const parent = command.optsWithGlobals();
578
+ const config = await loadConfig(parent);
579
+ const profileName = (parent.profile ?? process.env.STELLAR_AGENT_PROFILE ?? config.activeProfile ?? "testnet");
580
+ const context = { options: parent, config, profileName };
581
+ const { startApprovalBridge } = await import("@stellar-agent/freighter-bridge");
582
+ const bridge = await startApprovalBridge({
583
+ approvalsDir: context.config.storage.approvalsDir,
584
+ host: options.host,
585
+ port: options.port
586
+ });
587
+ if (parent.json) {
588
+ process.stdout.write(`${JSON.stringify(ok({ url: bridge.url, authToken: bridge.authToken, approvalsDir: context.config.storage.approvalsDir }))}\n`);
589
+ }
590
+ else {
591
+ process.stdout.write(`Approval bridge listening at ${bridge.url}\n`);
592
+ process.stdout.write("Approval bridge API requires the printed session token for non-browser requests.\n");
593
+ process.stdout.write(`Session token: ${bridge.authToken}\n`);
594
+ }
595
+ await new Promise((resolveStop) => {
596
+ process.once("SIGINT", resolveStop);
597
+ process.once("SIGTERM", resolveStop);
598
+ });
599
+ await bridge.close();
600
+ });
601
+ }
602
+ function addTransactionCommands(program) {
603
+ const tx = program.command("tx").description("Build and submit transaction XDR on Testnet or guarded Mainnet.");
604
+ tx
605
+ .command("build-payment")
606
+ .description("Build unsigned payment transaction XDR for browser-wallet signing.")
607
+ .requiredOption("--to <address>", "Destination public key")
608
+ .requiredOption("--amount <amount>", "Payment amount")
609
+ .option("--asset <asset>", "Asset", "XLM")
610
+ .option("--from <accountOrAddress>", "Local wallet name, watch-only wallet name, or source public key", "agent")
611
+ .option("--memo <memo>", "Memo")
612
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
613
+ .option("--allow-real-funds", "Permit guarded Mainnet payment-XDR building")
614
+ .option("--i-understand-real-funds", "Acknowledge this payment uses real funds")
615
+ .action(withContext(async (context, options) => {
616
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
617
+ assertGuardedRealFundsProfile(context, profile, {
618
+ allowRealFunds: Boolean(options.allowRealFunds),
619
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds),
620
+ action: "payment-XDR building"
621
+ });
622
+ const { buildPaymentTransactionXdr } = await import("@stellar-agent/stellar");
623
+ const sourcePublicKey = await resolvePaymentSourcePublicKey(context, options.from, { realFunds: profile.realFunds });
624
+ const request = paymentRequestSchema.parse({
625
+ source: sourcePublicKey,
626
+ destination: options.to,
627
+ amount: options.amount,
628
+ asset: options.asset,
629
+ memo: options.memo,
630
+ network: profile.name
631
+ });
632
+ const policy = await loadPolicy(context);
633
+ const history = await loadSpendHistory(context, request);
634
+ const policyDecision = evaluatePaymentRequest(policy, request, history);
635
+ if (policyDecision.status === "denied")
636
+ throw policyDeniedError();
637
+ if (profile.realFunds && policyDecision.status === "requires_approval") {
638
+ throw new StellarAgentError({
639
+ code: "APPROVAL_REQUIRED",
640
+ message: "Mainnet payment-XDR building requires a transaction approval request.",
641
+ hint: "Use tx request-payment-signature so the unsigned XDR is recorded for human approval.",
642
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
643
+ });
644
+ }
645
+ const built = await buildPaymentTransactionXdr({
646
+ sourcePublicKey,
647
+ destination: options.to,
648
+ amount: options.amount,
649
+ asset: options.asset,
650
+ ...(options.memo === undefined ? {} : { memo: options.memo }),
651
+ profile,
652
+ allowRealFunds: profile.realFunds,
653
+ feeStrategy: parseFeeStrategy(options.feeStrategy),
654
+ noCache: context.options.noCache
655
+ });
656
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
657
+ event: "transaction_built",
658
+ status: "unsigned_payment_xdr",
659
+ command: "tx build-payment",
660
+ profile: profile.name,
661
+ data: { source: sourcePublicKey, destination: options.to, asset: options.asset, amount: built.amount, policyDecision }
662
+ });
663
+ return { ...built, policyDecision, spendHistory: history, realFunds: profile.realFunds };
664
+ }, "Unsigned payment transaction built."));
665
+ tx
666
+ .command("request-payment-signature")
667
+ .description("Build unsigned payment XDR and create a local transaction approval request.")
668
+ .requiredOption("--to <address>", "Destination public key")
669
+ .requiredOption("--amount <amount>", "Payment amount")
670
+ .option("--asset <asset>", "Asset", "XLM")
671
+ .option("--from <accountOrAddress>", "Local wallet name, watch-only wallet name, or source public key", "agent")
672
+ .option("--memo <memo>", "Memo")
673
+ .option("--summary <summary>", "Human-readable signing summary")
674
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
675
+ .option("--allow-real-funds", "Permit a guarded Mainnet payment signature request")
676
+ .option("--i-understand-real-funds", "Acknowledge this payment uses real funds")
677
+ .action(withContext(async (context, options) => {
678
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
679
+ assertGuardedRealFundsProfile(context, profile, {
680
+ allowRealFunds: Boolean(options.allowRealFunds),
681
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds),
682
+ action: "payment signature request"
683
+ });
684
+ const { createTransactionXdrApprovalRequest } = await import("@stellar-agent/freighter-bridge");
685
+ const { buildPaymentTransactionXdr } = await import("@stellar-agent/stellar");
686
+ const sourcePublicKey = await resolvePaymentSourcePublicKey(context, options.from, { realFunds: profile.realFunds });
687
+ const request = paymentRequestSchema.parse({
688
+ source: sourcePublicKey,
689
+ destination: options.to,
690
+ amount: options.amount,
691
+ asset: options.asset,
692
+ memo: options.memo,
693
+ network: profile.name
694
+ });
695
+ const policy = await loadPolicy(context);
696
+ const history = await loadSpendHistory(context, request);
697
+ const policyDecision = evaluatePaymentRequest(policy, request, history);
698
+ if (policyDecision.status === "denied")
699
+ throw policyDeniedError();
700
+ const built = await buildPaymentTransactionXdr({
701
+ sourcePublicKey,
702
+ destination: options.to,
703
+ amount: options.amount,
704
+ asset: options.asset,
705
+ ...(options.memo === undefined ? {} : { memo: options.memo }),
706
+ profile,
707
+ allowRealFunds: profile.realFunds,
708
+ feeStrategy: parseFeeStrategy(options.feeStrategy),
709
+ noCache: context.options.noCache
710
+ });
711
+ const approval = await createTransactionXdrApprovalRequest({
712
+ approvalsDir: context.config.storage.approvalsDir,
713
+ network: profile.name,
714
+ transactionXdr: built.xdr,
715
+ summary: options.summary ?? `Sign ${built.amount} ${built.asset} payment to ${options.to} on ${profile.name}`
716
+ });
717
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
718
+ event: "approval_requested",
719
+ status: "pending",
720
+ command: "tx request-payment-signature",
721
+ profile: profile.name,
722
+ requestId: approval.id,
723
+ data: { approval, source: sourcePublicKey, destination: options.to, asset: options.asset, amount: built.amount, policyDecision }
724
+ });
725
+ return { approval, built, policyDecision, spendHistory: history, realFunds: profile.realFunds };
726
+ }, "Payment signature approval request created."));
727
+ tx
728
+ .command("submit-xdr")
729
+ .description("Submit signed transaction XDR to Horizon.")
730
+ .requiredOption("--xdr <base64>", "Signed transaction XDR")
731
+ .option("--allow-real-funds", "Permit guarded Mainnet signed-XDR submission")
732
+ .option("--i-understand-real-funds", "Acknowledge this transaction uses real funds")
733
+ .action(withContext(async (context, options) => {
734
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
735
+ assertGuardedRealFundsProfile(context, profile, {
736
+ allowRealFunds: Boolean(options.allowRealFunds),
737
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds),
738
+ action: "signed-XDR submission"
739
+ });
740
+ const { submitTransactionXdr } = await import("@stellar-agent/stellar");
741
+ const transaction = await submitTransactionXdr({
742
+ xdr: options.xdr,
743
+ profile,
744
+ allowRealFunds: profile.realFunds
745
+ });
746
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
747
+ event: "transaction_confirmed",
748
+ status: "signed_xdr_submitted",
749
+ command: "tx submit-xdr",
750
+ profile: profile.name,
751
+ data: { transaction }
752
+ });
753
+ const receiptPath = await writeSubmittedXdrReceipt(context, {
754
+ command: "tx submit-xdr",
755
+ profile,
756
+ operation: {
757
+ type: "tx.submit_xdr",
758
+ details: {
759
+ source: "external_signed_xdr",
760
+ realFundsAcknowledged: profile.realFunds
761
+ }
762
+ },
763
+ transaction
764
+ });
765
+ return { transaction, receiptPath, realFunds: profile.realFunds };
766
+ }, "Signed transaction submitted."));
767
+ tx
768
+ .command("submit-approval")
769
+ .description("Submit signed transaction XDR recorded on a local approval request.")
770
+ .argument("<id>", "Approval request id")
771
+ .option("--allow-real-funds", "Permit guarded Mainnet signed approval submission")
772
+ .option("--i-understand-real-funds", "Acknowledge this transaction uses real funds")
773
+ .action(withContext(async (context, id, options) => {
774
+ const { readApprovalRequest } = await import("@stellar-agent/freighter-bridge");
775
+ const approval = await readApprovalRequest(context.config.storage.approvalsDir, id);
776
+ if (approval.kind !== "transaction_xdr") {
777
+ throw new StellarAgentError({
778
+ code: "INVALID_INPUT",
779
+ message: "Approval request must be a transaction-XDR request.",
780
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
781
+ });
782
+ }
783
+ if (approval.network !== context.profileName) {
784
+ throw new StellarAgentError({
785
+ code: "MAINNET_NOT_ENABLED",
786
+ message: "The active profile must match the approval request network.",
787
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
788
+ });
789
+ }
790
+ if (approval.status !== "signed" || !approval.decision?.signedTransactionXdr) {
791
+ throw new StellarAgentError({
792
+ code: "APPROVAL_REQUIRED",
793
+ message: "Approval request does not contain signed transaction XDR.",
794
+ hint: "Open the approval bridge with Freighter installed, sign the request, then retry.",
795
+ docs: "docs/mainnet-safety.md#local-approval-bridge"
796
+ });
797
+ }
798
+ const profile = resolveNetworkProfile(approval.network, context.config.profiles);
799
+ assertGuardedRealFundsProfile(context, profile, {
800
+ allowRealFunds: Boolean(options.allowRealFunds),
801
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds),
802
+ action: "signed approval submission"
803
+ });
804
+ const { submitTransactionXdr } = await import("@stellar-agent/stellar");
805
+ const transaction = await submitTransactionXdr({
806
+ xdr: approval.decision.signedTransactionXdr,
807
+ profile,
808
+ allowRealFunds: profile.realFunds
809
+ });
810
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
811
+ event: "transaction_confirmed",
812
+ status: "signed_approval_submitted",
813
+ command: "tx submit-approval",
814
+ profile: profile.name,
815
+ requestId: approval.id,
816
+ data: {
817
+ approvalId: approval.id,
818
+ signerPublicKey: approval.decision.signerPublicKey,
819
+ transaction
820
+ }
821
+ });
822
+ const receiptPath = await writeSubmittedXdrReceipt(context, {
823
+ command: "tx submit-approval",
824
+ profile,
825
+ operation: {
826
+ type: "tx.submit_approval",
827
+ details: {
828
+ approvalId: approval.id,
829
+ signerPublicKey: approval.decision.signerPublicKey,
830
+ realFundsAcknowledged: profile.realFunds
831
+ }
832
+ },
833
+ transaction
834
+ });
835
+ return { approvalId: approval.id, signerPublicKey: approval.decision.signerPublicKey, transaction, receiptPath, realFunds: profile.realFunds };
836
+ }, "Signed approval transaction submitted."));
837
+ }
838
+ function addPayCommands(program) {
839
+ const pay = program.command("pay").description("Quote and send payments.");
840
+ pay
841
+ .command("quote")
842
+ .description("Evaluate a payment without signing or submitting.")
843
+ .requiredOption("--to <address>", "Destination public key")
844
+ .requiredOption("--amount <amount>", "Payment amount")
845
+ .option("--asset <asset>", "Asset", "XLM")
846
+ .option("--memo <memo>", "Memo")
847
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
848
+ .action(withContext(async (context, options) => {
849
+ const policy = await loadPolicy(context);
850
+ const request = paymentRequestSchema.parse({
851
+ destination: options.to,
852
+ amount: options.amount,
853
+ asset: options.asset,
854
+ memo: options.memo,
855
+ network: context.profileName
856
+ });
857
+ const history = await loadSpendHistory(context, request);
858
+ const { estimateTransactionFee } = await import("@stellar-agent/stellar");
859
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
860
+ return {
861
+ request,
862
+ estimatedFee: await estimateTransactionFee({
863
+ profile,
864
+ operationCount: 1,
865
+ strategy: parseFeeStrategy(options.feeStrategy),
866
+ noCache: context.options.noCache
867
+ }),
868
+ policyDecision: evaluatePaymentRequest(policy, request, history),
869
+ spendHistory: history,
870
+ profile: context.profileName
871
+ };
872
+ }, "Payment quote complete."));
873
+ pay
874
+ .command("send")
875
+ .description("Send an XLM or issued-asset payment on Testnet when policy allows.")
876
+ .requiredOption("--to <address>", "Destination public key")
877
+ .requiredOption("--amount <amount>", "Payment amount")
878
+ .option("--asset <asset>", "Asset", "XLM")
879
+ .option("--from <account>", "Local source wallet name", "agent")
880
+ .option("--memo <memo>", "Memo")
881
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
882
+ .option("--approval-id <id>", "Approved local approval request id")
883
+ .option("--dry-run", "Evaluate locally without submitting")
884
+ .action(withContext(async (context, options) => {
885
+ if (context.profileName === "mainnet") {
886
+ throw new StellarAgentError({
887
+ code: "MAINNET_NOT_ENABLED",
888
+ message: "Mainnet payment submission is blocked in v0.",
889
+ docs: "docs/mainnet-safety.md"
890
+ });
891
+ }
892
+ const policy = await loadPolicy(context);
893
+ const source = await loadWallet(context.config, options.from);
894
+ const request = paymentRequestSchema.parse({
895
+ source: source.publicKey,
896
+ destination: options.to,
897
+ amount: options.amount,
898
+ asset: options.asset,
899
+ memo: options.memo,
900
+ network: "testnet"
901
+ });
902
+ const history = await loadSpendHistory(context, request);
903
+ const decision = evaluatePaymentRequest(policy, request, history);
904
+ if (options.dryRun)
905
+ return { request, policyDecision: decision, spendHistory: history, dryRun: true };
906
+ if (decision.status === "denied") {
907
+ throw new StellarAgentError({
908
+ code: "POLICY_DENIED",
909
+ message: "Payment request was denied by policy.",
910
+ hint: "Run policy explain to inspect the matched rules.",
911
+ docs: "docs/troubleshooting.md#policy-denied"
912
+ });
913
+ }
914
+ if (decision.status === "requires_approval") {
915
+ const { assertPaymentApproval, createPaymentApprovalRequest } = await import("@stellar-agent/freighter-bridge");
916
+ if (options.approvalId) {
917
+ await assertPaymentApproval({
918
+ approvalsDir: context.config.storage.approvalsDir,
919
+ approvalId: options.approvalId,
920
+ payment: request
921
+ });
922
+ }
923
+ else {
924
+ const approval = await createPaymentApprovalRequest({
925
+ approvalsDir: context.config.storage.approvalsDir,
926
+ payment: request
927
+ });
928
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
929
+ event: "approval_requested",
930
+ status: "pending",
931
+ command: "pay send",
932
+ profile: "testnet",
933
+ requestId: approval.id,
934
+ data: approval
935
+ });
936
+ throw new StellarAgentError({
937
+ code: "APPROVAL_REQUIRED",
938
+ message: "Payment requires approval.",
939
+ hint: `Review approval '${approval.id}', then rerun with --approval-id ${approval.id}.`,
940
+ docs: "docs/mainnet-safety.md#local-approval-bridge",
941
+ details: { approvalId: approval.id, approval }
942
+ });
943
+ }
944
+ }
945
+ const { sendPayment } = await import("@stellar-agent/stellar");
946
+ const transaction = await sendPayment({
947
+ source,
948
+ destination: options.to,
949
+ amount: options.amount,
950
+ asset: options.asset,
951
+ ...(options.memo === undefined ? {} : { memo: options.memo }),
952
+ profile: context.config.profiles.testnet,
953
+ feeStrategy: parseFeeStrategy(options.feeStrategy),
954
+ noCache: context.options.noCache
955
+ });
956
+ const eventLog = join(context.config.storage.logsDir, "events.jsonl");
957
+ await appendEvent(eventLog, {
958
+ event: "transaction_confirmed",
959
+ status: "successful",
960
+ command: "pay send",
961
+ profile: "testnet",
962
+ data: { transaction, source: options.from, destination: options.to, asset: options.asset }
963
+ });
964
+ const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
965
+ command: "pay send",
966
+ profile: "testnet",
967
+ networkPassphrase: context.config.profiles.testnet.networkPassphrase,
968
+ realFunds: false,
969
+ payment: {
970
+ source: source.publicKey,
971
+ destination: options.to,
972
+ asset: options.asset,
973
+ amount: request.amount,
974
+ ...(options.memo === undefined ? {} : { memo: options.memo })
975
+ },
976
+ policyDecision: decision,
977
+ transaction,
978
+ ...(transaction.ledger === undefined ? {} : { ledger: { confirmedLedger: transaction.ledger } }),
979
+ eventLog
980
+ });
981
+ await appendEvent(eventLog, {
982
+ event: "receipt_written",
983
+ status: "success",
984
+ command: "pay send",
985
+ profile: "testnet",
986
+ data: { receiptPath }
987
+ });
988
+ return {
989
+ request,
990
+ policyDecision: decision,
991
+ transaction,
992
+ receiptPath
993
+ };
994
+ }, "Payment send complete."));
995
+ pay
996
+ .command("batch")
997
+ .description("Submit multiple Testnet payments in one guarded transaction.")
998
+ .requiredOption("--file <path>", "JSON file containing a payment array")
999
+ .option("--from <account>", "Local source wallet name", "agent")
1000
+ .option("--memo <memo>", "Transaction memo")
1001
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
1002
+ .option("--dry-run", "Evaluate locally without submitting")
1003
+ .action(withContext(async (context, options) => {
1004
+ if (context.profileName === "mainnet") {
1005
+ throw new StellarAgentError({
1006
+ code: "MAINNET_NOT_ENABLED",
1007
+ message: "Mainnet batch payment submission is blocked in v0.",
1008
+ hint: "Use guarded signed-XDR flows for Mainnet; stellar-agent will not auto-sign Mainnet payments.",
1009
+ docs: "docs/mainnet-safety.md"
1010
+ });
1011
+ }
1012
+ const source = await loadWallet(context.config, options.from);
1013
+ const payments = parseBatchPaymentsFile(await readFile(resolvePath(options.file), "utf8"));
1014
+ const policy = await loadPolicy(context);
1015
+ const checked = await evaluateBatchPaymentPolicy(context, {
1016
+ source: source.publicKey,
1017
+ payments,
1018
+ policy,
1019
+ ...(options.memo === undefined ? {} : { memo: options.memo })
1020
+ });
1021
+ if (checked.aggregate.status === "denied") {
1022
+ throw new StellarAgentError({
1023
+ code: "POLICY_DENIED",
1024
+ message: "One or more batch payments were denied by policy.",
1025
+ hint: "Run pay quote for each denied destination and adjust the batch or policy intentionally.",
1026
+ docs: "docs/troubleshooting.md#policy-denied",
1027
+ details: checked
1028
+ });
1029
+ }
1030
+ if (checked.aggregate.status === "requires_approval") {
1031
+ throw new StellarAgentError({
1032
+ code: "APPROVAL_REQUIRED",
1033
+ message: "One or more batch payments require approval.",
1034
+ hint: "Split approval-required payments into explicit pay send approval flows.",
1035
+ docs: "docs/mainnet-safety.md#local-approval-bridge",
1036
+ details: checked
1037
+ });
1038
+ }
1039
+ if (options.dryRun)
1040
+ return { ...checked, dryRun: true };
1041
+ const { sendPaymentBatch } = await import("@stellar-agent/stellar");
1042
+ const transaction = await sendPaymentBatch({
1043
+ source,
1044
+ payments,
1045
+ ...(options.memo === undefined ? {} : { memo: options.memo }),
1046
+ profile: context.config.profiles.testnet,
1047
+ feeStrategy: parseFeeStrategy(options.feeStrategy),
1048
+ noCache: context.options.noCache
1049
+ });
1050
+ const receiptPath = await writeOperationReceipt(context, {
1051
+ command: "pay batch",
1052
+ operation: {
1053
+ type: "pay.batch",
1054
+ source: source.publicKey,
1055
+ details: {
1056
+ operationCount: transaction.operationCount,
1057
+ payments: transaction.payments,
1058
+ aggregatePolicyDecision: checked.aggregate
1059
+ }
1060
+ },
1061
+ policyDecision: {
1062
+ status: checked.aggregate.status,
1063
+ network: "testnet",
1064
+ realFunds: false,
1065
+ matchedRules: checked.aggregate.matchedRules,
1066
+ reasons: ["Batch payment policy aggregate."]
1067
+ },
1068
+ transaction
1069
+ });
1070
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
1071
+ event: "transaction_confirmed",
1072
+ status: "batch_successful",
1073
+ command: "pay batch",
1074
+ profile: "testnet",
1075
+ data: { transaction, receiptPath }
1076
+ });
1077
+ return {
1078
+ ...checked,
1079
+ transaction,
1080
+ receiptPath
1081
+ };
1082
+ }, "Batch payment transaction submitted."));
1083
+ pay
1084
+ .command("x402")
1085
+ .description("Pay a local x402-style HTTP 402 resource on Testnet.")
1086
+ .argument("<url>", "Paid URL")
1087
+ .option("--from <account>", "Local source wallet name", "agent")
1088
+ .option("--allow-localhost-demo", "Temporarily allow localhost x402 requirements without editing policy")
1089
+ .option("--dry-run", "Evaluate the 402 requirement without submitting payment")
1090
+ .action(withContext(async (context, url, options) => {
1091
+ const { runX402Payment } = await import("@stellar-agent/x402-client");
1092
+ const source = await loadWallet(context.config, options.from);
1093
+ const basePolicy = await loadPolicy(context);
1094
+ const policy = options.allowLocalhostDemo ? enableX402ForUrl(basePolicy, url) : basePolicy;
1095
+ return runX402Payment({
1096
+ url,
1097
+ source,
1098
+ policy,
1099
+ profile: context.config.profiles.testnet,
1100
+ receiptsDir: context.config.storage.receiptsDir,
1101
+ eventLog: join(context.config.storage.logsDir, "events.jsonl"),
1102
+ command: "pay x402",
1103
+ loadSpendHistory: (request) => loadSpendHistory(context, request),
1104
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
1105
+ });
1106
+ }, "x402 payment complete."));
1107
+ pay
1108
+ .command("mpp")
1109
+ .description("Pay a local MPP one-time charge resource on Testnet.")
1110
+ .argument("<url>", "MPP URL")
1111
+ .option("--from <account>", "Local source wallet name", "agent")
1112
+ .option("--allow-localhost-demo", "Temporarily allow localhost MPP charges without editing policy")
1113
+ .option("--dry-run", "Evaluate the MPP charge without submitting payment")
1114
+ .action(withContext(async (context, url, options) => {
1115
+ const { runMppPayment } = await import("@stellar-agent/mpp-client");
1116
+ const source = await loadWallet(context.config, options.from);
1117
+ const basePolicy = await loadPolicy(context);
1118
+ const policy = options.allowLocalhostDemo ? enableX402ForUrl(basePolicy, url) : basePolicy;
1119
+ return runMppPayment({
1120
+ url,
1121
+ source,
1122
+ policy,
1123
+ profile: context.config.profiles.testnet,
1124
+ receiptsDir: context.config.storage.receiptsDir,
1125
+ eventLog: join(context.config.storage.logsDir, "events.jsonl"),
1126
+ command: "pay mpp",
1127
+ loadSpendHistory: (request) => loadSpendHistory(context, request),
1128
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
1129
+ });
1130
+ }, "MPP payment complete."));
1131
+ pay
1132
+ .command("mpp-session")
1133
+ .description("Open and use a local MPP session-budget resource on Testnet.")
1134
+ .argument("<url>", "MPP session URL")
1135
+ .option("--from <account>", "Local source wallet name", "agent")
1136
+ .option("--requests <n>", "Number of session requests to perform", parsePositiveIntegerOption, 2)
1137
+ .option("--allow-localhost-demo", "Temporarily allow localhost MPP session charges without editing policy")
1138
+ .option("--dry-run", "Evaluate the MPP session budget without submitting payment")
1139
+ .action(withContext(async (context, url, options) => {
1140
+ const { runMppSession } = await import("@stellar-agent/mpp-client");
1141
+ const source = await loadWallet(context.config, options.from);
1142
+ const basePolicy = await loadPolicy(context);
1143
+ const policy = options.allowLocalhostDemo ? enableX402ForUrl(basePolicy, url) : basePolicy;
1144
+ return runMppSession({
1145
+ url,
1146
+ source,
1147
+ policy,
1148
+ profile: context.config.profiles.testnet,
1149
+ receiptsDir: context.config.storage.receiptsDir,
1150
+ eventLog: join(context.config.storage.logsDir, "events.jsonl"),
1151
+ command: "pay mpp-session",
1152
+ requestCount: options.requests,
1153
+ loadSpendHistory: (request) => loadSpendHistory(context, request),
1154
+ ...(options.dryRun === undefined ? {} : { dryRun: options.dryRun })
1155
+ });
1156
+ }, "MPP session complete."));
1157
+ }
1158
+ function addClaimableCommands(program) {
1159
+ const claimable = program.command("claimable").description("Create, list, and claim claimable balances.");
1160
+ claimable
1161
+ .command("create")
1162
+ .description("Create a Testnet claimable balance.")
1163
+ .requiredOption("--to <address>", "Claimant public key")
1164
+ .requiredOption("--amount <amount>", "Amount")
1165
+ .option("--asset <asset>", "Asset", "XLM")
1166
+ .option("--from <account>", "Local source wallet name", "agent")
1167
+ .option("--claimant <address>", "Additional claimant public key; repeat for multiple claimants", collectArg, [])
1168
+ .option("--claimable-after <time>", "Only allow claiming at or after this Unix timestamp or ISO date/time")
1169
+ .option("--claimable-before <time>", "Only allow claiming before this Unix timestamp or ISO date/time")
1170
+ .action(withContext(async (context, options) => {
1171
+ const { createClaimableBalance } = await import("@stellar-agent/stellar");
1172
+ const source = await loadWallet(context.config, options.from);
1173
+ const result = await createClaimableBalance({
1174
+ source,
1175
+ claimant: options.to,
1176
+ claimants: options.claimant,
1177
+ amount: options.amount,
1178
+ asset: options.asset,
1179
+ ...(options.claimableAfter === undefined ? {} : { claimableAfter: options.claimableAfter }),
1180
+ ...(options.claimableBefore === undefined ? {} : { claimableBefore: options.claimableBefore }),
1181
+ profile: context.config.profiles.testnet
1182
+ });
1183
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
1184
+ event: "transaction_confirmed",
1185
+ status: "claimable_created",
1186
+ command: "claimable create",
1187
+ profile: "testnet",
1188
+ data: result
1189
+ });
1190
+ const receiptPath = await writeOperationReceipt(context, {
1191
+ command: "claimable create",
1192
+ operation: {
1193
+ type: "claimable.create",
1194
+ source: source.publicKey,
1195
+ claimant: result.claimant,
1196
+ claimants: result.claimants,
1197
+ asset: result.asset,
1198
+ amount: result.amount,
1199
+ predicate: result.predicate
1200
+ },
1201
+ transaction: result
1202
+ });
1203
+ return { ...result, receiptPath };
1204
+ }, "Claimable balance created."));
1205
+ claimable
1206
+ .command("list")
1207
+ .description("List claimable balances for an account or address.")
1208
+ .option("--account <name>", "Local wallet name")
1209
+ .option("--address <address>", "Raw claimant address")
1210
+ .action(withContext(async (context, options) => {
1211
+ const { listClaimableBalances } = await import("@stellar-agent/stellar");
1212
+ const address = options.address ?? (options.account ? (await loadWallet(context.config, options.account)).publicKey : undefined);
1213
+ if (!address) {
1214
+ throw new StellarAgentError({
1215
+ code: "INVALID_INPUT",
1216
+ message: "Provide --account or --address.",
1217
+ docs: "docs/claimable-balances.md"
1218
+ });
1219
+ }
1220
+ return { address, claimableBalances: await listClaimableBalances(address, context.config.profiles.testnet) };
1221
+ }, "Claimable balances listed."));
1222
+ claimable
1223
+ .command("claim")
1224
+ .description("Claim a claimable balance by balance id.")
1225
+ .requiredOption("--balance-id <id>", "Claimable balance id")
1226
+ .option("--account <name>", "Local wallet name", "merchant")
1227
+ .action(withContext(async (context, options) => {
1228
+ const { claimClaimableBalance } = await import("@stellar-agent/stellar");
1229
+ const source = await loadWallet(context.config, options.account);
1230
+ const result = await claimClaimableBalance({
1231
+ source,
1232
+ balanceId: options.balanceId,
1233
+ profile: context.config.profiles.testnet
1234
+ });
1235
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
1236
+ event: "transaction_confirmed",
1237
+ status: "claimable_claimed",
1238
+ command: "claimable claim",
1239
+ profile: "testnet",
1240
+ data: result
1241
+ });
1242
+ const receiptPath = await writeOperationReceipt(context, {
1243
+ command: "claimable claim",
1244
+ operation: {
1245
+ type: "claimable.claim",
1246
+ account: options.account,
1247
+ source: source.publicKey,
1248
+ balanceId: options.balanceId
1249
+ },
1250
+ transaction: result
1251
+ });
1252
+ return { ...result, receiptPath };
1253
+ }, "Claimable balance claimed."));
1254
+ }
1255
+ function addContractCommands(program) {
1256
+ const contract = program.command("contract").description("Invoke Soroban smart contracts via Stellar CLI.");
1257
+ contract
1258
+ .command("doctor")
1259
+ .description("Check whether the Stellar CLI is available for contract execution.")
1260
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1261
+ .action(withContext(async (_context, options) => {
1262
+ const { checkStellarCli } = await import("@stellar-agent/stellar");
1263
+ return checkStellarCli(options.stellarBinary);
1264
+ }, "Stellar CLI readiness checked."));
1265
+ contract
1266
+ .command("invoke")
1267
+ .description("Invoke a deployed contract using the installed stellar CLI.")
1268
+ .requiredOption("--id <contractId>", "Contract id")
1269
+ .requiredOption("--source <source>", "Stellar CLI identity, local wallet name, raw public key, or Testnet secret key")
1270
+ .requiredOption("--fn <functionName>", "Contract function name")
1271
+ .option("--arg <key=value>", "Contract argument; repeat for multiple args", collectArg, [])
1272
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1273
+ .option("--allow-real-funds", "Permit a guarded Mainnet contract operation")
1274
+ .option("--i-understand-real-funds", "Acknowledge this contract operation may use real funds")
1275
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1276
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1277
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1278
+ .action(withContext(async (context, options) => {
1279
+ const { invokeContractWithStellarCli } = await import("@stellar-agent/stellar");
1280
+ const contractContext = resolveContractExecutionContext(context, options.network, {
1281
+ mutating: true,
1282
+ allowRealFunds: Boolean(options.allowRealFunds),
1283
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds)
1284
+ });
1285
+ const source = await resolveContractSource(context, options.source, { realFunds: contractContext.realFunds });
1286
+ const contractArgs = parseKeyValueArgs(options.arg);
1287
+ const result = await invokeContractWithStellarCli({
1288
+ contractId: options.id,
1289
+ source,
1290
+ functionName: options.fn,
1291
+ contractArgs,
1292
+ network: options.network,
1293
+ rpcUrl: contractContext.rpcUrl,
1294
+ networkPassphrase: contractContext.networkPassphrase,
1295
+ stellarBinary: options.stellarBinary,
1296
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1297
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1298
+ });
1299
+ return attachContractSubmissionReceipt(context, {
1300
+ command: "contract invoke",
1301
+ network: options.network,
1302
+ result,
1303
+ operation: {
1304
+ type: "contract.invoke",
1305
+ source: options.source,
1306
+ details: {
1307
+ contractId: options.id,
1308
+ functionName: options.fn,
1309
+ args: contractArgs
1310
+ }
1311
+ }
1312
+ });
1313
+ }, "Contract invocation complete."));
1314
+ contract
1315
+ .command("deploy")
1316
+ .description("Deploy a Wasm contract using the installed stellar CLI.")
1317
+ .requiredOption("--source <source>", "Stellar CLI identity, local wallet name, raw public key, or Testnet secret key")
1318
+ .option("--wasm <path>", "Wasm file path")
1319
+ .option("--wasm-hash <hash>", "Uploaded Wasm hash")
1320
+ .option("--alias <alias>", "Stellar CLI alias for deployed contract")
1321
+ .option("--arg <key=value>", "Constructor argument; repeat for multiple args", collectArg, [])
1322
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1323
+ .option("--allow-real-funds", "Permit a guarded Mainnet contract operation")
1324
+ .option("--i-understand-real-funds", "Acknowledge this contract operation may use real funds")
1325
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1326
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1327
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1328
+ .action(withContext(async (context, options) => {
1329
+ if (!options.wasm && !options.wasmHash) {
1330
+ throw new StellarAgentError({
1331
+ code: "INVALID_INPUT",
1332
+ message: "Provide --wasm or --wasm-hash for contract deploy.",
1333
+ docs: "docs/smart-contracts.md#deploy"
1334
+ });
1335
+ }
1336
+ const { deployContractWithStellarCli } = await import("@stellar-agent/stellar");
1337
+ const contractContext = resolveContractExecutionContext(context, options.network, {
1338
+ mutating: true,
1339
+ allowRealFunds: Boolean(options.allowRealFunds),
1340
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds)
1341
+ });
1342
+ const source = await resolveContractSource(context, options.source, { realFunds: contractContext.realFunds });
1343
+ const constructorArgs = parseKeyValueArgs(options.arg);
1344
+ const result = await deployContractWithStellarCli({
1345
+ source,
1346
+ ...(options.wasm === undefined ? {} : { wasm: options.wasm }),
1347
+ ...(options.wasmHash === undefined ? {} : { wasmHash: options.wasmHash }),
1348
+ ...(options.alias === undefined ? {} : { alias: options.alias }),
1349
+ constructorArgs,
1350
+ network: options.network,
1351
+ rpcUrl: contractContext.rpcUrl,
1352
+ networkPassphrase: contractContext.networkPassphrase,
1353
+ stellarBinary: options.stellarBinary,
1354
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1355
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1356
+ });
1357
+ return attachContractSubmissionReceipt(context, {
1358
+ command: "contract deploy",
1359
+ network: options.network,
1360
+ result,
1361
+ operation: {
1362
+ type: "contract.deploy",
1363
+ source: options.source,
1364
+ details: {
1365
+ contractId: result.stdout.trim() || undefined,
1366
+ wasm: options.wasm,
1367
+ wasmHash: options.wasmHash,
1368
+ alias: options.alias,
1369
+ constructorArgs
1370
+ }
1371
+ }
1372
+ });
1373
+ }, "Contract deployed."));
1374
+ contract
1375
+ .command("upload")
1376
+ .description("Upload contract Wasm bytecode using the installed stellar CLI.")
1377
+ .requiredOption("--source <source>", "Stellar CLI identity, local wallet name, raw public key, or Testnet secret key")
1378
+ .requiredOption("--wasm <path>", "Wasm file path")
1379
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1380
+ .option("--allow-real-funds", "Permit a guarded Mainnet contract operation")
1381
+ .option("--i-understand-real-funds", "Acknowledge this contract operation may use real funds")
1382
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1383
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1384
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1385
+ .action(withContext(async (context, options) => {
1386
+ const { uploadContractWasmWithStellarCli } = await import("@stellar-agent/stellar");
1387
+ const contractContext = resolveContractExecutionContext(context, options.network, {
1388
+ mutating: true,
1389
+ allowRealFunds: Boolean(options.allowRealFunds),
1390
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds)
1391
+ });
1392
+ const source = await resolveContractSource(context, options.source, { realFunds: contractContext.realFunds });
1393
+ const result = await uploadContractWasmWithStellarCli({
1394
+ source,
1395
+ wasm: options.wasm,
1396
+ network: options.network,
1397
+ rpcUrl: contractContext.rpcUrl,
1398
+ networkPassphrase: contractContext.networkPassphrase,
1399
+ stellarBinary: options.stellarBinary,
1400
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1401
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1402
+ });
1403
+ return attachContractSubmissionReceipt(context, {
1404
+ command: "contract upload",
1405
+ network: options.network,
1406
+ result,
1407
+ operation: {
1408
+ type: "contract.upload",
1409
+ source: options.source,
1410
+ details: {
1411
+ wasm: options.wasm,
1412
+ wasmHash: result.stdout.trim() || undefined
1413
+ }
1414
+ }
1415
+ });
1416
+ }, "Contract Wasm uploaded."));
1417
+ contract
1418
+ .command("asset-deploy")
1419
+ .description("Deploy a Stellar Asset Contract using the installed stellar CLI.")
1420
+ .requiredOption("--source <source>", "Stellar CLI identity, local wallet name, raw public key, or Testnet secret key")
1421
+ .requiredOption("--asset <asset>", "Asset as native or CODE:G... issuer")
1422
+ .option("--alias <alias>", "Stellar CLI alias for deployed asset contract")
1423
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1424
+ .option("--allow-real-funds", "Permit a guarded Mainnet contract operation")
1425
+ .option("--i-understand-real-funds", "Acknowledge this contract operation may use real funds")
1426
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1427
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1428
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1429
+ .action(withContext(async (context, options) => {
1430
+ const { deployAssetContractWithStellarCli } = await import("@stellar-agent/stellar");
1431
+ const contractContext = resolveContractExecutionContext(context, options.network, {
1432
+ mutating: true,
1433
+ allowRealFunds: Boolean(options.allowRealFunds),
1434
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds)
1435
+ });
1436
+ const source = await resolveContractSource(context, options.source, { realFunds: contractContext.realFunds });
1437
+ const result = await deployAssetContractWithStellarCli({
1438
+ source,
1439
+ asset: options.asset,
1440
+ ...(options.alias === undefined ? {} : { alias: options.alias }),
1441
+ network: options.network,
1442
+ rpcUrl: contractContext.rpcUrl,
1443
+ networkPassphrase: contractContext.networkPassphrase,
1444
+ stellarBinary: options.stellarBinary,
1445
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1446
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1447
+ });
1448
+ return attachContractSubmissionReceipt(context, {
1449
+ command: "contract asset-deploy",
1450
+ network: options.network,
1451
+ result,
1452
+ operation: {
1453
+ type: "contract.asset-deploy",
1454
+ source: options.source,
1455
+ asset: options.asset,
1456
+ details: {
1457
+ contractId: result.stdout.trim() || undefined,
1458
+ alias: options.alias
1459
+ }
1460
+ }
1461
+ });
1462
+ }, "Asset contract deployed."));
1463
+ contract
1464
+ .command("asset-id")
1465
+ .description("Calculate the Stellar Asset Contract id for an asset.")
1466
+ .requiredOption("--asset <asset>", "Asset as native or CODE:G... issuer")
1467
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1468
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1469
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1470
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1471
+ .action(withContext(async (context, options) => {
1472
+ const { assetContractIdWithStellarCli } = await import("@stellar-agent/stellar");
1473
+ const contractContext = resolveContractExecutionContext(context, options.network, { mutating: false });
1474
+ return assetContractIdWithStellarCli({
1475
+ asset: options.asset,
1476
+ network: options.network,
1477
+ rpcUrl: contractContext.rpcUrl,
1478
+ networkPassphrase: contractContext.networkPassphrase,
1479
+ stellarBinary: options.stellarBinary,
1480
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1481
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1482
+ });
1483
+ }, "Asset contract id calculated."));
1484
+ contract
1485
+ .command("info")
1486
+ .description("Read contract info using the installed stellar CLI.")
1487
+ .option("--kind <kind>", "Info kind: interface, meta, env-meta, build, hash", "interface")
1488
+ .option("--id <contractId>", "Contract id")
1489
+ .option("--wasm <path>", "Wasm file path")
1490
+ .option("--wasm-hash <hash>", "Uploaded Wasm hash")
1491
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1492
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1493
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1494
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1495
+ .action(withContext(async (context, options) => {
1496
+ if (!["interface", "meta", "env-meta", "build", "hash"].includes(options.kind)) {
1497
+ throw new StellarAgentError({
1498
+ code: "INVALID_INPUT",
1499
+ message: "Info kind must be one of: interface, meta, env-meta, build, hash.",
1500
+ docs: "docs/smart-contracts.md#contract-info"
1501
+ });
1502
+ }
1503
+ if (!options.id && !options.wasm && !options.wasmHash) {
1504
+ throw new StellarAgentError({
1505
+ code: "INVALID_INPUT",
1506
+ message: "Provide --id, --wasm, or --wasm-hash for contract info.",
1507
+ docs: "docs/smart-contracts.md#contract-info"
1508
+ });
1509
+ }
1510
+ const { contractInfoWithStellarCli } = await import("@stellar-agent/stellar");
1511
+ const contractContext = resolveContractExecutionContext(context, options.network, { mutating: false });
1512
+ return contractInfoWithStellarCli({
1513
+ kind: options.kind,
1514
+ ...(options.id === undefined ? {} : { contractId: options.id }),
1515
+ ...(options.wasm === undefined ? {} : { wasm: options.wasm }),
1516
+ ...(options.wasmHash === undefined ? {} : { wasmHash: options.wasmHash }),
1517
+ network: options.network,
1518
+ rpcUrl: contractContext.rpcUrl,
1519
+ networkPassphrase: contractContext.networkPassphrase,
1520
+ stellarBinary: options.stellarBinary,
1521
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1522
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1523
+ });
1524
+ }, "Contract info loaded."));
1525
+ contract
1526
+ .command("read")
1527
+ .description("Read contract instance, storage, or Wasm ledger data using the installed stellar CLI.")
1528
+ .option("--id <contractId>", "Contract id")
1529
+ .option("--key <key>", "Storage key")
1530
+ .option("--key-xdr <xdr>", "Storage key XDR")
1531
+ .option("--wasm <path>", "Wasm file path")
1532
+ .option("--wasm-hash <hash>", "Wasm hash")
1533
+ .option("--durability <durability>", "persistent or temporary", "persistent")
1534
+ .option("--output <output>", "Output type: string, json, or xdr", "string")
1535
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1536
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1537
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1538
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1539
+ .action(withContext(async (context, options) => {
1540
+ validateDurability(options.durability);
1541
+ validateContractReadOutput(options.output);
1542
+ const { readContractWithStellarCli } = await import("@stellar-agent/stellar");
1543
+ const contractContext = resolveContractExecutionContext(context, options.network, { mutating: false });
1544
+ return readContractWithStellarCli({
1545
+ ...(options.id === undefined ? {} : { contractId: options.id }),
1546
+ ...(options.key === undefined ? {} : { key: options.key }),
1547
+ ...(options.keyXdr === undefined ? {} : { keyXdr: options.keyXdr }),
1548
+ ...(options.wasm === undefined ? {} : { wasm: options.wasm }),
1549
+ ...(options.wasmHash === undefined ? {} : { wasmHash: options.wasmHash }),
1550
+ durability: options.durability,
1551
+ output: options.output,
1552
+ network: options.network,
1553
+ rpcUrl: contractContext.rpcUrl,
1554
+ networkPassphrase: contractContext.networkPassphrase,
1555
+ stellarBinary: options.stellarBinary,
1556
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1557
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1558
+ });
1559
+ }, "Contract data read."));
1560
+ contract
1561
+ .command("fetch")
1562
+ .description("Fetch contract Wasm bytecode using the installed stellar CLI.")
1563
+ .option("--id <contractId>", "Contract id")
1564
+ .option("--wasm-hash <hash>", "Wasm hash")
1565
+ .option("--out-file <path>", "Output file path; stdout is used when omitted")
1566
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1567
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1568
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1569
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1570
+ .action(withContext(async (context, options) => {
1571
+ if (Boolean(options.id) === Boolean(options.wasmHash)) {
1572
+ throw new StellarAgentError({
1573
+ code: "INVALID_INPUT",
1574
+ message: "Provide exactly one of --id or --wasm-hash for contract fetch.",
1575
+ docs: "docs/smart-contracts.md#fetch"
1576
+ });
1577
+ }
1578
+ const { fetchContractWasmWithStellarCli } = await import("@stellar-agent/stellar");
1579
+ const contractContext = resolveContractExecutionContext(context, options.network, { mutating: false });
1580
+ return fetchContractWasmWithStellarCli({
1581
+ ...(options.id === undefined ? {} : { contractId: options.id }),
1582
+ ...(options.wasmHash === undefined ? {} : { wasmHash: options.wasmHash }),
1583
+ ...(options.outFile === undefined ? {} : { outFile: options.outFile }),
1584
+ network: options.network,
1585
+ rpcUrl: contractContext.rpcUrl,
1586
+ networkPassphrase: contractContext.networkPassphrase,
1587
+ stellarBinary: options.stellarBinary,
1588
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1589
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1590
+ });
1591
+ }, "Contract Wasm fetched."));
1592
+ contract
1593
+ .command("extend")
1594
+ .description("Extend contract instance, storage, or Wasm TTL using the installed stellar CLI.")
1595
+ .requiredOption("--source <source>", "Stellar CLI identity, local wallet name, raw public key, or Testnet secret key")
1596
+ .requiredOption("--ledgers-to-extend <n>", "Number of ledgers to extend", parseLedgersToExtendOption)
1597
+ .option("--id <contractId>", "Contract id")
1598
+ .option("--key <key>", "Storage key")
1599
+ .option("--key-xdr <xdr>", "Storage key XDR")
1600
+ .option("--wasm <path>", "Wasm file path")
1601
+ .option("--wasm-hash <hash>", "Wasm hash")
1602
+ .option("--durability <durability>", "persistent or temporary", "persistent")
1603
+ .option("--ttl-ledger-only", "Only print the new TTL ledger")
1604
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1605
+ .option("--allow-real-funds", "Permit a guarded Mainnet contract operation")
1606
+ .option("--i-understand-real-funds", "Acknowledge this contract operation may use real funds")
1607
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1608
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1609
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1610
+ .action(withContext(async (context, options) => {
1611
+ validateDurability(options.durability);
1612
+ const { extendContractWithStellarCli } = await import("@stellar-agent/stellar");
1613
+ const contractContext = resolveContractExecutionContext(context, options.network, {
1614
+ mutating: true,
1615
+ allowRealFunds: Boolean(options.allowRealFunds),
1616
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds)
1617
+ });
1618
+ const source = await resolveContractSource(context, options.source, { realFunds: contractContext.realFunds });
1619
+ const result = await extendContractWithStellarCli({
1620
+ source,
1621
+ ledgersToExtend: options.ledgersToExtend,
1622
+ ...(options.id === undefined ? {} : { contractId: options.id }),
1623
+ ...(options.key === undefined ? {} : { key: options.key }),
1624
+ ...(options.keyXdr === undefined ? {} : { keyXdr: options.keyXdr }),
1625
+ ...(options.wasm === undefined ? {} : { wasm: options.wasm }),
1626
+ ...(options.wasmHash === undefined ? {} : { wasmHash: options.wasmHash }),
1627
+ durability: options.durability,
1628
+ ...(options.ttlLedgerOnly === undefined ? {} : { ttlLedgerOnly: options.ttlLedgerOnly }),
1629
+ network: options.network,
1630
+ rpcUrl: contractContext.rpcUrl,
1631
+ networkPassphrase: contractContext.networkPassphrase,
1632
+ stellarBinary: options.stellarBinary,
1633
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1634
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1635
+ });
1636
+ return attachContractSubmissionReceipt(context, {
1637
+ command: "contract extend",
1638
+ network: options.network,
1639
+ result,
1640
+ operation: {
1641
+ type: "contract.extend",
1642
+ source: options.source,
1643
+ details: {
1644
+ contractId: options.id,
1645
+ key: options.key,
1646
+ keyXdr: options.keyXdr,
1647
+ wasm: options.wasm,
1648
+ wasmHash: options.wasmHash,
1649
+ durability: options.durability,
1650
+ ledgersToExtend: options.ledgersToExtend,
1651
+ ttlLedgerOnly: Boolean(options.ttlLedgerOnly)
1652
+ }
1653
+ }
1654
+ });
1655
+ }, "Contract TTL extended."));
1656
+ contract
1657
+ .command("restore")
1658
+ .description("Restore archived contract instance, storage, or Wasm using the installed stellar CLI.")
1659
+ .requiredOption("--source <source>", "Stellar CLI identity, local wallet name, raw public key, or Testnet secret key")
1660
+ .option("--id <contractId>", "Contract id")
1661
+ .option("--key <key>", "Storage key")
1662
+ .option("--key-xdr <xdr>", "Storage key XDR")
1663
+ .option("--wasm <path>", "Wasm file path")
1664
+ .option("--wasm-hash <hash>", "Wasm hash")
1665
+ .option("--durability <durability>", "persistent or temporary", "persistent")
1666
+ .option("--network <network>", "Stellar CLI network name", "testnet")
1667
+ .option("--allow-real-funds", "Permit a guarded Mainnet contract operation")
1668
+ .option("--i-understand-real-funds", "Acknowledge this contract operation may use real funds")
1669
+ .option("--stellar-binary <path>", "Path to stellar CLI binary", "stellar")
1670
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1671
+ .option("--stellar-no-cache", "Pass --no-cache to Stellar CLI")
1672
+ .action(withContext(async (context, options) => {
1673
+ validateDurability(options.durability);
1674
+ const { restoreContractWithStellarCli } = await import("@stellar-agent/stellar");
1675
+ const contractContext = resolveContractExecutionContext(context, options.network, {
1676
+ mutating: true,
1677
+ allowRealFunds: Boolean(options.allowRealFunds),
1678
+ acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds)
1679
+ });
1680
+ const source = await resolveContractSource(context, options.source, { realFunds: contractContext.realFunds });
1681
+ const result = await restoreContractWithStellarCli({
1682
+ source,
1683
+ ...(options.id === undefined ? {} : { contractId: options.id }),
1684
+ ...(options.key === undefined ? {} : { key: options.key }),
1685
+ ...(options.keyXdr === undefined ? {} : { keyXdr: options.keyXdr }),
1686
+ ...(options.wasm === undefined ? {} : { wasm: options.wasm }),
1687
+ ...(options.wasmHash === undefined ? {} : { wasmHash: options.wasmHash }),
1688
+ durability: options.durability,
1689
+ network: options.network,
1690
+ rpcUrl: contractContext.rpcUrl,
1691
+ networkPassphrase: contractContext.networkPassphrase,
1692
+ stellarBinary: options.stellarBinary,
1693
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1694
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1695
+ });
1696
+ return attachContractSubmissionReceipt(context, {
1697
+ command: "contract restore",
1698
+ network: options.network,
1699
+ result,
1700
+ operation: {
1701
+ type: "contract.restore",
1702
+ source: options.source,
1703
+ details: {
1704
+ contractId: options.id,
1705
+ key: options.key,
1706
+ keyXdr: options.keyXdr,
1707
+ wasm: options.wasm,
1708
+ wasmHash: options.wasmHash,
1709
+ durability: options.durability
1710
+ }
1711
+ }
1712
+ });
1713
+ }, "Contract restored."));
1714
+ }
1715
+ function addMarketCommands(program) {
1716
+ const market = program.command("market").description("Inspect markets, liquidity pools, and market-aware alerts.");
1717
+ const pools = market.command("pools").description("Inspect built-in Stellar AMM liquidity pools.");
1718
+ pools
1719
+ .command("list")
1720
+ .description("List core Stellar liquidity pools, optionally filtered by exact reserve assets or participant account.")
1721
+ .option("--asset-a <asset>", "First reserve asset, such as XLM or USD:G...")
1722
+ .option("--asset-b <asset>", "Second reserve asset, such as XLM or USD:G...")
1723
+ .option("--account <account>", "Local wallet name or public key participating in pools")
1724
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1725
+ .option("--limit <n>", "Number of records", parseIntegerOption, 10)
1726
+ .action(withContext(async (context, options) => {
1727
+ if (Boolean(options.assetA) !== Boolean(options.assetB)) {
1728
+ throw new StellarAgentError({
1729
+ code: "INVALID_INPUT",
1730
+ message: "Pool asset filtering requires both --asset-a and --asset-b.",
1731
+ docs: "docs/market-liquidity.md#core-pool-inspection"
1732
+ });
1733
+ }
1734
+ const { listLiquidityPools } = await import("@stellar-agent/stellar");
1735
+ const profile = resolveMarketProfile(context, options.network);
1736
+ return listLiquidityPools({
1737
+ profile,
1738
+ ...(options.assetA === undefined ? {} : { assetA: options.assetA }),
1739
+ ...(options.assetB === undefined ? {} : { assetB: options.assetB }),
1740
+ ...(options.account === undefined ? {} : { account: await resolvePublicAccount(context, options.account) }),
1741
+ limit: options.limit
1742
+ });
1743
+ }, "Liquidity pools listed."));
1744
+ const pool = market.command("pool").description("Inspect one core Stellar AMM liquidity pool.");
1745
+ pool
1746
+ .command("inspect")
1747
+ .description("Load reserves, shares, fee, and trustline count for a core liquidity pool.")
1748
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1749
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1750
+ .action(withContext(async (context, options) => {
1751
+ const { inspectLiquidityPool } = await import("@stellar-agent/stellar");
1752
+ return inspectLiquidityPool({ poolId: options.pool, profile: resolveMarketProfile(context, options.network) });
1753
+ }, "Liquidity pool inspected."));
1754
+ pool
1755
+ .command("trades")
1756
+ .description("Fetch recent Horizon trades for a core liquidity pool.")
1757
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1758
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1759
+ .option("--limit <n>", "Number of records", parseIntegerOption, 10)
1760
+ .action(withContext(async (context, options) => {
1761
+ const { liquidityPoolTrades } = await import("@stellar-agent/stellar");
1762
+ return liquidityPoolTrades({ poolId: options.pool, profile: resolveMarketProfile(context, options.network), limit: options.limit });
1763
+ }, "Liquidity pool trades loaded."));
1764
+ pool
1765
+ .command("position")
1766
+ .description("Inspect pool-share positions for a local wallet or public key.")
1767
+ .option("--account <account>", "Local wallet name or public key", "agent")
1768
+ .option("--pool <poolId>", "Limit to one liquidity pool id")
1769
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1770
+ .action(withContext(async (context, options) => {
1771
+ const { inspectLiquidityPoolPosition } = await import("@stellar-agent/stellar");
1772
+ return inspectLiquidityPoolPosition({
1773
+ account: await resolvePublicAccount(context, options.account),
1774
+ ...(options.pool === undefined ? {} : { poolId: options.pool }),
1775
+ profile: resolveMarketProfile(context, options.network)
1776
+ });
1777
+ }, "Liquidity pool position inspected."));
1778
+ const lp = market.command("lp").description("Preflight and submit guarded core Stellar liquidity pool actions.");
1779
+ lp
1780
+ .command("preflight")
1781
+ .description("Preflight a core liquidity pool deposit or withdrawal with policy context.")
1782
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1783
+ .option("--account <account>", "Local wallet name or public key", "agent")
1784
+ .option("--action <action>", "deposit or withdraw", "deposit")
1785
+ .option("--max-a <amount>", "Deposit max amount for reserve A")
1786
+ .option("--max-b <amount>", "Deposit max amount for reserve B")
1787
+ .option("--min-price <price>", "Deposit minimum reserve B per reserve A price")
1788
+ .option("--max-price <price>", "Deposit maximum reserve B per reserve A price")
1789
+ .option("--shares <amount>", "Pool shares to withdraw")
1790
+ .option("--min-a <amount>", "Withdrawal minimum reserve A amount", "0")
1791
+ .option("--min-b <amount>", "Withdrawal minimum reserve B amount", "0")
1792
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1793
+ .action(withContext(async (context, options) => {
1794
+ const profile = resolveMarketProfile(context, options.network);
1795
+ const preflight = await runLiquidityPreflight(context, profile, options);
1796
+ const policy = await loadPolicy(context);
1797
+ const policyDecision = evaluateMarketLiquidityRequest(policy, liquidityPolicyRequest(profile, preflight));
1798
+ return { policyDecision, preflight };
1799
+ }, "Liquidity pool preflight complete."));
1800
+ lp
1801
+ .command("deposit")
1802
+ .description("Submit a guarded Testnet core liquidity pool deposit.")
1803
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1804
+ .requiredOption("--max-a <amount>", "Deposit max amount for reserve A")
1805
+ .requiredOption("--max-b <amount>", "Deposit max amount for reserve B")
1806
+ .requiredOption("--min-price <price>", "Deposit minimum reserve B per reserve A price")
1807
+ .requiredOption("--max-price <price>", "Deposit maximum reserve B per reserve A price")
1808
+ .option("--source <account>", "Local Testnet wallet name", "agent")
1809
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", parseFeeStrategy, "medium")
1810
+ .action(withContext(async (context, options) => {
1811
+ return runLiquiditySubmitCommand(context, { command: "market lp deposit", action: "deposit", ...options });
1812
+ }, "Liquidity pool deposit submitted."));
1813
+ lp
1814
+ .command("withdraw")
1815
+ .description("Submit a guarded Testnet core liquidity pool withdrawal.")
1816
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1817
+ .requiredOption("--shares <amount>", "Pool shares to withdraw")
1818
+ .option("--min-a <amount>", "Withdrawal minimum reserve A amount", "0")
1819
+ .option("--min-b <amount>", "Withdrawal minimum reserve B amount", "0")
1820
+ .option("--source <account>", "Local Testnet wallet name", "agent")
1821
+ .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", parseFeeStrategy, "medium")
1822
+ .action(withContext(async (context, options) => {
1823
+ return runLiquiditySubmitCommand(context, { command: "market lp withdraw", action: "withdraw", ...options });
1824
+ }, "Liquidity pool withdrawal submitted."));
1825
+ const trustline = lp.command("trustline").description("Manage core liquidity pool share trustlines.");
1826
+ trustline
1827
+ .command("add")
1828
+ .description("Create a Testnet trustline for a liquidity pool share asset.")
1829
+ .option("--pool <poolId>", "Existing liquidity pool id")
1830
+ .option("--asset-a <asset>", "First reserve asset when deriving a pool id")
1831
+ .option("--asset-b <asset>", "Second reserve asset when deriving a pool id")
1832
+ .option("--account <account>", "Local Testnet wallet name", "agent")
1833
+ .option("--limit <amount>", "Pool share trustline limit")
1834
+ .action(withContext(async (context, options) => {
1835
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
1836
+ if (profile.realFunds) {
1837
+ throw new StellarAgentError({
1838
+ code: "MAINNET_NOT_ENABLED",
1839
+ message: "Mainnet liquidity pool trustline creation requires an external signer.",
1840
+ docs: "docs/mainnet-safety.md#mainnet-liquidity"
1841
+ });
1842
+ }
1843
+ const wallet = await loadWallet(context.config, options.account);
1844
+ const { changeLiquidityPoolTrustline } = await import("@stellar-agent/stellar");
1845
+ const transaction = await changeLiquidityPoolTrustline({
1846
+ source: wallet,
1847
+ ...(options.pool === undefined ? {} : { poolId: options.pool }),
1848
+ ...(options.assetA === undefined ? {} : { assetA: options.assetA }),
1849
+ ...(options.assetB === undefined ? {} : { assetB: options.assetB }),
1850
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
1851
+ profile
1852
+ });
1853
+ const receiptPath = await writeOperationReceipt(context, {
1854
+ command: "market lp trustline add",
1855
+ operation: {
1856
+ type: "market.lp.trustline.add",
1857
+ account: wallet.publicKey,
1858
+ details: { liquidityPool: { poolId: transaction.poolId, limit: transaction.limit } }
1859
+ },
1860
+ transaction
1861
+ });
1862
+ return { status: "trustline_added", poolId: transaction.poolId, transaction, receiptPath };
1863
+ }, "Liquidity pool trustline added."));
1864
+ const listen = market.command("listen").description("Evaluate one or more market alert checks and return JSON events.");
1865
+ listen
1866
+ .command("price")
1867
+ .description("Alert when a core pool reserve price crosses a threshold.")
1868
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1869
+ .option("--above <price>", "Trigger when reserve B per reserve A is above this price")
1870
+ .option("--below <price>", "Trigger when reserve B per reserve A is below this price")
1871
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1872
+ .option("--polls <n>", "Number of checks", parsePositiveIntegerOption, 1)
1873
+ .option("--interval-ms <n>", "Delay between checks", parsePositiveIntegerOption, 1000)
1874
+ .action(withContext(async (context, options) => {
1875
+ return listenForPoolPrice(context, options);
1876
+ }, "Market price listener evaluated."));
1877
+ listen
1878
+ .command("position")
1879
+ .description("Alert when a pool-share position crosses a local threshold.")
1880
+ .requiredOption("--pool <poolId>", "Liquidity pool id")
1881
+ .option("--account <account>", "Local wallet name or public key", "agent")
1882
+ .option("--shares-below <amount>", "Trigger when pool shares are below this amount")
1883
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1884
+ .action(withContext(async (context, options) => {
1885
+ const { inspectLiquidityPoolPosition } = await import("@stellar-agent/stellar");
1886
+ const account = await resolvePublicAccount(context, options.account);
1887
+ const position = await inspectLiquidityPoolPosition({
1888
+ account,
1889
+ poolId: options.pool,
1890
+ profile: resolveMarketProfile(context, options.network)
1891
+ });
1892
+ const shares = position.positions[0]?.shares ?? "0.0000000";
1893
+ const sharesBelow = options.sharesBelow === undefined ? undefined : parseNonnegativeDecimalInput(options.sharesBelow, "shares-below");
1894
+ const triggered = sharesBelow === undefined ? false : Number(shares) < sharesBelow;
1895
+ return {
1896
+ type: "market.alert",
1897
+ rule: "lp-position-shares-below",
1898
+ status: triggered ? "triggered" : "not_triggered",
1899
+ observed: shares,
1900
+ threshold: options.sharesBelow ?? null,
1901
+ position
1902
+ };
1903
+ }, "Market position listener evaluated."));
1904
+ const soroban = market.command("soroban").description("Inspect Soroban AMM contracts without submitting liquidity actions.");
1905
+ const sorobanPool = soroban.command("pool").description("Read-only Soroban pool investigation.");
1906
+ sorobanPool
1907
+ .command("inspect")
1908
+ .description("Inspect Soroban contract interface metadata for a potential AMM pool.")
1909
+ .requiredOption("--id <contractId>", "Soroban contract id")
1910
+ .option("--network <network>", "Stellar CLI network", "testnet")
1911
+ .option("--stellar-binary <path>", "Path to stellar CLI binary")
1912
+ .option("--stellar-config-dir <path>", "Stellar CLI config directory")
1913
+ .option("--stellar-no-cache", "Pass --no-cache to stellar CLI")
1914
+ .action(withContext(async (context, options) => {
1915
+ const profile = resolveContractExecutionContext(context, options.network, {
1916
+ mutating: false
1917
+ });
1918
+ const { contractInfoWithStellarCli } = await import("@stellar-agent/stellar");
1919
+ const result = await contractInfoWithStellarCli({
1920
+ kind: "interface",
1921
+ contractId: options.id,
1922
+ network: options.network,
1923
+ rpcUrl: profile.rpcUrl,
1924
+ networkPassphrase: profile.networkPassphrase,
1925
+ ...(options.stellarBinary === undefined ? {} : { stellarBinary: options.stellarBinary }),
1926
+ ...(options.stellarConfigDir === undefined ? {} : { stellarConfigDir: options.stellarConfigDir }),
1927
+ ...(options.stellarNoCache === undefined ? {} : { noCache: options.stellarNoCache })
1928
+ });
1929
+ return {
1930
+ contractId: options.id,
1931
+ boundary: "read_only",
1932
+ mutationSupported: false,
1933
+ result
1934
+ };
1935
+ }, "Soroban pool inspected."));
1936
+ sorobanPool
1937
+ .command("preflight")
1938
+ .description("Explain why Soroban AMM mutation requires a protocol-specific adapter before submission.")
1939
+ .requiredOption("--id <contractId>", "Soroban contract id")
1940
+ .requiredOption("--action <action>", "deposit or withdraw")
1941
+ .action(withContext(async (_context, options) => {
1942
+ if (options.action !== "deposit" && options.action !== "withdraw") {
1943
+ throw new StellarAgentError({
1944
+ code: "INVALID_INPUT",
1945
+ message: "Soroban pool action must be deposit or withdraw.",
1946
+ docs: "docs/market-liquidity.md#soroban-pool-boundary"
1947
+ });
1948
+ }
1949
+ return {
1950
+ contractId: options.id,
1951
+ action: options.action,
1952
+ status: "adapter_required",
1953
+ mutationSupported: false,
1954
+ requirements: [
1955
+ "Use a protocol-specific adapter with documented contract interfaces.",
1956
+ "Add policy controls before mutation.",
1957
+ "Require simulation, external Mainnet signing, and receipts for submitted transactions."
1958
+ ]
1959
+ };
1960
+ }, "Soroban pool preflight boundary explained."));
1961
+ }
1962
+ function addStrategyCommands(program) {
1963
+ const strategy = program.command("strategy").description("Explain and simulate market-aware strategy proposals without hidden signing.");
1964
+ strategy
1965
+ .command("explain")
1966
+ .description("Validate and explain a local strategy file.")
1967
+ .argument("<file>", "Strategy JSON or YAML file")
1968
+ .action(withContext(async (_context, file) => {
1969
+ const parsed = await readStrategyFile(file);
1970
+ return explainStrategy(parsed);
1971
+ }, "Strategy explained."));
1972
+ strategy
1973
+ .command("simulate")
1974
+ .description("Simulate a local strategy file without submitting transactions.")
1975
+ .argument("<file>", "Strategy JSON or YAML file")
1976
+ .action(withContext(async (_context, file) => {
1977
+ const parsed = await readStrategyFile(file);
1978
+ return { ...explainStrategy(parsed), simulation: { submitted: false, signing: false, mode: "dry_run" } };
1979
+ }, "Strategy simulation complete."));
1980
+ const investigate = strategy.command("investigate").description("Investigate market options without execution.");
1981
+ investigate
1982
+ .command("liquidity")
1983
+ .description("Compare liquidity-pool context for an asset pair or explicit pool.")
1984
+ .option("--pair <assetA/assetB>", "Asset pair, such as XLM/USD:G...")
1985
+ .option("--pool <poolId>", "Specific liquidity pool id")
1986
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
1987
+ .option("--limit <n>", "Number of records", parseIntegerOption, 5)
1988
+ .action(withContext(async (context, options) => {
1989
+ const { inspectLiquidityPool, liquidityPoolTrades, listLiquidityPools } = await import("@stellar-agent/stellar");
1990
+ const profile = resolveMarketProfile(context, options.network);
1991
+ if (options.pool) {
1992
+ const pool = await inspectLiquidityPool({ poolId: options.pool, profile });
1993
+ const trades = await liquidityPoolTrades({ poolId: options.pool, profile, limit: options.limit });
1994
+ return liquidityInvestigation({ pool, trades: trades.records, profile });
1995
+ }
1996
+ if (!options.pair) {
1997
+ throw new StellarAgentError({
1998
+ code: "INVALID_INPUT",
1999
+ message: "Provide --pool or --pair for liquidity investigation.",
2000
+ docs: "docs/market-liquidity.md#strategy-investigation"
2001
+ });
2002
+ }
2003
+ const [assetA, assetB] = parsePairOption(options.pair);
2004
+ const pools = await listLiquidityPools({ assetA, assetB, profile, limit: options.limit });
2005
+ return {
2006
+ pair: { assetA, assetB },
2007
+ pools: pools.records.map((pool) => liquidityInvestigation({ pool, trades: [], profile }))
2008
+ };
2009
+ }, "Liquidity strategy investigation complete."));
2010
+ }
2011
+ function addDefiCommands(program) {
2012
+ const defi = program.command("defi").description("Inspect and preflight guarded DeFi workflows.");
2013
+ const blend = defi.command("blend").description("Inspect Blend pools and preflight Blend pool requests.");
2014
+ blend
2015
+ .command("deployments")
2016
+ .description("Show known Blend deployment contracts, assets, pools, and canonical trustline issuers.")
2017
+ .option("--network <name>", "Blend deployment network: testnet or mainnet")
2018
+ .option("--refresh", "Fetch current Blend deployment maps from blend-utils and blend-ui")
2019
+ .action(withContext(async (context, options) => {
2020
+ const { blendDeployment, fetchBlendDeployment } = await loadDefi();
2021
+ const network = resolveBlendNetworkOption(context, options.network);
2022
+ return options.refresh ? await fetchBlendDeployment(network) : blendDeployment(network);
2023
+ }, "Blend deployments loaded."));
2024
+ const pool = blend.command("pool").description("Inspect Blend pools.");
2025
+ pool
2026
+ .command("inspect")
2027
+ .description("Load a Blend pool, reserves, APYs, and estimated aggregate pool values.")
2028
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2029
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
2030
+ .option("--version <version>", "Pool version: v1 or v2")
2031
+ .action(withContext(async (context, options) => {
2032
+ const { blendDeployment, inspectBlendPool, resolveBlendPool } = await loadDefi();
2033
+ const profile = resolveBlendProfile(context, options.network);
2034
+ const deployment = blendDeployment(blendNetworkForProfile(profile));
2035
+ const poolDeployment = resolveBlendPool(deployment, options.pool);
2036
+ const poolVersion = resolveBlendPoolVersion(options.version ?? poolDeployment.version);
2037
+ return inspectBlendPool({
2038
+ profile,
2039
+ poolId: poolDeployment.contractId,
2040
+ ...(poolVersion === undefined ? {} : { poolVersion })
2041
+ });
2042
+ }, "Blend pool inspected."));
2043
+ const position = blend.command("position").description("Inspect Blend user positions.");
2044
+ position
2045
+ .command("inspect")
2046
+ .description("Load a user's Blend supply, collateral, liabilities, health factor, and APYs.")
2047
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2048
+ .option("--account <account>", "Local wallet name or public key", "agent")
2049
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
2050
+ .option("--version <version>", "Pool version: v1 or v2")
2051
+ .action(withContext(async (context, options) => {
2052
+ const { blendDeployment, inspectBlendPosition, resolveBlendPool } = await loadDefi();
2053
+ const profile = resolveBlendProfile(context, options.network);
2054
+ const deployment = blendDeployment(blendNetworkForProfile(profile));
2055
+ const poolDeployment = resolveBlendPool(deployment, options.pool);
2056
+ const account = await resolvePublicAccount(context, options.account);
2057
+ const poolVersion = resolveBlendPoolVersion(options.version ?? poolDeployment.version);
2058
+ return inspectBlendPosition({
2059
+ profile,
2060
+ poolId: poolDeployment.contractId,
2061
+ userId: account,
2062
+ ...(poolVersion === undefined ? {} : { poolVersion })
2063
+ });
2064
+ }, "Blend position inspected."));
2065
+ blend
2066
+ .command("preflight")
2067
+ .description("Preflight Blend requests with decoded request types, expected b/d tokens, position estimates, and policy.")
2068
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2069
+ .option("--account <account>", "Local wallet name or public key", "agent")
2070
+ .requiredOption("--request <type:asset:amount...>", "Blend request, repeatable", collectOption, [])
2071
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
2072
+ .option("--version <version>", "Pool version: v1 or v2")
2073
+ .action(withContext(async (context, options) => {
2074
+ const { blendDeployment, parseBlendRequest, preflightBlendActions, resolveBlendAsset, resolveBlendPool } = await loadDefi();
2075
+ const profile = resolveBlendProfile(context, options.network);
2076
+ const deployment = blendDeployment(blendNetworkForProfile(profile));
2077
+ const poolDeployment = resolveBlendPool(deployment, options.pool);
2078
+ const account = await resolvePublicAccount(context, options.account);
2079
+ const actions = options.request.map((entry) => {
2080
+ const parsed = parseBlendRequest(entry);
2081
+ const asset = resolveBlendAsset(deployment, parsed.asset);
2082
+ return { ...parsed, asset: asset.contractId };
2083
+ });
2084
+ const poolVersion = resolveBlendPoolVersion(options.version ?? poolDeployment.version);
2085
+ const preflight = await preflightBlendActions({
2086
+ profile,
2087
+ poolId: poolDeployment.contractId,
2088
+ userId: account,
2089
+ actions,
2090
+ ...(poolVersion === undefined ? {} : { poolVersion })
2091
+ });
2092
+ const policy = await loadPolicy(context);
2093
+ const borrowValue = preflight.actions
2094
+ .filter((action) => action.type === "borrow")
2095
+ .reduce((total, action) => total + (action.value ?? 0), 0);
2096
+ const protocolExposureValue = (preflight.after?.totalSupplied ?? preflight.before?.totalSupplied ?? 0) +
2097
+ (preflight.after?.totalBorrowed ?? preflight.before?.totalBorrowed ?? 0);
2098
+ const policyDecision = evaluateDefiBlendRequest(policy, {
2099
+ network: profile.realFunds ? "mainnet" : "testnet",
2100
+ pool: poolDeployment.contractId,
2101
+ requestTypes: preflight.actions.map((action) => action.type),
2102
+ borrowValue: String(borrowValue),
2103
+ protocolExposureValue: String(protocolExposureValue),
2104
+ ...(preflight.after?.healthFactor === undefined ? {} : { healthFactorAfter: preflight.after.healthFactor })
2105
+ });
2106
+ return {
2107
+ deployment: {
2108
+ network: deployment.network,
2109
+ pool: poolDeployment
2110
+ },
2111
+ policyDecision,
2112
+ preflight
2113
+ };
2114
+ }, "Blend preflight complete."));
2115
+ blend
2116
+ .command("supply")
2117
+ .description("Submit a guarded Testnet Blend supply request.")
2118
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2119
+ .requiredOption("--asset <asset>", "Blend asset symbol or contract id")
2120
+ .requiredOption("--amount <amount>", "Asset amount")
2121
+ .option("--source <account>", "Local Testnet wallet name", "agent")
2122
+ .option("--collateral", "Supply as collateral")
2123
+ .action(withContext(async (context, options) => {
2124
+ return runBlendSubmitCommand(context, {
2125
+ command: "defi blend supply",
2126
+ pool: options.pool,
2127
+ source: options.source,
2128
+ actions: [
2129
+ {
2130
+ type: options.collateral ? "supply_collateral" : "supply",
2131
+ asset: options.asset,
2132
+ amount: options.amount
2133
+ }
2134
+ ]
2135
+ });
2136
+ }, "Blend supply submitted."));
2137
+ blend
2138
+ .command("borrow")
2139
+ .description("Submit a guarded Testnet Blend borrow request.")
2140
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2141
+ .requiredOption("--asset <asset>", "Blend asset symbol or contract id")
2142
+ .requiredOption("--amount <amount>", "Asset amount")
2143
+ .option("--source <account>", "Local Testnet wallet name", "agent")
2144
+ .action(withContext(async (context, options) => {
2145
+ return runBlendSubmitCommand(context, {
2146
+ command: "defi blend borrow",
2147
+ pool: options.pool,
2148
+ source: options.source,
2149
+ actions: [{ type: "borrow", asset: options.asset, amount: options.amount }]
2150
+ });
2151
+ }, "Blend borrow submitted."));
2152
+ blend
2153
+ .command("repay")
2154
+ .description("Submit a guarded Testnet Blend repay request.")
2155
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2156
+ .requiredOption("--asset <asset>", "Blend asset symbol or contract id")
2157
+ .requiredOption("--amount <amount>", "Asset amount")
2158
+ .option("--source <account>", "Local Testnet wallet name", "agent")
2159
+ .action(withContext(async (context, options) => {
2160
+ return runBlendSubmitCommand(context, {
2161
+ command: "defi blend repay",
2162
+ pool: options.pool,
2163
+ source: options.source,
2164
+ actions: [{ type: "repay", asset: options.asset, amount: options.amount }]
2165
+ });
2166
+ }, "Blend repay submitted."));
2167
+ blend
2168
+ .command("withdraw")
2169
+ .description("Submit a guarded Testnet Blend withdraw request.")
2170
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2171
+ .requiredOption("--asset <asset>", "Blend asset symbol or contract id")
2172
+ .requiredOption("--amount <amount>", "Asset amount")
2173
+ .option("--source <account>", "Local Testnet wallet name", "agent")
2174
+ .option("--collateral", "Withdraw from collateral")
2175
+ .action(withContext(async (context, options) => {
2176
+ return runBlendSubmitCommand(context, {
2177
+ command: "defi blend withdraw",
2178
+ pool: options.pool,
2179
+ source: options.source,
2180
+ actions: [
2181
+ {
2182
+ type: options.collateral ? "withdraw_collateral" : "withdraw",
2183
+ asset: options.asset,
2184
+ amount: options.amount
2185
+ }
2186
+ ]
2187
+ });
2188
+ }, "Blend withdraw submitted."));
2189
+ blend
2190
+ .command("batch")
2191
+ .description("Submit a guarded Testnet batch of Blend requests in one transaction.")
2192
+ .requiredOption("--pool <pool>", "Pool contract id or deployment alias")
2193
+ .option("--source <account>", "Local Testnet wallet name", "agent")
2194
+ .requiredOption("--request <type:asset:amount...>", "Blend request, repeatable", collectOption, [])
2195
+ .action(withContext(async (context, options) => {
2196
+ const { parseBlendRequest } = await loadDefi();
2197
+ return runBlendSubmitCommand(context, {
2198
+ command: "defi blend batch",
2199
+ pool: options.pool,
2200
+ source: options.source,
2201
+ actions: options.request.map(parseBlendRequest)
2202
+ });
2203
+ }, "Blend batch submitted."));
2204
+ const trustline = blend.command("trustline").description("Resolve and create trustlines for Blend non-native reserves.");
2205
+ trustline
2206
+ .command("guide")
2207
+ .description("Show the classic asset backing a Blend reserve and the trustline command to run.")
2208
+ .requiredOption("--asset <asset>", "Blend asset symbol or contract id")
2209
+ .option("--account <account>", "Local wallet name", "agent")
2210
+ .option("--network <name>", "Network profile to inspect: testnet or mainnet")
2211
+ .action(withContext(async (context, options) => {
2212
+ return blendTrustlineGuide(context, options);
2213
+ }, "Blend trustline guidance loaded."));
2214
+ trustline
2215
+ .command("add")
2216
+ .description("Create the classic issued-asset trustline for a Blend reserve on Testnet.")
2217
+ .requiredOption("--asset <asset>", "Blend asset symbol or contract id")
2218
+ .option("--account <account>", "Local Testnet wallet name", "agent")
2219
+ .option("--limit <amount>", "Trustline limit")
2220
+ .action(withContext(async (context, options) => {
2221
+ return addBlendTrustline(context, options);
2222
+ }, "Blend trustline added."));
2223
+ }
2224
+ function addPolicyCommands(program) {
2225
+ const policy = program.command("policy").description("Manage and evaluate spend policies.");
2226
+ policy
2227
+ .command("init")
2228
+ .description("Write a safe default policy.")
2229
+ .option("--network <network>", "Policy network", "testnet")
2230
+ .option("--output <path>", "Output path")
2231
+ .action(withContext(async (context, options) => {
2232
+ const target = defaultPolicyForNetwork(options.network);
2233
+ const path = options.output ??
2234
+ join(context.config.storage.policiesDir, options.network === "mainnet" ? "default-mainnet.yaml" : "default-testnet.yaml");
2235
+ await writeFile(resolvePath(path), policyToYaml(target), { mode: 0o600 });
2236
+ return { path: resolvePath(path), policy: target };
2237
+ }, "Policy initialized."));
2238
+ policy
2239
+ .command("check")
2240
+ .description("Validate a policy YAML file.")
2241
+ .argument("[path]", "Policy path")
2242
+ .action(withContext(async (context, path) => {
2243
+ const policy = await loadPolicy(context, path);
2244
+ return { valid: true, name: policy.name, network: policy.network };
2245
+ }, "Policy valid."));
2246
+ policy
2247
+ .command("explain")
2248
+ .description("Explain a payment request against a policy.")
2249
+ .option("--request <path>", "JSON payment request file")
2250
+ .option("--to <address>", "Destination public key")
2251
+ .option("--amount <amount>", "Payment amount")
2252
+ .option("--asset <asset>", "Asset", "XLM")
2253
+ .option("--memo <memo>", "Memo")
2254
+ .action(withContext(async (context, options) => {
2255
+ const policy = await loadPolicy(context);
2256
+ const raw = options.request
2257
+ ? JSON.parse(await readFile(resolvePath(options.request), "utf8"))
2258
+ : { destination: options.to, amount: options.amount, asset: options.asset, memo: options.memo, network: context.profileName };
2259
+ const request = paymentRequestSchema.parse(raw);
2260
+ const history = await loadSpendHistory(context, request);
2261
+ return { ...evaluatePaymentRequest(policy, request, history), spendHistory: history };
2262
+ }, "Policy explanation complete."));
2263
+ policy
2264
+ .command("test")
2265
+ .description("Run bundled policy fixture checks.")
2266
+ .action(withContext(async () => {
2267
+ const fixtures = [
2268
+ evaluatePaymentRequest(DEFAULT_TESTNET_POLICY, fixtureRequest("1")),
2269
+ evaluatePaymentRequest(DEFAULT_TESTNET_POLICY, fixtureRequest("11")),
2270
+ evaluatePaymentRequest(DEFAULT_TESTNET_POLICY, fixtureRequest("6")),
2271
+ evaluatePaymentRequest(DEFAULT_MAINNET_POLICY, { ...fixtureRequest("0.01"), network: "mainnet" })
2272
+ ];
2273
+ return { passed: fixtures.length, decisions: fixtures.map((fixture) => fixture.status) };
2274
+ }, "Policy fixtures passed."));
2275
+ }
2276
+ function addLedgerCommands(program) {
2277
+ const ledger = program.command("ledger").description("Inspect Stellar ledger data.");
2278
+ ledger
2279
+ .command("latest")
2280
+ .description("Fetch the latest ledger from Horizon.")
2281
+ .action(withContext(async (context) => latestLedger(resolveNetworkProfile(context.profileName, context.config.profiles)), "Latest ledger loaded."));
2282
+ ledger
2283
+ .command("tx")
2284
+ .description("Fetch a transaction by hash.")
2285
+ .argument("<hash>", "Transaction hash")
2286
+ .action(withContext(async (context, hash) => lookupTransaction(hash, resolveNetworkProfile(context.profileName, context.config.profiles)), "Transaction loaded."));
2287
+ ledger
2288
+ .command("account")
2289
+ .description("Show local account balances.")
2290
+ .argument("[name]", "Wallet name", "agent")
2291
+ .action(withContext(async (context, name) => ({
2292
+ account: name,
2293
+ balances: await walletBalances(context.config, name)
2294
+ }), "Account ledger data loaded."));
2295
+ ledger
2296
+ .command("payments")
2297
+ .description("Fetch recent payment operations for an account from Horizon.")
2298
+ .option("--account <name>", "Local wallet name", "agent")
2299
+ .option("--address <address>", "Raw Stellar public key")
2300
+ .option("--limit <n>", "Number of records", parseIntegerOption, 10)
2301
+ .action(withContext(async (context, options) => {
2302
+ const { accountPayments } = await import("@stellar-agent/stellar");
2303
+ const address = options.address ?? (await loadWallet(context.config, options.account)).publicKey;
2304
+ return {
2305
+ address,
2306
+ ...(await accountPayments({
2307
+ address,
2308
+ profile: resolveNetworkProfile(context.profileName, context.config.profiles),
2309
+ limit: options.limit
2310
+ }))
2311
+ };
2312
+ }, "Ledger payments loaded."));
2313
+ ledger
2314
+ .command("effects")
2315
+ .description("Fetch recent effects for an account or transaction from Horizon.")
2316
+ .option("--account <name>", "Local wallet name", "agent")
2317
+ .option("--address <address>", "Raw Stellar public key")
2318
+ .option("--tx <hash>", "Transaction hash")
2319
+ .option("--limit <n>", "Number of records", parseIntegerOption, 10)
2320
+ .action(withContext(async (context, options) => {
2321
+ const { accountEffects, transactionEffects } = await import("@stellar-agent/stellar");
2322
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
2323
+ if (options.tx) {
2324
+ return {
2325
+ transaction: options.tx,
2326
+ ...(await transactionEffects({ hash: options.tx, profile, limit: options.limit }))
2327
+ };
2328
+ }
2329
+ const address = options.address ?? (await loadWallet(context.config, options.account)).publicKey;
2330
+ return {
2331
+ address,
2332
+ ...(await accountEffects({ address, profile, limit: options.limit }))
2333
+ };
2334
+ }, "Ledger effects loaded."));
2335
+ ledger
2336
+ .command("export")
2337
+ .description("Export local receipt and event-log metadata as a JSON report.")
2338
+ .option("--output <path>", "Output JSON path")
2339
+ .action(withContext(async (context, options) => {
2340
+ return writeLocalReport(context, options.output ?? join(context.config.storage.ledgersDir, "ledger-report.json"));
2341
+ }, "Ledger report exported."));
2342
+ }
2343
+ function addReceiptCommands(program) {
2344
+ const receipts = program.command("receipts").description("List, show, verify, and export receipts.");
2345
+ receipts
2346
+ .command("list")
2347
+ .description("List local receipts.")
2348
+ .action(withContext(async (context) => ({ receipts: await listReceipts(context.config.storage.receiptsDir) }), "Receipts listed."));
2349
+ receipts
2350
+ .command("latest")
2351
+ .description("Show the latest local receipt.")
2352
+ .action(withContext(async (context) => {
2353
+ const latest = await latestReceipt(context.config.storage.receiptsDir);
2354
+ if (!latest)
2355
+ return { receipt: null };
2356
+ return latest;
2357
+ }, "Latest receipt loaded."));
2358
+ receipts
2359
+ .command("show")
2360
+ .description("Show a receipt file.")
2361
+ .argument("<path>", "Receipt path")
2362
+ .action(withContext(async (_context, path) => readReceipt(path), "Receipt loaded."));
2363
+ receipts
2364
+ .command("verify")
2365
+ .description("Verify a receipt file.")
2366
+ .argument("<path>", "Receipt path")
2367
+ .option("--ledger", "Also verify the receipt transaction against Horizon")
2368
+ .action(withContext(async (context, path, options) => {
2369
+ const receipt = await readReceipt(path);
2370
+ verifyReceipt(receipt);
2371
+ const ledger = options.ledger ? await verifyReceiptAgainstLedger(context, receipt) : undefined;
2372
+ return { valid: true, path: resolvePath(path), ...(ledger === undefined ? {} : { ledger }) };
2373
+ }, "Receipt valid."));
2374
+ receipts
2375
+ .command("export")
2376
+ .description("Export receipt paths.")
2377
+ .action(withContext(async (context) => ({ receipts: await listReceipts(context.config.storage.receiptsDir) }), "Receipts exported."));
2378
+ }
2379
+ function addMainnetCommands(program) {
2380
+ const mainnet = program.command("mainnet").description("Guarded Mainnet readiness and enablement.");
2381
+ mainnet
2382
+ .command("status")
2383
+ .description("Show Mainnet guard status.")
2384
+ .action(withContext(async (context) => ({
2385
+ realFunds: true,
2386
+ enabled: Boolean(context.config.profiles.mainnet?.enabled),
2387
+ requiresExplicitApproval: true
2388
+ }), "Mainnet status loaded."));
2389
+ mainnet
2390
+ .command("enable")
2391
+ .description("Enable guarded Mainnet profile metadata.")
2392
+ .option("--i-understand-real-funds", "Required acknowledgement")
2393
+ .action(withContext(async (context, options) => {
2394
+ if (!options.iUnderstandRealFunds) {
2395
+ throw new StellarAgentError({
2396
+ code: "MAINNET_NOT_ENABLED",
2397
+ message: "Enabling Mainnet requires --i-understand-real-funds.",
2398
+ docs: "docs/mainnet-safety.md"
2399
+ });
2400
+ }
2401
+ const mainnet = context.config.profiles.mainnet;
2402
+ if (mainnet)
2403
+ mainnet.enabled = true;
2404
+ await writeConfig(context.config, context.options);
2405
+ return { enabled: true, realFunds: true, autoApproval: false };
2406
+ }, "Mainnet enabled with guarded settings."));
2407
+ mainnet
2408
+ .command("disable")
2409
+ .description("Disable Mainnet profile metadata.")
2410
+ .action(withContext(async (context) => {
2411
+ const mainnet = context.config.profiles.mainnet;
2412
+ if (mainnet)
2413
+ mainnet.enabled = false;
2414
+ await writeConfig(context.config, context.options);
2415
+ return { enabled: false, realFunds: true };
2416
+ }, "Mainnet disabled."));
2417
+ mainnet
2418
+ .command("readiness")
2419
+ .description("Show a Mainnet readiness checklist.")
2420
+ .action(withContext(async (context) => ({
2421
+ realFunds: true,
2422
+ enabled: Boolean(context.config.profiles.mainnet?.enabled),
2423
+ checklist: [
2424
+ "Freighter or another human approval flow is required.",
2425
+ "Mainnet auto-approval is disabled.",
2426
+ "Policy must require explicit approval.",
2427
+ "Receipts must mark realFunds: true."
2428
+ ]
2429
+ }), "Mainnet readiness checked."));
2430
+ }
2431
+ function withContext(handler, humanMessage) {
2432
+ return async (...args) => {
2433
+ const command = args.at(-1);
2434
+ const options = command.optsWithGlobals();
2435
+ try {
2436
+ const config = await loadConfig(options);
2437
+ const profileName = (options.profile ??
2438
+ process.env.STELLAR_AGENT_PROFILE ??
2439
+ config.activeProfile ??
2440
+ "testnet");
2441
+ const context = { options, config, profileName };
2442
+ const data = await handler(context, ...args.slice(0, -1));
2443
+ printSuccess(options, data, humanMessage);
2444
+ }
2445
+ catch (error) {
2446
+ printError(options, error);
2447
+ }
2448
+ };
2449
+ }
2450
+ async function loadConfig(options) {
2451
+ const configPath = options.config ?? process.env.STELLAR_AGENT_CONFIG;
2452
+ const resolvedConfigPath = configPath ? resolvePath(configPath) : undefined;
2453
+ const defaultConfig = createDefaultConfig(resolvedConfigPath ? dirname(resolvedConfigPath) : undefined);
2454
+ if (!configPath)
2455
+ return configSchema.parse(defaultConfig);
2456
+ try {
2457
+ const raw = await readFile(resolvedConfigPath, "utf8");
2458
+ const parsed = parseYaml(raw);
2459
+ return configSchema.parse(deepMerge(defaultConfig, parsed));
2460
+ }
2461
+ catch (error) {
2462
+ if (error?.code === "ENOENT") {
2463
+ return configSchema.parse(defaultConfig);
2464
+ }
2465
+ throw new StellarAgentError({
2466
+ code: "CONFIG_INVALID",
2467
+ message: "Config file is invalid.",
2468
+ details: error,
2469
+ docs: "docs/troubleshooting.md#invalid-wallet"
2470
+ });
2471
+ }
2472
+ }
2473
+ async function writeConfig(config, options) {
2474
+ const path = resolvePath(options.config ?? process.env.STELLAR_AGENT_CONFIG ?? join(config.storage.rootDir, "config.yaml"));
2475
+ await import("node:fs/promises").then(({ mkdir }) => mkdir(dirname(path), { recursive: true }));
2476
+ await writeFile(path, stringifyYaml(config), { mode: 0o600 });
2477
+ }
2478
+ async function createTestnetWalletResult(context, name, fund) {
2479
+ const wallet = await ensureWallet(context.config, name);
2480
+ if (!fund)
2481
+ return { wallet };
2482
+ const { fundWithFriendbot } = await import("@stellar-agent/stellar");
2483
+ const funding = await fundWithFriendbot(wallet.publicKey, context.config.profiles.testnet);
2484
+ await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
2485
+ event: "command_finished",
2486
+ status: "funded",
2487
+ command: "wallet create-testnet",
2488
+ profile: "testnet",
2489
+ data: { account: name, publicKey: wallet.publicKey, funding }
2490
+ });
2491
+ return { wallet, funding };
2492
+ }
2493
+ async function loadPolicy(context, explicitPath) {
2494
+ const policyPath = explicitPath ??
2495
+ context.options.policy ??
2496
+ process.env.STELLAR_AGENT_POLICY ??
2497
+ join(context.config.storage.policiesDir, context.profileName === "mainnet" ? "default-mainnet.yaml" : "default-testnet.yaml");
2498
+ try {
2499
+ return parsePolicyYaml(await readFile(resolvePath(policyPath), "utf8"));
2500
+ }
2501
+ catch (error) {
2502
+ if (error?.code === "ENOENT")
2503
+ return defaultPolicyForNetwork(context.profileName);
2504
+ throw error;
2505
+ }
2506
+ }
2507
+ function resolveBlendNetworkOption(context, network) {
2508
+ if (!network)
2509
+ return blendNetworkForProfile(resolveNetworkProfile(context.profileName, context.config.profiles));
2510
+ const normalized = network.toLowerCase();
2511
+ if (normalized === "testnet" || normalized === "mainnet")
2512
+ return normalized;
2513
+ throw new StellarAgentError({
2514
+ code: "INVALID_INPUT",
2515
+ message: "Blend network must be testnet or mainnet.",
2516
+ docs: "docs/defi-blend.md"
2517
+ });
2518
+ }
2519
+ async function loadDefi() {
2520
+ return await import("@stellar-agent/defi");
2521
+ }
2522
+ function blendNetworkForProfile(profile) {
2523
+ return profile.realFunds || profile.name === "mainnet" ? "mainnet" : "testnet";
2524
+ }
2525
+ function resolveBlendProfile(context, network) {
2526
+ return resolveNetworkProfile(resolveBlendNetworkOption(context, network), context.config.profiles);
2527
+ }
2528
+ function resolveBlendPoolVersion(version) {
2529
+ if (version === undefined)
2530
+ return undefined;
2531
+ const normalized = version.toLowerCase();
2532
+ if (normalized === "v1" || normalized === "v2")
2533
+ return normalized;
2534
+ throw new StellarAgentError({
2535
+ code: "INVALID_INPUT",
2536
+ message: "Blend pool version must be v1 or v2.",
2537
+ docs: "docs/defi-blend.md"
2538
+ });
2539
+ }
2540
+ async function resolvePublicAccount(context, account) {
2541
+ if (/^G[A-Z2-7]{55}$/.test(account))
2542
+ return account;
2543
+ return (await loadWalletPublic(context.config, account)).publicKey;
2544
+ }
2545
+ function resolveMarketProfile(context, network) {
2546
+ if (!network)
2547
+ return resolveNetworkProfile(context.profileName, context.config.profiles);
2548
+ const normalized = network.toLowerCase();
2549
+ if (normalized !== "testnet" && normalized !== "mainnet") {
2550
+ throw new StellarAgentError({
2551
+ code: "INVALID_INPUT",
2552
+ message: "Market network must be testnet or mainnet.",
2553
+ docs: "docs/market-liquidity.md"
2554
+ });
2555
+ }
2556
+ return resolveNetworkProfile(normalized, context.config.profiles);
2557
+ }
2558
+ async function runLiquidityPreflight(context, profile, options) {
2559
+ const action = parseLiquidityAction(options.action);
2560
+ const account = await resolvePublicAccount(context, options.account);
2561
+ const { preflightLiquidityPoolDeposit, preflightLiquidityPoolWithdraw } = await import("@stellar-agent/stellar");
2562
+ if (action === "deposit") {
2563
+ if (!options.maxA || !options.maxB || !options.minPrice || !options.maxPrice) {
2564
+ throw new StellarAgentError({
2565
+ code: "INVALID_INPUT",
2566
+ message: "Liquidity deposit preflight requires --max-a, --max-b, --min-price, and --max-price.",
2567
+ docs: "docs/market-liquidity.md#lp-preflight"
2568
+ });
2569
+ }
2570
+ validatePriceBounds(options.minPrice, options.maxPrice);
2571
+ return preflightLiquidityPoolDeposit({
2572
+ poolId: options.pool,
2573
+ maxAmountA: options.maxA,
2574
+ maxAmountB: options.maxB,
2575
+ minPrice: options.minPrice,
2576
+ maxPrice: options.maxPrice,
2577
+ account,
2578
+ profile
2579
+ });
2580
+ }
2581
+ if (!options.shares) {
2582
+ throw new StellarAgentError({
2583
+ code: "INVALID_INPUT",
2584
+ message: "Liquidity withdrawal preflight requires --shares.",
2585
+ docs: "docs/market-liquidity.md#lp-preflight"
2586
+ });
2587
+ }
2588
+ return preflightLiquidityPoolWithdraw({
2589
+ poolId: options.pool,
2590
+ shares: options.shares,
2591
+ minAmountA: options.minA,
2592
+ minAmountB: options.minB,
2593
+ account,
2594
+ profile
2595
+ });
2596
+ }
2597
+ function parseLiquidityAction(action) {
2598
+ if (action === "deposit" || action === "withdraw")
2599
+ return action;
2600
+ throw new StellarAgentError({
2601
+ code: "INVALID_INPUT",
2602
+ message: "Liquidity action must be deposit or withdraw.",
2603
+ docs: "docs/market-liquidity.md#lp-preflight"
2604
+ });
2605
+ }
2606
+ async function runLiquiditySubmitCommand(context, args) {
2607
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
2608
+ if (profile.realFunds) {
2609
+ throw new StellarAgentError({
2610
+ code: "MAINNET_NOT_ENABLED",
2611
+ message: "Mainnet liquidity pool mutation requires an external signer and is not available through local auto-signing.",
2612
+ hint: "Use Testnet for local liquidity workflows.",
2613
+ docs: "docs/mainnet-safety.md#mainnet-liquidity"
2614
+ });
2615
+ }
2616
+ const wallet = await loadWallet(context.config, args.source);
2617
+ if (args.action === "deposit")
2618
+ validatePriceBounds(args.minPrice, args.maxPrice);
2619
+ const { preflightLiquidityPoolDeposit, preflightLiquidityPoolWithdraw, submitLiquidityPoolDeposit, submitLiquidityPoolWithdraw } = await import("@stellar-agent/stellar");
2620
+ const preflight = args.action === "deposit"
2621
+ ? await preflightLiquidityPoolDeposit({
2622
+ poolId: args.pool,
2623
+ maxAmountA: args.maxA,
2624
+ maxAmountB: args.maxB,
2625
+ minPrice: args.minPrice,
2626
+ maxPrice: args.maxPrice,
2627
+ account: wallet.publicKey,
2628
+ profile
2629
+ })
2630
+ : await preflightLiquidityPoolWithdraw({
2631
+ poolId: args.pool,
2632
+ shares: args.shares,
2633
+ minAmountA: args.minA,
2634
+ minAmountB: args.minB,
2635
+ account: wallet.publicKey,
2636
+ profile
2637
+ });
2638
+ const policy = await loadPolicy(context);
2639
+ const policyDecision = evaluateMarketLiquidityRequest(policy, liquidityPolicyRequest(profile, preflight));
2640
+ if (policyDecision.status !== "allowed")
2641
+ throw marketPolicyDeniedError(policyDecision);
2642
+ const submitted = args.action === "deposit"
2643
+ ? await submitLiquidityPoolDeposit({
2644
+ source: wallet,
2645
+ poolId: args.pool,
2646
+ maxAmountA: args.maxA,
2647
+ maxAmountB: args.maxB,
2648
+ minPrice: args.minPrice,
2649
+ maxPrice: args.maxPrice,
2650
+ profile,
2651
+ feeStrategy: args.feeStrategy,
2652
+ ...(context.options.noCache === undefined ? {} : { noCache: context.options.noCache })
2653
+ })
2654
+ : await submitLiquidityPoolWithdraw({
2655
+ source: wallet,
2656
+ poolId: args.pool,
2657
+ shares: args.shares,
2658
+ minAmountA: args.minA,
2659
+ minAmountB: args.minB,
2660
+ profile,
2661
+ feeStrategy: args.feeStrategy,
2662
+ ...(context.options.noCache === undefined ? {} : { noCache: context.options.noCache })
2663
+ });
2664
+ const receiptPath = await writeOperationReceipt(context, {
2665
+ command: args.command,
2666
+ policyDecision,
2667
+ operation: {
2668
+ type: `market.lp.${args.action}`,
2669
+ source: args.source,
2670
+ account: wallet.publicKey,
2671
+ details: { liquidityPool: submitted.preflight }
2672
+ },
2673
+ transaction: {
2674
+ hash: submitted.hash,
2675
+ ...(submitted.ledger === undefined ? {} : { ledger: submitted.ledger }),
2676
+ successful: submitted.successful,
2677
+ ...(submitted.feeCharged === undefined ? {} : { feeCharged: submitted.feeCharged })
2678
+ }
2679
+ });
2680
+ return {
2681
+ status: "submitted",
2682
+ transaction: {
2683
+ hash: submitted.hash,
2684
+ ...(submitted.ledger === undefined ? {} : { ledger: submitted.ledger }),
2685
+ successful: submitted.successful,
2686
+ ...(submitted.feeCharged === undefined ? {} : { feeCharged: submitted.feeCharged })
2687
+ },
2688
+ receiptPath,
2689
+ policyDecision,
2690
+ preflight: submitted.preflight
2691
+ };
2692
+ }
2693
+ function liquidityPolicyRequest(profile, preflight) {
2694
+ return {
2695
+ network: profile.realFunds ? "mainnet" : "testnet",
2696
+ pool: preflight.pool.id,
2697
+ assets: preflight.assets,
2698
+ action: preflight.action,
2699
+ exposureValue: liquidityExposureValue(preflight),
2700
+ priceBoundsProvided: preflight.action === "deposit" ? Boolean(preflight.deposit?.minPrice && preflight.deposit.maxPrice) : true
2701
+ };
2702
+ }
2703
+ function liquidityExposureValue(preflight) {
2704
+ if (preflight.deposit) {
2705
+ return String(Number(preflight.deposit.maxAmountA) + Number(preflight.deposit.maxAmountB));
2706
+ }
2707
+ return "0";
2708
+ }
2709
+ function marketPolicyDeniedError(decision) {
2710
+ return new StellarAgentError({
2711
+ code: "POLICY_DENIED",
2712
+ message: "Market liquidity request was denied by policy.",
2713
+ hint: "Run market lp preflight to inspect the matched rules.",
2714
+ docs: "docs/market-liquidity.md#policy",
2715
+ details: decision
2716
+ });
2717
+ }
2718
+ async function listenForPoolPrice(context, options) {
2719
+ if (options.above === undefined && options.below === undefined) {
2720
+ throw new StellarAgentError({
2721
+ code: "INVALID_INPUT",
2722
+ message: "Price listener requires --above or --below.",
2723
+ docs: "docs/market-liquidity.md#market-listeners"
2724
+ });
2725
+ }
2726
+ const above = options.above === undefined ? undefined : parsePositiveDecimalInput(options.above, "above");
2727
+ const below = options.below === undefined ? undefined : parsePositiveDecimalInput(options.below, "below");
2728
+ const { inspectLiquidityPool } = await import("@stellar-agent/stellar");
2729
+ const profile = resolveMarketProfile(context, options.network);
2730
+ const events = [];
2731
+ for (let poll = 0; poll < options.polls; poll += 1) {
2732
+ if (poll > 0)
2733
+ await sleep(options.intervalMs);
2734
+ const pool = await inspectLiquidityPool({ poolId: options.pool, profile });
2735
+ const price = poolReservePrice(pool);
2736
+ const aboveTriggered = above === undefined ? false : price > above;
2737
+ const belowTriggered = below === undefined ? false : price < below;
2738
+ events.push({
2739
+ type: "market.alert",
2740
+ rule: "core-pool-price",
2741
+ status: aboveTriggered || belowTriggered ? "triggered" : "not_triggered",
2742
+ pool: options.pool,
2743
+ observed: price.toFixed(7),
2744
+ thresholds: {
2745
+ ...(options.above === undefined ? {} : { above: options.above }),
2746
+ ...(options.below === undefined ? {} : { below: options.below })
2747
+ },
2748
+ assets: pool.reserves.map((reserve) => reserve.asset)
2749
+ });
2750
+ }
2751
+ return { events, triggered: events.some((event) => event.status === "triggered") };
2752
+ }
2753
+ function poolReservePrice(pool) {
2754
+ if (pool.reserves.length !== 2 || !pool.reserves[0] || !pool.reserves[1]) {
2755
+ throw new StellarAgentError({
2756
+ code: "LEDGER_LOOKUP_FAILED",
2757
+ message: "Liquidity pool record did not include exactly two reserves.",
2758
+ docs: "docs/market-liquidity.md#core-pool-inspection"
2759
+ });
2760
+ }
2761
+ const reserveA = Number(pool.reserves[0].amount);
2762
+ const reserveB = Number(pool.reserves[1].amount);
2763
+ if (!Number.isFinite(reserveA) || !Number.isFinite(reserveB) || reserveA <= 0) {
2764
+ throw new StellarAgentError({
2765
+ code: "LEDGER_LOOKUP_FAILED",
2766
+ message: "Liquidity pool reserves did not contain a usable price.",
2767
+ docs: "docs/market-liquidity.md#market-listeners"
2768
+ });
2769
+ }
2770
+ return reserveB / reserveA;
2771
+ }
2772
+ function validatePriceBounds(minPrice, maxPrice) {
2773
+ const min = parsePositiveDecimalInput(minPrice, "min-price");
2774
+ const max = parsePositiveDecimalInput(maxPrice, "max-price");
2775
+ if (min > max) {
2776
+ throw new StellarAgentError({
2777
+ code: "INVALID_INPUT",
2778
+ message: "Liquidity deposit --min-price must be less than or equal to --max-price.",
2779
+ docs: "docs/market-liquidity.md#lp-preflight"
2780
+ });
2781
+ }
2782
+ }
2783
+ function parsePositiveDecimalInput(input, label) {
2784
+ const value = parseNonnegativeDecimalInput(input, label);
2785
+ if (value <= 0) {
2786
+ throw new StellarAgentError({
2787
+ code: "INVALID_INPUT",
2788
+ message: `${label} must be greater than zero.`,
2789
+ docs: "docs/market-liquidity.md"
2790
+ });
2791
+ }
2792
+ return value;
2793
+ }
2794
+ function parseNonnegativeDecimalInput(input, label) {
2795
+ if (!/^(0|[1-9]\d*)(\.\d+)?$/.test(input.trim())) {
2796
+ throw new StellarAgentError({
2797
+ code: "INVALID_INPUT",
2798
+ message: `${label} must be a decimal number.`,
2799
+ docs: "docs/market-liquidity.md"
2800
+ });
2801
+ }
2802
+ const value = Number(input);
2803
+ if (!Number.isFinite(value) || value < 0) {
2804
+ throw new StellarAgentError({
2805
+ code: "INVALID_INPUT",
2806
+ message: `${label} must be a finite nonnegative number.`,
2807
+ docs: "docs/market-liquidity.md"
2808
+ });
2809
+ }
2810
+ return value;
2811
+ }
2812
+ async function readStrategyFile(file) {
2813
+ const raw = await readFile(resolvePath(file), "utf8");
2814
+ const parsed = file.endsWith(".json") ? JSON.parse(raw) : parseYaml(raw);
2815
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2816
+ throw new StellarAgentError({
2817
+ code: "INVALID_INPUT",
2818
+ message: "Strategy file must contain a JSON or YAML object.",
2819
+ docs: "docs/market-liquidity.md#strategy-investigation"
2820
+ });
2821
+ }
2822
+ return parsed;
2823
+ }
2824
+ function explainStrategy(strategy) {
2825
+ const kind = typeof strategy.kind === "string" ? strategy.kind : "liquidity";
2826
+ const actions = Array.isArray(strategy.actions) ? strategy.actions : [];
2827
+ return {
2828
+ status: "explained",
2829
+ kind,
2830
+ actionCount: actions.length,
2831
+ submitted: false,
2832
+ signing: false,
2833
+ requiredControls: [
2834
+ "Run market lp preflight before any core LP mutation.",
2835
+ "Require policy approval for any request outside configured market.liquidity limits.",
2836
+ "Use external signing for Mainnet.",
2837
+ "Write receipts for submitted Testnet liquidity actions."
2838
+ ],
2839
+ riskNotes: [
2840
+ "Strategy files are proposals, not profitability guarantees.",
2841
+ "Testnet liquidity does not prove Mainnet profitability.",
2842
+ "Quotes and pool snapshots can change before submission."
2843
+ ],
2844
+ strategy
2845
+ };
2846
+ }
2847
+ function parsePairOption(pair) {
2848
+ const separator = pair.indexOf("/");
2849
+ if (separator <= 0 || separator === pair.length - 1) {
2850
+ throw new StellarAgentError({
2851
+ code: "INVALID_INPUT",
2852
+ message: "Pair must use assetA/assetB format.",
2853
+ docs: "docs/market-liquidity.md#strategy-investigation"
2854
+ });
2855
+ }
2856
+ return [pair.slice(0, separator), pair.slice(separator + 1)];
2857
+ }
2858
+ function sleep(ms) {
2859
+ return new Promise((resolve) => setTimeout(resolve, ms));
2860
+ }
2861
+ function liquidityInvestigation(args) {
2862
+ return {
2863
+ network: args.profile.name,
2864
+ pool: args.pool,
2865
+ currentPrice: poolReservePrice(args.pool).toFixed(7),
2866
+ recentTrades: args.trades,
2867
+ execution: {
2868
+ submitted: false,
2869
+ mutationSupported: args.profile.realFunds ? "external_signer_required" : "testnet_preflight_required"
2870
+ },
2871
+ riskNotes: [
2872
+ "Liquidity fees are not guaranteed profit.",
2873
+ "LP positions can lose relative value versus holding reserve assets.",
2874
+ "Inspect issuer risk and trustlines before depositing."
2875
+ ]
2876
+ };
2877
+ }
2878
+ function collectOption(value, previous) {
2879
+ previous.push(value);
2880
+ return previous;
2881
+ }
2882
+ async function runBlendSubmitCommand(context, args) {
2883
+ const { blendDeployment, preflightBlendActions, resolveBlendAsset, resolveBlendPool, submitBlendActions } = await loadDefi();
2884
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
2885
+ if (profile.realFunds) {
2886
+ throw new StellarAgentError({
2887
+ code: "MAINNET_NOT_ENABLED",
2888
+ message: "Mainnet Blend mutation requires an external signer and is not available through local auto-signing.",
2889
+ hint: "Use Testnet for local Blend mutation workflows.",
2890
+ docs: "docs/mainnet-safety.md#mainnet-defi"
2891
+ });
2892
+ }
2893
+ const deployment = blendDeployment(blendNetworkForProfile(profile));
2894
+ const pool = resolveBlendPool(deployment, args.pool);
2895
+ const wallet = await loadWallet(context.config, args.source);
2896
+ const actions = args.actions.map((action) => {
2897
+ const asset = resolveBlendAsset(deployment, action.asset);
2898
+ return { ...action, asset: asset.contractId };
2899
+ });
2900
+ const preflight = await preflightBlendActions({
2901
+ profile,
2902
+ poolId: pool.contractId,
2903
+ userId: wallet.publicKey,
2904
+ actions,
2905
+ poolVersion: pool.version
2906
+ });
2907
+ const policy = await loadPolicy(context);
2908
+ const policyDecision = evaluateDefiBlendRequest(policy, blendPolicyRequest(profile, pool.contractId, preflight));
2909
+ if (policyDecision.status !== "allowed")
2910
+ throw defiPolicyDeniedError(policyDecision);
2911
+ const submitted = await submitBlendActions({
2912
+ profile,
2913
+ poolId: pool.contractId,
2914
+ userId: wallet.publicKey,
2915
+ actions,
2916
+ poolVersion: pool.version,
2917
+ sourceSecretKey: wallet.secretKey
2918
+ });
2919
+ const receiptPath = await writeBlendReceipt(context, {
2920
+ command: args.command,
2921
+ policyDecision,
2922
+ operation: {
2923
+ type: "defi.blend.submit",
2924
+ source: args.source,
2925
+ account: wallet.publicKey,
2926
+ details: {
2927
+ blend: {
2928
+ pool,
2929
+ actions: submitted.preflight.actions,
2930
+ before: submitted.preflight.before,
2931
+ after: submitted.preflight.after,
2932
+ simulation: submitted.simulation,
2933
+ decodedEvents: submitted.decodedEvents
2934
+ }
2935
+ }
2936
+ },
2937
+ transaction: {
2938
+ hash: submitted.hash,
2939
+ ...(submitted.ledger === undefined ? {} : { ledger: submitted.ledger }),
2940
+ successful: submitted.successful,
2941
+ ...(submitted.feeCharged === undefined ? {} : { feeCharged: submitted.feeCharged })
2942
+ }
2943
+ });
2944
+ return {
2945
+ status: "submitted",
2946
+ transaction: {
2947
+ hash: submitted.hash,
2948
+ ...(submitted.ledger === undefined ? {} : { ledger: submitted.ledger }),
2949
+ successful: submitted.successful,
2950
+ ...(submitted.feeCharged === undefined ? {} : { feeCharged: submitted.feeCharged })
2951
+ },
2952
+ receiptPath,
2953
+ policyDecision,
2954
+ preflight: submitted.preflight,
2955
+ simulation: submitted.simulation,
2956
+ decodedEvents: submitted.decodedEvents
2957
+ };
2958
+ }
2959
+ function blendPolicyRequest(profile, pool, preflight) {
2960
+ const borrowValue = preflight.actions
2961
+ .filter((action) => action.type === "borrow")
2962
+ .reduce((total, action) => total + (action.value ?? 0), 0);
2963
+ const protocolExposureValue = (preflight.after?.totalSupplied ?? preflight.before?.totalSupplied ?? 0) +
2964
+ (preflight.after?.totalBorrowed ?? preflight.before?.totalBorrowed ?? 0);
2965
+ return {
2966
+ network: profile.realFunds ? "mainnet" : "testnet",
2967
+ pool,
2968
+ requestTypes: preflight.actions.map((action) => action.type),
2969
+ borrowValue: String(borrowValue),
2970
+ protocolExposureValue: String(protocolExposureValue),
2971
+ ...(preflight.after?.healthFactor === undefined ? {} : { healthFactorAfter: preflight.after.healthFactor })
2972
+ };
2973
+ }
2974
+ function defiPolicyDeniedError(decision) {
2975
+ return new StellarAgentError({
2976
+ code: "POLICY_DENIED",
2977
+ message: "Blend DeFi request was denied by policy.",
2978
+ hint: "Run defi blend preflight to inspect the matched rules.",
2979
+ docs: "docs/defi-blend.md#policy",
2980
+ details: decision
2981
+ });
2982
+ }
2983
+ async function blendTrustlineGuide(context, options) {
2984
+ const { blendDeployment, resolveBlendAsset } = await loadDefi();
2985
+ const profile = resolveBlendProfile(context, options.network);
2986
+ const deployment = blendDeployment(blendNetworkForProfile(profile));
2987
+ const asset = resolveBlendAsset(deployment, options.asset);
2988
+ const account = await resolvePublicAccount(context, options.account);
2989
+ const rawPublicAccount = /^G[A-Z2-7]{55}$/.test(options.account);
2990
+ const trustlines = profile.realFunds || asset.classicAsset === undefined || rawPublicAccount
2991
+ ? []
2992
+ : await walletTrustlines(context.config, options.account);
2993
+ const hasTrustline = asset.classicAsset
2994
+ ? trustlines.some((trustline) => trustline.asset.toUpperCase() === asset.classicAsset.toUpperCase())
2995
+ : false;
2996
+ return {
2997
+ network: deployment.network,
2998
+ account,
2999
+ asset,
3000
+ requiresTrustline: asset.classicAsset !== undefined,
3001
+ hasTrustline,
3002
+ command: asset.classicAsset === undefined || profile.realFunds || rawPublicAccount
3003
+ ? null
3004
+ : `stellar-agent defi blend trustline add --account ${options.account} --asset ${asset.symbol}`
3005
+ };
3006
+ }
3007
+ async function addBlendTrustline(context, options) {
3008
+ const { blendDeployment, resolveBlendAsset } = await loadDefi();
3009
+ const profile = resolveNetworkProfile(context.profileName, context.config.profiles);
3010
+ if (profile.realFunds) {
3011
+ throw new StellarAgentError({
3012
+ code: "MAINNET_NOT_ENABLED",
3013
+ message: "Mainnet trustline creation requires an external signer.",
3014
+ docs: "docs/mainnet-safety.md#mainnet-defi"
3015
+ });
3016
+ }
3017
+ const deployment = blendDeployment(blendNetworkForProfile(profile));
3018
+ const asset = resolveBlendAsset(deployment, options.asset);
3019
+ if (!asset.classicAsset) {
3020
+ return {
3021
+ status: "not_required",
3022
+ asset,
3023
+ message: "This Blend reserve does not require a classic issued-asset trustline."
3024
+ };
3025
+ }
3026
+ const wallet = await loadWallet(context.config, options.account);
3027
+ const { changeTrustline } = await import("@stellar-agent/stellar");
3028
+ const transaction = await changeTrustline({
3029
+ source: wallet,
3030
+ asset: asset.classicAsset,
3031
+ profile,
3032
+ ...(options.limit === undefined ? {} : { limit: options.limit })
3033
+ });
3034
+ const receiptPath = await writeOperationReceipt(context, {
3035
+ command: "defi blend trustline add",
3036
+ operation: {
3037
+ type: "defi.blend.trustline.add",
3038
+ account: options.account,
3039
+ asset: asset.classicAsset,
3040
+ limit: transaction.limit,
3041
+ details: { blend: { asset } }
3042
+ },
3043
+ transaction
3044
+ });
3045
+ return {
3046
+ status: "trustline_added",
3047
+ asset,
3048
+ account: wallet.publicKey,
3049
+ transaction,
3050
+ receiptPath
3051
+ };
3052
+ }
3053
+ async function loadSpendHistory(context, request) {
3054
+ return spendHistoryFromReceipts(context.config.storage.receiptsDir, {
3055
+ profile: request.network,
3056
+ asset: request.asset
3057
+ });
3058
+ }
3059
+ function parseFeeStrategy(value) {
3060
+ if (value === "base" || value === "low" || value === "medium" || value === "high" || value === "p95")
3061
+ return value;
3062
+ throw new StellarAgentError({
3063
+ code: "INVALID_INPUT",
3064
+ message: "Fee strategy must be base, low, medium, high, or p95.",
3065
+ hint: "Use --fee-strategy medium unless you intentionally need a different fee posture.",
3066
+ docs: "docs/troubleshooting.md#fee-too-low"
3067
+ });
3068
+ }
3069
+ function parseBatchPaymentsFile(raw) {
3070
+ let parsed;
3071
+ try {
3072
+ parsed = JSON.parse(raw);
3073
+ }
3074
+ catch (error) {
3075
+ throw new StellarAgentError({
3076
+ code: "INVALID_INPUT",
3077
+ message: "Batch payment file must be valid JSON.",
3078
+ hint: "Use an array like [{\"destination\":\"G...\",\"amount\":\"1\",\"asset\":\"XLM\"}].",
3079
+ docs: "docs/troubleshooting.md#batch-transaction-failed",
3080
+ details: String(error)
3081
+ });
3082
+ }
3083
+ if (!Array.isArray(parsed) || parsed.length < 1 || parsed.length > 100) {
3084
+ throw new StellarAgentError({
3085
+ code: "INVALID_INPUT",
3086
+ message: "Batch payment file must contain 1 to 100 payments.",
3087
+ docs: "docs/troubleshooting.md#batch-transaction-failed"
3088
+ });
3089
+ }
3090
+ return parsed.map((item, index) => {
3091
+ if (!item || typeof item !== "object") {
3092
+ throw new StellarAgentError({
3093
+ code: "INVALID_INPUT",
3094
+ message: `Batch payment ${index + 1} must be an object.`,
3095
+ docs: "docs/troubleshooting.md#batch-transaction-failed"
3096
+ });
3097
+ }
3098
+ const candidate = item;
3099
+ if (typeof candidate.destination !== "string" || typeof candidate.amount !== "string") {
3100
+ throw new StellarAgentError({
3101
+ code: "INVALID_INPUT",
3102
+ message: `Batch payment ${index + 1} requires destination and amount strings.`,
3103
+ docs: "docs/troubleshooting.md#batch-transaction-failed"
3104
+ });
3105
+ }
3106
+ const asset = typeof candidate.asset === "string" ? candidate.asset : "XLM";
3107
+ const amount = parseAmount(candidate.amount, asset).value;
3108
+ return { destination: candidate.destination, amount, asset };
3109
+ });
3110
+ }
3111
+ async function evaluateBatchPaymentPolicy(context, args) {
3112
+ const histories = new Map();
3113
+ const results = [];
3114
+ for (const [index, payment] of args.payments.entries()) {
3115
+ const request = paymentRequestSchema.parse({
3116
+ source: args.source,
3117
+ destination: payment.destination,
3118
+ amount: payment.amount,
3119
+ asset: payment.asset,
3120
+ memo: args.memo,
3121
+ network: "testnet"
3122
+ });
3123
+ const assetKey = request.asset.toUpperCase();
3124
+ let history = histories.get(assetKey);
3125
+ if (!history) {
3126
+ history = await loadSpendHistory(context, request);
3127
+ histories.set(assetKey, { ...history });
3128
+ }
3129
+ const decision = evaluatePaymentRequest(args.policy, request, history);
3130
+ results.push({ index, request, spendHistory: history, policyDecision: decision });
3131
+ if (decision.status === "allowed") {
3132
+ histories.set(assetKey, incrementBatchSpendHistory(history, request));
3133
+ }
3134
+ }
3135
+ const status = results.some((result) => result.policyDecision.status === "denied")
3136
+ ? "denied"
3137
+ : results.some((result) => result.policyDecision.status === "requires_approval")
3138
+ ? "requires_approval"
3139
+ : "allowed";
3140
+ return {
3141
+ aggregate: {
3142
+ status,
3143
+ matchedRules: [...new Set(results.flatMap((result) => result.policyDecision.matchedRules))]
3144
+ },
3145
+ payments: results
3146
+ };
3147
+ }
3148
+ function incrementBatchSpendHistory(history, request) {
3149
+ if (history.unreadable)
3150
+ return history;
3151
+ const amount = parseAmount(request.amount, request.asset).stroops;
3152
+ const add = (value) => formatStroops(parseNonnegativeStroops(value, request.asset) + amount);
3153
+ return {
3154
+ ...history,
3155
+ dailyTotal: add(history.dailyTotal),
3156
+ monthlyTotal: add(history.monthlyTotal),
3157
+ knownRecipients: [...new Set([...(history.knownRecipients ?? []), request.destination])],
3158
+ ...(request.domain
3159
+ ? { knownDomains: [...new Set([...(history.knownDomains ?? []), request.domain])] }
3160
+ : history.knownDomains === undefined
3161
+ ? {}
3162
+ : { knownDomains: history.knownDomains })
3163
+ };
3164
+ }
3165
+ function parseNonnegativeStroops(value, asset) {
3166
+ const raw = value ?? "0";
3167
+ if (/^0(?:\.0{0,7})?$/.test(raw))
3168
+ return 0n;
3169
+ return parseAmount(raw, asset).stroops;
3170
+ }
3171
+ function policyDeniedError() {
3172
+ return new StellarAgentError({
3173
+ code: "POLICY_DENIED",
3174
+ message: "Payment request was denied by policy.",
3175
+ hint: "Run policy explain to inspect the matched rules.",
3176
+ docs: "docs/troubleshooting.md#policy-denied"
3177
+ });
3178
+ }
3179
+ function assertGuardedRealFundsProfile(context, profile, options) {
3180
+ if (!profile.realFunds)
3181
+ return;
3182
+ if (!context.config.profiles.mainnet?.enabled) {
3183
+ throw new StellarAgentError({
3184
+ code: "MAINNET_NOT_ENABLED",
3185
+ message: `Mainnet ${options.action} requires mainnet enablement.`,
3186
+ hint: "Run stellar-agent mainnet enable --i-understand-real-funds first.",
3187
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
3188
+ });
3189
+ }
3190
+ if (!options.allowRealFunds || !options.acknowledgeRealFunds) {
3191
+ throw new StellarAgentError({
3192
+ code: "MAINNET_NOT_ENABLED",
3193
+ message: `Mainnet ${options.action} requires --allow-real-funds and --i-understand-real-funds.`,
3194
+ hint: "Use signed XDR from a human-controlled Mainnet wallet; stellar-agent will not auto-sign Mainnet transactions.",
3195
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
3196
+ });
3197
+ }
3198
+ }
3199
+ async function verifyReceiptAgainstLedger(context, receipt) {
3200
+ const transaction = (await lookupTransaction(receipt.transaction.hash, resolveNetworkProfile(receipt.profile, context.config.profiles)));
3201
+ if (transaction.hash !== receipt.transaction.hash) {
3202
+ throw new StellarAgentError({
3203
+ code: "LEDGER_LOOKUP_FAILED",
3204
+ message: "Ledger transaction hash did not match the receipt.",
3205
+ docs: "docs/ledger-logging.md#receipt-verification"
3206
+ });
3207
+ }
3208
+ if (receipt.transaction.ledger !== undefined && transaction.ledger !== receipt.transaction.ledger) {
3209
+ throw new StellarAgentError({
3210
+ code: "LEDGER_LOOKUP_FAILED",
3211
+ message: "Ledger transaction number did not match the receipt.",
3212
+ docs: "docs/ledger-logging.md#receipt-verification"
3213
+ });
3214
+ }
3215
+ if (receipt.transaction.successful !== undefined && transaction.successful !== receipt.transaction.successful) {
3216
+ throw new StellarAgentError({
3217
+ code: "LEDGER_LOOKUP_FAILED",
3218
+ message: "Ledger transaction success status did not match the receipt.",
3219
+ docs: "docs/ledger-logging.md#receipt-verification"
3220
+ });
3221
+ }
3222
+ return {
3223
+ verified: true,
3224
+ hash: transaction.hash,
3225
+ ledger: transaction.ledger,
3226
+ successful: transaction.successful,
3227
+ feeCharged: transaction.fee_charged?.toString()
3228
+ };
3229
+ }
3230
+ function printSuccess(options, data, humanMessage) {
3231
+ process.exitCode = EXIT_CODES.success;
3232
+ if (options.json) {
3233
+ process.stdout.write(`${JSON.stringify(ok(redactSensitive(data)))}\n`);
3234
+ return;
3235
+ }
3236
+ if (!options.quiet) {
3237
+ process.stdout.write(`${humanMessage}\n`);
3238
+ if (data !== undefined)
3239
+ process.stdout.write(`${JSON.stringify(redactSensitive(data), null, 2)}\n`);
3240
+ }
3241
+ }
3242
+ function printError(options, error) {
3243
+ const serialized = serializeError(error);
3244
+ process.exitCode = error instanceof StellarAgentError ? error.exitCode : EXIT_CODES.general;
3245
+ if (options.json) {
3246
+ process.stdout.write(`${JSON.stringify(fail(serialized))}\n`);
3247
+ return;
3248
+ }
3249
+ process.stderr.write(`${serialized.code}: ${serialized.message}\n`);
3250
+ if (serialized.hint)
3251
+ process.stderr.write(`Hint: ${serialized.hint}\n`);
3252
+ if (serialized.docs)
3253
+ process.stderr.write(`Docs: ${serialized.docs}\n`);
3254
+ }
3255
+ function fixtureRequest(amount) {
3256
+ return {
3257
+ destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
3258
+ amount,
3259
+ asset: "XLM",
3260
+ network: "testnet"
3261
+ };
3262
+ }
3263
+ function deepMerge(base, overlay) {
3264
+ if (!isPlainObject(base) || !isPlainObject(overlay))
3265
+ return overlay;
3266
+ const output = { ...base };
3267
+ for (const [key, value] of Object.entries(overlay)) {
3268
+ output[key] = key in output ? deepMerge(output[key], value) : value;
3269
+ }
3270
+ return output;
3271
+ }
3272
+ function isPlainObject(value) {
3273
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3274
+ }
3275
+ function collectArg(value, previous) {
3276
+ return [...previous, value];
3277
+ }
3278
+ function parseKeyValueArgs(values) {
3279
+ const parsed = {};
3280
+ for (const value of values) {
3281
+ const separator = value.indexOf("=");
3282
+ if (separator <= 0) {
3283
+ throw new StellarAgentError({
3284
+ code: "INVALID_INPUT",
3285
+ message: `Contract arg '${value}' must use key=value format.`,
3286
+ docs: "docs/smart-contracts.md"
3287
+ });
3288
+ }
3289
+ parsed[value.slice(0, separator)] = value.slice(separator + 1);
3290
+ }
3291
+ return parsed;
3292
+ }
3293
+ function parseIntegerOption(value) {
3294
+ const parsed = Number.parseInt(value, 10);
3295
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 200) {
3296
+ throw new StellarAgentError({
3297
+ code: "INVALID_INPUT",
3298
+ message: "Limit must be an integer between 1 and 200.",
3299
+ docs: "docs/ledger-logging.md"
3300
+ });
3301
+ }
3302
+ return parsed;
3303
+ }
3304
+ function parsePortOption(value) {
3305
+ const parsed = Number.parseInt(value, 10);
3306
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
3307
+ throw new StellarAgentError({
3308
+ code: "INVALID_INPUT",
3309
+ message: "Port must be an integer between 0 and 65535."
3310
+ });
3311
+ }
3312
+ return parsed;
3313
+ }
3314
+ function parsePositiveIntegerOption(value) {
3315
+ const parsed = Number.parseInt(value, 10);
3316
+ if (!Number.isInteger(parsed) || parsed < 1) {
3317
+ throw new StellarAgentError({
3318
+ code: "INVALID_INPUT",
3319
+ message: "Value must be a positive integer."
3320
+ });
3321
+ }
3322
+ return parsed;
3323
+ }
3324
+ function parseLedgersToExtendOption(value) {
3325
+ const parsed = Number.parseInt(value, 10);
3326
+ if (!Number.isInteger(parsed) || parsed < 1) {
3327
+ throw new StellarAgentError({
3328
+ code: "INVALID_INPUT",
3329
+ message: "Ledgers to extend must be a positive integer.",
3330
+ docs: "docs/smart-contracts.md#extend-and-restore"
3331
+ });
3332
+ }
3333
+ return parsed;
3334
+ }
3335
+ function validateDurability(value) {
3336
+ if (value !== "persistent" && value !== "temporary") {
3337
+ throw new StellarAgentError({
3338
+ code: "INVALID_INPUT",
3339
+ message: "Durability must be persistent or temporary.",
3340
+ docs: "docs/smart-contracts.md"
3341
+ });
3342
+ }
3343
+ }
3344
+ function validateContractReadOutput(value) {
3345
+ if (value !== "string" && value !== "json" && value !== "xdr") {
3346
+ throw new StellarAgentError({
3347
+ code: "INVALID_INPUT",
3348
+ message: "Contract read output must be string, json, or xdr.",
3349
+ docs: "docs/smart-contracts.md#read"
3350
+ });
3351
+ }
3352
+ }
3353
+ async function writeOperationReceipt(context, args) {
3354
+ const eventLog = join(context.config.storage.logsDir, "events.jsonl");
3355
+ const profile = resolveNetworkProfile("testnet", context.config.profiles);
3356
+ const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
3357
+ command: args.command,
3358
+ profile: "testnet",
3359
+ networkPassphrase: profile.networkPassphrase,
3360
+ realFunds: false,
3361
+ operation: args.operation,
3362
+ policyDecision: args.policyDecision
3363
+ ? { status: args.policyDecision.status, matchedRules: args.policyDecision.matchedRules }
3364
+ : { status: "allowed", matchedRules: ["testnet_operation"] },
3365
+ transaction: args.transaction,
3366
+ ...(args.transaction.ledger === undefined ? {} : { ledger: { confirmedLedger: args.transaction.ledger } }),
3367
+ eventLog
3368
+ });
3369
+ await appendEvent(eventLog, {
3370
+ event: "receipt_written",
3371
+ status: "success",
3372
+ command: args.command,
3373
+ profile: "testnet",
3374
+ data: { receiptPath }
3375
+ });
3376
+ return receiptPath;
3377
+ }
3378
+ async function writeBlendReceipt(context, args) {
3379
+ const eventLog = join(context.config.storage.logsDir, "events.jsonl");
3380
+ const profile = resolveNetworkProfile("testnet", context.config.profiles);
3381
+ const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
3382
+ command: args.command,
3383
+ profile: "testnet",
3384
+ networkPassphrase: profile.networkPassphrase,
3385
+ realFunds: false,
3386
+ operation: args.operation,
3387
+ policyDecision: {
3388
+ status: args.policyDecision.status,
3389
+ matchedRules: args.policyDecision.matchedRules
3390
+ },
3391
+ transaction: args.transaction,
3392
+ ...(args.transaction.ledger === undefined ? {} : { ledger: { confirmedLedger: args.transaction.ledger } }),
3393
+ eventLog
3394
+ });
3395
+ await appendEvent(eventLog, {
3396
+ event: "receipt_written",
3397
+ status: "success",
3398
+ command: args.command,
3399
+ profile: "testnet",
3400
+ data: { receiptPath }
3401
+ });
3402
+ return receiptPath;
3403
+ }
3404
+ async function writeSubmittedXdrReceipt(context, args) {
3405
+ const eventLog = join(context.config.storage.logsDir, "events.jsonl");
3406
+ const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
3407
+ command: args.command,
3408
+ profile: args.profile.name,
3409
+ networkPassphrase: args.profile.networkPassphrase,
3410
+ realFunds: args.profile.realFunds,
3411
+ operation: args.operation,
3412
+ policyDecision: {
3413
+ status: args.profile.realFunds ? "requires_approval" : "allowed",
3414
+ matchedRules: args.profile.realFunds
3415
+ ? ["signed_xdr_external_wallet", "real_funds_acknowledged"]
3416
+ : ["signed_xdr_external_wallet"]
3417
+ },
3418
+ transaction: args.transaction,
3419
+ ...(args.transaction.ledger === undefined ? {} : { ledger: { confirmedLedger: args.transaction.ledger } }),
3420
+ eventLog
3421
+ });
3422
+ await appendEvent(eventLog, {
3423
+ event: "receipt_written",
3424
+ status: "success",
3425
+ command: args.command,
3426
+ profile: args.profile.name,
3427
+ data: { receiptPath }
3428
+ });
3429
+ return receiptPath;
3430
+ }
3431
+ async function attachContractSubmissionReceipt(context, args) {
3432
+ if (args.network.toLowerCase() !== "testnet")
3433
+ return args.result;
3434
+ const transactionHash = extractStellarCliTransactionHash(`${args.result.stderr}\n${args.result.stdout}`);
3435
+ if (!transactionHash)
3436
+ return args.result;
3437
+ const receiptPath = await writeOperationReceipt(context, {
3438
+ command: args.command,
3439
+ operation: args.operation,
3440
+ transaction: {
3441
+ hash: transactionHash,
3442
+ successful: true
3443
+ }
3444
+ });
3445
+ return { ...args.result, transactionHash, receiptPath };
3446
+ }
3447
+ function extractStellarCliTransactionHash(output) {
3448
+ return parseStellarCliTransactionHash(output);
3449
+ }
3450
+ async function writeLocalReport(context, outputPath) {
3451
+ const path = resolvePath(outputPath);
3452
+ await mkdir(dirname(path), { recursive: true });
3453
+ const report = await buildLocalReport(context);
3454
+ await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
3455
+ return { path, report };
3456
+ }
3457
+ async function buildLocalReport(context) {
3458
+ const receiptPaths = await listReceipts(context.config.storage.receiptsDir);
3459
+ const receipts = await Promise.all(receiptPaths.map(async (receiptPath) => ({
3460
+ path: receiptPath,
3461
+ receipt: await readReceipt(receiptPath)
3462
+ })));
3463
+ const wallets = await Promise.all(["agent", "merchant", "auditor"].map(async (name) => {
3464
+ try {
3465
+ const wallet = await loadWallet(context.config, name);
3466
+ return { name, exists: true, publicKey: wallet.publicKey };
3467
+ }
3468
+ catch {
3469
+ return { name, exists: false };
3470
+ }
3471
+ }));
3472
+ return {
3473
+ schemaVersion: "stellar-agent.report.v1",
3474
+ generatedAt: new Date().toISOString(),
3475
+ profile: context.profileName,
3476
+ storage: context.config.storage,
3477
+ wallets,
3478
+ receipts,
3479
+ eventLog: join(context.config.storage.logsDir, "events.jsonl"),
3480
+ redactions: {
3481
+ secretKeysIncluded: false
3482
+ }
3483
+ };
3484
+ }
3485
+ function enableX402ForUrl(policy, url) {
3486
+ const host = new URL(url).host;
3487
+ if (!/^localhost(?::\d+)?$/.test(host) && !/^127\.0\.0\.1(?::\d+)?$/.test(host)) {
3488
+ return policy;
3489
+ }
3490
+ return {
3491
+ ...policy,
3492
+ x402: {
3493
+ ...policy.x402,
3494
+ enabled: true,
3495
+ allowDomains: Array.from(new Set([...policy.x402.allowDomains, host])),
3496
+ maxPricePerRequest: policy.x402.maxPricePerRequest || "1 XLM"
3497
+ }
3498
+ };
3499
+ }
3500
+ function resolveContractExecutionContext(context, network, options) {
3501
+ const realFunds = isRealFundsContractNetwork(network);
3502
+ const profile = realFunds ? context.config.profiles.mainnet : context.config.profiles.testnet;
3503
+ if (!profile) {
3504
+ throw new StellarAgentError({
3505
+ code: "PROFILE_NOT_FOUND",
3506
+ message: `${realFunds ? "Mainnet" : "Testnet"} profile is not configured.`
3507
+ });
3508
+ }
3509
+ if (realFunds) {
3510
+ if (!profile.enabled) {
3511
+ throw new StellarAgentError({
3512
+ code: "MAINNET_NOT_ENABLED",
3513
+ message: "Mainnet contract operations require mainnet enablement.",
3514
+ hint: "Run stellar-agent mainnet enable --i-understand-real-funds first.",
3515
+ docs: "docs/mainnet-safety.md#mainnet-contracts"
3516
+ });
3517
+ }
3518
+ if (options.mutating && (!options.allowRealFunds || !options.acknowledgeRealFunds)) {
3519
+ throw new StellarAgentError({
3520
+ code: "MAINNET_NOT_ENABLED",
3521
+ message: "Mainnet contract operations require --allow-real-funds and --i-understand-real-funds.",
3522
+ hint: "Use an external Stellar CLI identity or browser-wallet signing flow; stellar-agent will not import Mainnet secret keys.",
3523
+ docs: "docs/mainnet-safety.md#mainnet-contracts"
3524
+ });
3525
+ }
3526
+ }
3527
+ return {
3528
+ realFunds,
3529
+ rpcUrl: profile.rpcUrl,
3530
+ networkPassphrase: profile.networkPassphrase
3531
+ };
3532
+ }
3533
+ function isRealFundsContractNetwork(network) {
3534
+ return ["mainnet", "public", "pubnet"].includes(network.toLowerCase());
3535
+ }
3536
+ async function resolveContractSource(context, source, options = {}) {
3537
+ if (options.realFunds) {
3538
+ if (/^S[A-Z2-7]{55}$/.test(source)) {
3539
+ throw new StellarAgentError({
3540
+ code: "SECRET_KEY_BLOCKED",
3541
+ message: "Raw secret keys are not accepted for Mainnet contract operations.",
3542
+ hint: "Use a Stellar CLI identity or browser-wallet signing flow for Mainnet.",
3543
+ docs: "docs/mainnet-safety.md#mainnet-contracts"
3544
+ });
3545
+ }
3546
+ try {
3547
+ const wallet = await loadWalletPublic(context.config, source);
3548
+ if (wallet.hasSecret) {
3549
+ throw new StellarAgentError({
3550
+ code: "MAINNET_NOT_ENABLED",
3551
+ message: "Local generated Testnet wallets cannot be used for Mainnet contract operations.",
3552
+ hint: "Use a Stellar CLI identity or a watch-only Mainnet public wallet.",
3553
+ docs: "docs/mainnet-safety.md#mainnet-contracts"
3554
+ });
3555
+ }
3556
+ return wallet.publicKey;
3557
+ }
3558
+ catch (error) {
3559
+ if (error instanceof StellarAgentError && error.code !== "WALLET_NOT_FOUND")
3560
+ throw error;
3561
+ }
3562
+ return source;
3563
+ }
3564
+ if (/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(source)) {
3565
+ try {
3566
+ const wallet = await loadWallet(context.config, source);
3567
+ return wallet.secretKey;
3568
+ }
3569
+ catch {
3570
+ return source;
3571
+ }
3572
+ }
3573
+ return source;
3574
+ }
3575
+ async function resolvePaymentSourcePublicKey(context, source, options = {}) {
3576
+ if (/^G[A-Z2-7]{55}$/.test(source))
3577
+ return source;
3578
+ if (options.realFunds && /^S[A-Z2-7]{55}$/.test(source)) {
3579
+ throw new StellarAgentError({
3580
+ code: "SECRET_KEY_BLOCKED",
3581
+ message: "Raw secret keys are not accepted for Mainnet payment-XDR workflows.",
3582
+ hint: "Import a watch-only Mainnet public wallet or pass a raw public key.",
3583
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
3584
+ });
3585
+ }
3586
+ const wallet = await loadWalletPublic(context.config, source);
3587
+ if (options.realFunds && wallet.hasSecret) {
3588
+ throw new StellarAgentError({
3589
+ code: "MAINNET_NOT_ENABLED",
3590
+ message: "Local generated Testnet wallets cannot be used for Mainnet payment-XDR workflows.",
3591
+ hint: "Import a watch-only Mainnet public wallet or pass a raw public key.",
3592
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
3593
+ });
3594
+ }
3595
+ return wallet.publicKey;
3596
+ }
3597
+ if (isCliEntrypoint()) {
3598
+ const argv = process.argv[2] === "--" ? [process.argv[0] ?? "node", process.argv[1] ?? "stellar-agent", ...process.argv.slice(3)] : process.argv;
3599
+ await buildProgram().parseAsync(argv);
3600
+ }
3601
+ export function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta.url) {
3602
+ if (!argvPath)
3603
+ return false;
3604
+ return realpathSync(argvPath) === realpathSync(fileURLToPath(moduleUrl));
3605
+ }
3606
+ //# sourceMappingURL=index.js.map