@stellar-agent/cli 0.4.3 → 0.5.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 CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { EXIT_CODES, StellarAgentError, configSchema, createDefaultConfig, fail, formatStroops, ok, parseAmount, paymentRequestSchema, redactSensitive, resolvePath, serializeError } from "@stellar-agent/core";
2
+ import { EXIT_CODES, MAINNET_AGENT_WALLET_AUTOSIGN_WARNING, MAINNET_AGENT_WALLET_WARNING, StellarAgentError, configSchema, assertMainnetAgentWalletBalanceWithinBudget as assertCoreMainnetAgentWalletBalanceWithinBudget, assertMainnetAgentWalletPaymentPreflight, createDefaultConfig, fail, formatStroops, mainnetAgentWalletConfigFingerprint as coreMainnetAgentWalletConfigFingerprint, mainnetAgentWalletIntegrityFailures as coreMainnetAgentWalletIntegrityFailures, mainnetAgentWalletRiskBudgetState as coreMainnetAgentWalletRiskBudgetState, ok, parseAmount, paymentRequestSchema, redactSensitive, resolvePath, serializeError } from "@stellar-agent/core";
3
3
  import { latestLedger, lookupTransaction, parseStellarCliTransactionHash, resolveNetworkProfile } from "@stellar-agent/stellar";
4
4
  import { appendEvent, latestReceipt, listReceipts, readReceipt, spendHistoryFromReceipts, verifyReceipt, writeReceipt } from "@stellar-agent/ledger-logger";
5
5
  import { DEFAULT_MAINNET_POLICY, DEFAULT_TESTNET_POLICY, defaultPolicyForNetwork, evaluateDefiAquariusRequest, evaluateDefiBlendRequest, evaluateMarketLiquidityRequest, evaluatePaymentRequest, parsePolicyYaml, policyToYaml } from "@stellar-agent/policy";
6
6
  import { createTestnetHarness, ensureWallet, importPublicWallet, initTestnetWorkspace, listWalletPublicViews, loadWallet, loadWalletPublic, walletBalances, walletTrustlines } from "@stellar-agent/testnet-suite";
7
7
  import { Command } from "commander";
8
+ import { createHash } from "node:crypto";
8
9
  import { realpathSync } from "node:fs";
9
10
  import { createRequire } from "node:module";
10
11
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -15,6 +16,7 @@ const require = createRequire(import.meta.url);
15
16
  const { version: VERSION } = require("../package.json");
16
17
  export function buildProgram() {
17
18
  const program = new Command();
19
+ const parseState = { json: false };
18
20
  program
19
21
  .name("stellar-agent")
20
22
  .description("Stellar Agent Bridge\n\nSafe agentic payments on Stellar from your terminal.")
@@ -49,12 +51,19 @@ Common commands:
49
51
  stellar-agent market pools list --asset-a XLM --asset-b USDC:G... --json
50
52
  stellar-agent market lp preflight --pool 0123... --max-a 1 --max-b 1 --min-price 0.9 --max-price 1.1 --json
51
53
  stellar-agent policy explain --request ./payment-request.json`);
54
+ program.configureOutput({
55
+ writeErr: (str) => {
56
+ if (!parseState.json)
57
+ process.stderr.write(str);
58
+ }
59
+ });
52
60
  addProfileCommands(program);
53
61
  addTestnetCommands(program);
54
62
  addWalletCommands(program);
55
63
  addApprovalCommands(program);
56
64
  addTransactionCommands(program);
57
65
  addPayCommands(program);
66
+ addX402Commands(program);
58
67
  addClaimableCommands(program);
59
68
  addContractCommands(program);
60
69
  addMarketCommands(program);
@@ -65,21 +74,31 @@ Common commands:
65
74
  addReceiptCommands(program);
66
75
  addCacheCommands(program);
67
76
  addMainnetCommands(program);
68
- installVersionPrecheck(program);
77
+ installVersionPrecheck(program, parseState);
69
78
  program.action(() => {
70
79
  program.outputHelp();
71
80
  });
72
81
  return program;
73
82
  }
74
- function installVersionPrecheck(program) {
83
+ function installVersionPrecheck(program, parseState) {
75
84
  const originalParseAsync = program.parseAsync.bind(program);
76
85
  program.parseAsync = (async (argv, parseOptions) => {
86
+ parseState.json = jsonOptionRequested(argv, parseOptions);
77
87
  const versionOptions = rootVersionOptions(program, argv, parseOptions);
78
88
  if (versionOptions) {
79
89
  printVersion(versionOptions);
80
- program._exit(EXIT_CODES.success, "commander.version", VERSION);
90
+ return program;
91
+ }
92
+ installExitOverride(program);
93
+ try {
94
+ return await originalParseAsync(argv, parseOptions);
95
+ }
96
+ catch (error) {
97
+ if (isCommanderHelpOrVersionExit(error))
98
+ return program;
99
+ printError({ json: parseState.json }, commanderErrorToStellarAgentError(error));
100
+ return program;
81
101
  }
82
- return await originalParseAsync(argv, parseOptions);
83
102
  });
84
103
  }
85
104
  function rootVersionOptions(program, argv = process.argv, parseOptions) {
@@ -117,6 +136,14 @@ function userArgs(argv, parseOptions) {
117
136
  return args.slice(1);
118
137
  return args.slice(2);
119
138
  }
139
+ function jsonOptionRequested(argv = process.argv, parseOptions) {
140
+ return userArgs(argv, parseOptions).includes("--json");
141
+ }
142
+ function installExitOverride(command) {
143
+ command.exitOverride();
144
+ for (const subcommand of command.commands)
145
+ installExitOverride(subcommand);
146
+ }
120
147
  function addCacheCommands(program) {
121
148
  const cache = program.command("cache").description("Inspect and clear in-process session caches.");
122
149
  cache
@@ -623,6 +650,7 @@ function addApprovalCommands(program) {
623
650
  .description("Start the local approval bridge HTTP server.")
624
651
  .option("--host <host>", "Host", "127.0.0.1")
625
652
  .option("--port <port>", "Port", parsePortOption, 0)
653
+ .option("--allow-remote-access", "Allow binding the approval bridge to a non-loopback host")
626
654
  .action(async (options, command) => {
627
655
  const parent = command.optsWithGlobals();
628
656
  const config = await loadConfig(parent);
@@ -632,13 +660,15 @@ function addApprovalCommands(program) {
632
660
  const bridge = await startApprovalBridge({
633
661
  approvalsDir: context.config.storage.approvalsDir,
634
662
  host: options.host,
635
- port: options.port
663
+ port: options.port,
664
+ allowRemoteAccess: Boolean(options.allowRemoteAccess)
636
665
  });
637
666
  if (parent.json) {
638
- process.stdout.write(`${JSON.stringify(ok({ url: bridge.url, authToken: bridge.authToken, approvalsDir: context.config.storage.approvalsDir }))}\n`);
667
+ process.stdout.write(`${JSON.stringify(ok({ url: bridge.url, uiUrl: bridge.uiUrl, authToken: bridge.authToken, approvalsDir: context.config.storage.approvalsDir }))}\n`);
639
668
  }
640
669
  else {
641
670
  process.stdout.write(`Approval bridge listening at ${bridge.url}\n`);
671
+ process.stdout.write(`Open approval UI: ${bridge.uiUrl}\n`);
642
672
  process.stdout.write("Approval bridge API requires the printed session token for non-browser requests.\n");
643
673
  process.stdout.write(`Session token: ${bridge.authToken}\n`);
644
674
  }
@@ -684,6 +714,7 @@ function addTransactionCommands(program) {
684
714
  const policyDecision = evaluatePaymentRequest(policy, request, history);
685
715
  if (policyDecision.status === "denied")
686
716
  throw policyDeniedError();
717
+ const mainnetAgentWallet = await assertMainnetAgentWalletPaymentAllowed(context, request);
687
718
  if (profile.realFunds && policyDecision.status === "requires_approval") {
688
719
  throw new StellarAgentError({
689
720
  code: "APPROVAL_REQUIRED",
@@ -710,7 +741,13 @@ function addTransactionCommands(program) {
710
741
  profile: profile.name,
711
742
  data: { source: sourcePublicKey, destination: options.to, asset: options.asset, amount: built.amount, policyDecision }
712
743
  });
713
- return { ...built, policyDecision, spendHistory: history, realFunds: profile.realFunds };
744
+ return {
745
+ ...built,
746
+ policyDecision,
747
+ spendHistory: mainnetAgentWallet?.spendHistory ?? history,
748
+ realFunds: profile.realFunds,
749
+ ...(mainnetAgentWallet === undefined ? {} : { mainnetAgentWallet })
750
+ };
714
751
  }, "Unsigned payment transaction built."));
715
752
  tx
716
753
  .command("request-payment-signature")
@@ -747,6 +784,7 @@ function addTransactionCommands(program) {
747
784
  const policyDecision = evaluatePaymentRequest(policy, request, history);
748
785
  if (policyDecision.status === "denied")
749
786
  throw policyDeniedError();
787
+ const mainnetAgentWallet = await assertMainnetAgentWalletPaymentAllowed(context, request);
750
788
  const built = await buildPaymentTransactionXdr({
751
789
  sourcePublicKey,
752
790
  destination: options.to,
@@ -762,7 +800,8 @@ function addTransactionCommands(program) {
762
800
  approvalsDir: context.config.storage.approvalsDir,
763
801
  network: profile.name,
764
802
  transactionXdr: built.xdr,
765
- summary: options.summary ?? `Sign ${built.amount} ${built.asset} payment to ${options.to} on ${profile.name}`
803
+ summary: options.summary ?? `Sign ${built.amount} ${built.asset} payment to ${options.to} on ${profile.name}`,
804
+ payment: request
766
805
  });
767
806
  await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
768
807
  event: "approval_requested",
@@ -772,7 +811,14 @@ function addTransactionCommands(program) {
772
811
  requestId: approval.id,
773
812
  data: { approval, source: sourcePublicKey, destination: options.to, asset: options.asset, amount: built.amount, policyDecision }
774
813
  });
775
- return { approval, built, policyDecision, spendHistory: history, realFunds: profile.realFunds };
814
+ return {
815
+ approval,
816
+ built,
817
+ policyDecision,
818
+ spendHistory: mainnetAgentWallet?.spendHistory ?? history,
819
+ realFunds: profile.realFunds,
820
+ ...(mainnetAgentWallet === undefined ? {} : { mainnetAgentWallet })
821
+ };
776
822
  }, "Payment signature approval request created."));
777
823
  tx
778
824
  .command("submit-xdr")
@@ -851,6 +897,10 @@ function addTransactionCommands(program) {
851
897
  acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds),
852
898
  action: "signed approval submission"
853
899
  });
900
+ const approvalPayment = approval.payment;
901
+ const approvedPayment = approvalPayment === undefined
902
+ ? undefined
903
+ : await evaluateApprovedPaymentBeforeSubmission(context, approvalPayment);
854
904
  const { submitTransactionXdr } = await import("@stellar-agent/stellar");
855
905
  const transaction = await submitTransactionXdr({
856
906
  xdr: approval.decision.signedTransactionXdr,
@@ -869,19 +919,38 @@ function addTransactionCommands(program) {
869
919
  transaction
870
920
  }
871
921
  });
872
- const receiptPath = await writeSubmittedXdrReceipt(context, {
873
- command: "tx submit-approval",
874
- profile,
875
- operation: {
876
- type: "tx.submit_approval",
877
- details: {
878
- approvalId: approval.id,
879
- signerPublicKey: approval.decision.signerPublicKey,
880
- realFundsAcknowledged: profile.realFunds
881
- }
882
- },
883
- transaction
884
- });
922
+ let receiptPath;
923
+ if (approvedPayment === undefined || approvalPayment === undefined) {
924
+ receiptPath = await writeSubmittedXdrReceipt(context, {
925
+ command: "tx submit-approval",
926
+ profile,
927
+ operation: {
928
+ type: "tx.submit_approval",
929
+ details: {
930
+ approvalId: approval.id,
931
+ signerPublicKey: approval.decision.signerPublicKey,
932
+ realFundsAcknowledged: profile.realFunds
933
+ }
934
+ },
935
+ transaction
936
+ });
937
+ }
938
+ else {
939
+ receiptPath = await writeSubmittedPaymentApprovalReceipt(context, {
940
+ command: "tx submit-approval",
941
+ profile,
942
+ approvalId: approval.id,
943
+ ...(approval.decision.signerPublicKey === undefined
944
+ ? {}
945
+ : { signerPublicKey: approval.decision.signerPublicKey }),
946
+ payment: approvalPayment,
947
+ policyDecision: approvedPayment.policyDecision,
948
+ transaction,
949
+ ...(approvedPayment.mainnetAgentWallet === undefined
950
+ ? {}
951
+ : { mainnetAgentWallet: approvedPayment.mainnetAgentWallet })
952
+ });
953
+ }
885
954
  return { approvalId: approval.id, signerPublicKey: approval.decision.signerPublicKey, transaction, receiptPath, realFunds: profile.realFunds };
886
955
  }, "Signed approval transaction submitted."));
887
956
  }
@@ -922,7 +991,7 @@ function addPayCommands(program) {
922
991
  }, "Payment quote complete."));
923
992
  pay
924
993
  .command("send")
925
- .description("Send an XLM or issued-asset payment on Testnet when policy allows.")
994
+ .description("Send an XLM or issued-asset payment on Testnet, or an explicitly armed Mainnet agent-wallet payment.")
926
995
  .requiredOption("--to <address>", "Destination public key")
927
996
  .requiredOption("--amount <amount>", "Payment amount")
928
997
  .option("--asset <asset>", "Asset", "XLM")
@@ -930,14 +999,13 @@ function addPayCommands(program) {
930
999
  .option("--memo <memo>", "Memo")
931
1000
  .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
932
1001
  .option("--approval-id <id>", "Approved local approval request id")
1002
+ .option("--allow-real-funds", "Permit the guarded Mainnet agent-wallet autosign path")
1003
+ .option("--i-understand-real-funds", "Acknowledge this payment can spend real Mainnet funds")
1004
+ .option("--i-understand-agent-wallet-autosign", "Acknowledge bounded Mainnet agent-wallet autosigning risk")
933
1005
  .option("--dry-run", "Evaluate locally without submitting")
934
1006
  .action(withContext(async (context, options) => {
935
1007
  if (context.profileName === "mainnet") {
936
- throw new StellarAgentError({
937
- code: "MAINNET_NOT_ENABLED",
938
- message: "Mainnet payment submission is blocked in v0.",
939
- docs: "docs/mainnet-safety.md"
940
- });
1008
+ return sendMainnetAgentWalletPayment(context, options);
941
1009
  }
942
1010
  const policy = await loadPolicy(context);
943
1011
  const source = await loadWallet(context.config, options.from);
@@ -1205,6 +1273,45 @@ function addPayCommands(program) {
1205
1273
  });
1206
1274
  }, "MPP session complete."));
1207
1275
  }
1276
+ function addX402Commands(program) {
1277
+ const x402 = program.command("x402").description("Scaffold and inspect x402-style paid API workflows.");
1278
+ x402
1279
+ .command("init-server")
1280
+ .description("Create a local Testnet x402-style paid API server scaffold.")
1281
+ .option("--out <dir>", "Output directory", "./stellar-agent-x402-server")
1282
+ .option("--force", "Overwrite scaffold files if they already exist")
1283
+ .action(withContext(async (_context, options) => {
1284
+ const outDir = resolvePath(options.out);
1285
+ const files = x402ServerScaffoldFiles();
1286
+ await mkdir(outDir, { recursive: true });
1287
+ for (const [name, contents] of Object.entries(files)) {
1288
+ const target = join(outDir, name);
1289
+ if (!options.force) {
1290
+ try {
1291
+ await readFile(target, "utf8");
1292
+ throw new StellarAgentError({
1293
+ code: "INVALID_INPUT",
1294
+ message: `Refusing to overwrite existing scaffold file '${target}'.`,
1295
+ hint: "Use --force to replace scaffold files intentionally."
1296
+ });
1297
+ }
1298
+ catch (error) {
1299
+ if (error instanceof StellarAgentError)
1300
+ throw error;
1301
+ if (error?.code !== "ENOENT")
1302
+ throw error;
1303
+ }
1304
+ }
1305
+ await writeFile(target, contents, { mode: name.endsWith(".mjs") ? 0o755 : 0o600 });
1306
+ }
1307
+ return {
1308
+ path: outDir,
1309
+ files: Object.keys(files),
1310
+ network: "testnet",
1311
+ realFunds: false
1312
+ };
1313
+ }, "x402 server scaffold initialized."));
1314
+ }
1208
1315
  function addClaimableCommands(program) {
1209
1316
  const claimable = program.command("claimable").description("Create, list, and claim claimable balances.");
1210
1317
  claimable
@@ -2354,7 +2461,7 @@ function addDefiCommands(program) {
2354
2461
  ...(options.shares === undefined ? {} : { shareAmount: options.shares }),
2355
2462
  ...(options.minAmount.length === 0 ? {} : { minAmounts: options.minAmount })
2356
2463
  });
2357
- const policy = await loadPolicy(context);
2464
+ const policy = await loadPolicyForRequestNetwork(context, network);
2358
2465
  const policyDecision = evaluateDefiAquariusRequest(policy, aquariusLpPolicyRequest(preflight));
2359
2466
  return { policyDecision, preflight };
2360
2467
  }, "Aquarius LP preflight complete."));
@@ -2388,15 +2495,16 @@ function addDefiCommands(program) {
2388
2495
  .option("--network <name>", "Network profile to inspect: testnet or mainnet")
2389
2496
  .action(withContext(async (context, options) => {
2390
2497
  const { preflightAquariusSwap } = await loadDefi();
2498
+ const network = resolveAquariusNetworkOption(context, options.network);
2391
2499
  const preflight = await preflightAquariusSwap({
2392
- network: resolveAquariusNetworkOption(context, options.network),
2500
+ network,
2393
2501
  inputAsset: options.from,
2394
2502
  outputAsset: options.to,
2395
2503
  amount: options.amount,
2396
2504
  mode: parseAquariusSwapMode(options.mode),
2397
2505
  slippageBps: options.slippageBps
2398
2506
  });
2399
- const policy = await loadPolicy(context);
2507
+ const policy = await loadPolicyForRequestNetwork(context, network);
2400
2508
  const policyDecision = evaluateDefiAquariusRequest(policy, aquariusSwapPolicyRequest(preflight));
2401
2509
  return { policyDecision, preflight };
2402
2510
  }, "Aquarius swap preflight complete."));
@@ -2427,9 +2535,11 @@ function addPolicyCommands(program) {
2427
2535
  .action(withContext(async (context, options) => {
2428
2536
  const target = defaultPolicyForNetwork(options.network);
2429
2537
  const path = options.output ??
2430
- join(context.config.storage.policiesDir, options.network === "mainnet" ? "default-mainnet.yaml" : "default-testnet.yaml");
2431
- await writeFile(resolvePath(path), policyToYaml(target), { mode: 0o600 });
2432
- return { path: resolvePath(path), policy: target };
2538
+ join(context.config.storage.policiesDir, defaultPolicyFilename(options.network));
2539
+ const resolvedPath = resolvePath(path);
2540
+ await mkdir(dirname(resolvedPath), { recursive: true });
2541
+ await writeFile(resolvedPath, policyToYaml(target), { mode: 0o600 });
2542
+ return { path: resolvedPath, policy: target };
2433
2543
  }, "Policy initialized."));
2434
2544
  policy
2435
2545
  .command("check")
@@ -2551,6 +2661,15 @@ function addReceiptCommands(program) {
2551
2661
  return { receipt: null };
2552
2662
  return latest;
2553
2663
  }, "Latest receipt loaded."));
2664
+ receipts
2665
+ .command("summary")
2666
+ .description("Summarize local receipt spend, Mainnet exposure, and recent activity.")
2667
+ .option("--profile <name>", "Filter by profile: testnet, mainnet, or local")
2668
+ .option("--asset <asset>", "Filter by payment asset")
2669
+ .action(withContext(async (context, options) => receiptSummary(context, {
2670
+ ...(options.profile === undefined ? {} : { profile: options.profile }),
2671
+ ...(options.asset === undefined ? {} : { asset: options.asset })
2672
+ }), "Receipt summary loaded."));
2554
2673
  receipts
2555
2674
  .command("show")
2556
2675
  .description("Show a receipt file.")
@@ -2623,6 +2742,228 @@ function addMainnetCommands(program) {
2623
2742
  "Receipts must mark realFunds: true."
2624
2743
  ]
2625
2744
  }), "Mainnet readiness checked."));
2745
+ const agentWallet = mainnet.command("agent-wallet").description("Manage a risk-budgeted Mainnet agent wallet for guarded external-signing workflows.");
2746
+ agentWallet
2747
+ .command("create")
2748
+ .description("Create or replace the dedicated Mainnet agent-wallet profile without storing a Mainnet secret key.")
2749
+ .requiredOption("--address <address>", "Dedicated Mainnet agent wallet public key")
2750
+ .option("--name <name>", "Local watch-only wallet name", "mainnet-agent")
2751
+ .option("--max-balance <amount>", "Maximum allowed wallet balance", "25")
2752
+ .option("--per-tx-limit <amount>", "Maximum single payment amount", "1")
2753
+ .option("--daily-limit <amount>", "Maximum daily payment amount", "5")
2754
+ .option("--monthly-limit <amount>", "Optional monthly payment amount")
2755
+ .option("--asset <asset>", "Allowed asset; repeat for more than one", collectArg, [])
2756
+ .option("--allow-destination <address>", "Allowed destination; repeat for more than one", collectArg, [])
2757
+ .action(withContext(async (context, options) => {
2758
+ const now = new Date().toISOString();
2759
+ const riskBudget = parseMainnetAgentWalletRiskBudget(options);
2760
+ await importPublicWallet({
2761
+ config: context.config,
2762
+ name: options.name,
2763
+ publicKey: options.address,
2764
+ network: "mainnet"
2765
+ });
2766
+ context.config.mainnetAgentWallet = {
2767
+ schemaVersion: "stellar-agent.mainnetAgentWallet.v1",
2768
+ walletName: options.name,
2769
+ publicKey: options.address,
2770
+ status: "disarmed",
2771
+ createdAt: context.config.mainnetAgentWallet?.createdAt ?? now,
2772
+ updatedAt: now,
2773
+ riskBudget
2774
+ };
2775
+ await writeConfig(context.config, context.options);
2776
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2777
+ }, "Mainnet agent wallet created."));
2778
+ agentWallet
2779
+ .command("enable")
2780
+ .description("Configure the dedicated Mainnet agent wallet, optionally enable env-var autosigning, and optionally arm it.")
2781
+ .requiredOption("--address <address>", "Dedicated Mainnet agent wallet public key")
2782
+ .option("--name <name>", "Local watch-only wallet name", "mainnet-agent")
2783
+ .option("--max-balance <amount>", "Maximum allowed wallet balance", "25")
2784
+ .option("--per-tx-limit <amount>", "Maximum single payment amount", "1")
2785
+ .option("--daily-limit <amount>", "Maximum daily payment amount", "5")
2786
+ .option("--monthly-limit <amount>", "Optional monthly payment amount")
2787
+ .option("--asset <asset>", "Allowed asset; repeat for more than one", collectArg, [])
2788
+ .option("--allow-destination <address>", "Allowed destination; repeat for more than one", collectArg, [])
2789
+ .option("--enable-autosign", "Enable env-var based autosigning for this agent wallet only")
2790
+ .option("--secret-key-env <name>", "Environment variable containing the agent-wallet secret key", "STELLAR_AGENT_MAINNET_AGENT_SECRET_KEY")
2791
+ .option("--i-understand-agent-wallet-autosign", "Acknowledge bounded Mainnet agent-wallet autosigning risk")
2792
+ .option("--arm", "Arm the wallet after configuration")
2793
+ .option("--i-understand-real-funds", "Acknowledge this wallet can spend real Mainnet funds")
2794
+ .action(withContext(async (context, options) => {
2795
+ const now = new Date().toISOString();
2796
+ const riskBudget = parseMainnetAgentWalletRiskBudget(options);
2797
+ await importPublicWallet({
2798
+ config: context.config,
2799
+ name: options.name,
2800
+ publicKey: options.address,
2801
+ network: "mainnet"
2802
+ });
2803
+ context.config.mainnetAgentWallet = {
2804
+ schemaVersion: "stellar-agent.mainnetAgentWallet.v1",
2805
+ walletName: options.name,
2806
+ publicKey: options.address,
2807
+ status: "disarmed",
2808
+ createdAt: context.config.mainnetAgentWallet?.createdAt ?? now,
2809
+ updatedAt: now,
2810
+ riskBudget,
2811
+ ...(options.enableAutosign
2812
+ ? {
2813
+ autosign: mainnetAgentWalletAutosignConfig({
2814
+ secretKeyEnvVar: options.secretKeyEnv,
2815
+ acknowledged: Boolean(options.iUnderstandAgentWalletAutosign)
2816
+ })
2817
+ }
2818
+ : {})
2819
+ };
2820
+ if (options.arm) {
2821
+ context.config.mainnetAgentWallet = await armMainnetAgentWallet(context, {
2822
+ iUnderstandRealFunds: Boolean(options.iUnderstandRealFunds)
2823
+ });
2824
+ }
2825
+ await writeConfig(context.config, context.options);
2826
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2827
+ }, "Mainnet agent wallet enabled."));
2828
+ agentWallet
2829
+ .command("status")
2830
+ .description("Show Mainnet agent-wallet arming and risk-budget status.")
2831
+ .action(withContext(async (context) => {
2832
+ const wallet = context.config.mainnetAgentWallet;
2833
+ if (!wallet)
2834
+ return { configured: false, armed: false, realFunds: true };
2835
+ return {
2836
+ configured: true,
2837
+ ...mainnetAgentWalletView(wallet),
2838
+ ...(await mainnetAgentWalletIntegrity(context, wallet))
2839
+ };
2840
+ }, "Mainnet agent wallet status loaded."));
2841
+ const autosign = agentWallet.command("autosign").description("Manage env-var based Mainnet agent-wallet autosigning.");
2842
+ autosign
2843
+ .command("enable")
2844
+ .description("Enable autosigning for the armed agent-wallet path only; no secret key is stored.")
2845
+ .option("--secret-key-env <name>", "Environment variable containing the agent-wallet secret key", "STELLAR_AGENT_MAINNET_AGENT_SECRET_KEY")
2846
+ .option("--i-understand-agent-wallet-autosign", "Acknowledge bounded Mainnet agent-wallet autosigning risk")
2847
+ .action(withContext(async (context, options) => {
2848
+ const wallet = requireMainnetAgentWallet(context.config);
2849
+ context.config.mainnetAgentWallet = {
2850
+ ...wallet,
2851
+ status: "disarmed",
2852
+ updatedAt: new Date().toISOString(),
2853
+ autosign: mainnetAgentWalletAutosignConfig({
2854
+ secretKeyEnvVar: options.secretKeyEnv,
2855
+ acknowledged: Boolean(options.iUnderstandAgentWalletAutosign)
2856
+ }),
2857
+ arming: undefined
2858
+ };
2859
+ await writeConfig(context.config, context.options);
2860
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2861
+ }, "Mainnet agent-wallet autosigning enabled and wallet disarmed for re-arming."));
2862
+ autosign
2863
+ .command("disable")
2864
+ .description("Disable Mainnet agent-wallet autosigning and disarm the wallet.")
2865
+ .action(withContext(async (context) => {
2866
+ const wallet = requireMainnetAgentWallet(context.config);
2867
+ context.config.mainnetAgentWallet = {
2868
+ ...wallet,
2869
+ status: "disarmed",
2870
+ updatedAt: new Date().toISOString(),
2871
+ autosign: { ...(wallet.autosign ?? mainnetAgentWalletAutosignConfig({ secretKeyEnvVar: "STELLAR_AGENT_MAINNET_AGENT_SECRET_KEY", acknowledged: true })), enabled: false },
2872
+ arming: undefined
2873
+ };
2874
+ await writeConfig(context.config, context.options);
2875
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2876
+ }, "Mainnet agent-wallet autosigning disabled."));
2877
+ autosign
2878
+ .command("status")
2879
+ .description("Show Mainnet agent-wallet autosigning status without reading the secret key.")
2880
+ .action(withContext(async (context) => {
2881
+ const wallet = requireMainnetAgentWallet(context.config);
2882
+ return {
2883
+ publicKey: wallet.publicKey,
2884
+ autosign: mainnetAgentWalletAutosignView(wallet),
2885
+ warning: mainnetAgentWalletAutosignWarning()
2886
+ };
2887
+ }, "Mainnet agent-wallet autosigning status loaded."));
2888
+ agentWallet
2889
+ .command("limits")
2890
+ .description("Show or update Mainnet agent-wallet risk-budget limits. Updating limits disarms the wallet.")
2891
+ .option("--max-balance <amount>", "Maximum allowed wallet balance")
2892
+ .option("--per-tx-limit <amount>", "Maximum single payment amount")
2893
+ .option("--daily-limit <amount>", "Maximum daily payment amount")
2894
+ .option("--monthly-limit <amount>", "Optional monthly payment amount")
2895
+ .option("--asset <asset>", "Allowed asset; repeat for more than one", collectArg, [])
2896
+ .option("--allow-destination <address>", "Allowed destination; repeat for more than one", collectArg, [])
2897
+ .action(withContext(async (context, options) => {
2898
+ const wallet = requireMainnetAgentWallet(context.config);
2899
+ const updates = parseMainnetAgentWalletRiskBudget({
2900
+ maxBalance: options.maxBalance ?? wallet.riskBudget.maxBalance,
2901
+ perTxLimit: options.perTxLimit ?? wallet.riskBudget.perTxLimit,
2902
+ dailyLimit: options.dailyLimit ?? wallet.riskBudget.dailyLimit,
2903
+ monthlyLimit: options.monthlyLimit ?? wallet.riskBudget.monthlyLimit,
2904
+ asset: options.asset.length > 0 ? options.asset : wallet.riskBudget.allowedAssets,
2905
+ allowDestination: options.allowDestination.length > 0 ? options.allowDestination : wallet.riskBudget.allowedDestinations
2906
+ });
2907
+ context.config.mainnetAgentWallet = {
2908
+ ...wallet,
2909
+ status: "disarmed",
2910
+ updatedAt: new Date().toISOString(),
2911
+ riskBudget: updates,
2912
+ arming: undefined
2913
+ };
2914
+ await writeConfig(context.config, context.options);
2915
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2916
+ }, "Mainnet agent wallet limits updated."));
2917
+ agentWallet
2918
+ .command("arm")
2919
+ .description("Arm the Mainnet agent wallet after explicit Mainnet enablement and risk-budget checks.")
2920
+ .option("--i-understand-real-funds", "Acknowledge this wallet can spend real funds through guarded external-signing workflows")
2921
+ .action(withContext(async (context, options) => {
2922
+ context.config.mainnetAgentWallet = await armMainnetAgentWallet(context, {
2923
+ iUnderstandRealFunds: Boolean(options.iUnderstandRealFunds)
2924
+ });
2925
+ await writeConfig(context.config, context.options);
2926
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2927
+ }, "Mainnet agent wallet armed."));
2928
+ agentWallet
2929
+ .command("disarm")
2930
+ .description("Disarm the Mainnet agent wallet.")
2931
+ .action(withContext(async (context) => {
2932
+ const wallet = requireMainnetAgentWallet(context.config);
2933
+ context.config.mainnetAgentWallet = {
2934
+ ...wallet,
2935
+ status: "disarmed",
2936
+ updatedAt: new Date().toISOString(),
2937
+ arming: undefined
2938
+ };
2939
+ await writeConfig(context.config, context.options);
2940
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2941
+ }, "Mainnet agent wallet disarmed."));
2942
+ agentWallet
2943
+ .command("rotate")
2944
+ .description("Rotate the dedicated Mainnet agent-wallet public key. Rotation disarms the wallet.")
2945
+ .requiredOption("--address <address>", "New dedicated Mainnet agent wallet public key")
2946
+ .option("--name <name>", "Local watch-only wallet name")
2947
+ .action(withContext(async (context, options) => {
2948
+ const wallet = requireMainnetAgentWallet(context.config);
2949
+ const walletName = options.name ?? wallet.walletName;
2950
+ await importPublicWallet({
2951
+ config: context.config,
2952
+ name: walletName,
2953
+ publicKey: options.address,
2954
+ network: "mainnet"
2955
+ });
2956
+ context.config.mainnetAgentWallet = {
2957
+ ...wallet,
2958
+ walletName,
2959
+ publicKey: options.address,
2960
+ status: "disarmed",
2961
+ updatedAt: new Date().toISOString(),
2962
+ arming: undefined
2963
+ };
2964
+ await writeConfig(context.config, context.options);
2965
+ return mainnetAgentWalletView(context.config.mainnetAgentWallet);
2966
+ }, "Mainnet agent wallet rotated and disarmed."));
2626
2967
  }
2627
2968
  function withContext(handler, humanMessage) {
2628
2969
  return async (...args) => {
@@ -2686,20 +3027,40 @@ async function createTestnetWalletResult(context, name, fund) {
2686
3027
  });
2687
3028
  return { wallet, funding };
2688
3029
  }
2689
- async function loadPolicy(context, explicitPath) {
3030
+ async function loadPolicy(context, explicitPath, network = context.profileName) {
2690
3031
  const policyPath = explicitPath ??
2691
3032
  context.options.policy ??
2692
3033
  process.env.STELLAR_AGENT_POLICY ??
2693
- join(context.config.storage.policiesDir, context.profileName === "mainnet" ? "default-mainnet.yaml" : "default-testnet.yaml");
3034
+ join(context.config.storage.policiesDir, defaultPolicyFilename(network));
2694
3035
  try {
2695
3036
  return parsePolicyYaml(await readFile(resolvePath(policyPath), "utf8"));
2696
3037
  }
2697
3038
  catch (error) {
2698
3039
  if (error?.code === "ENOENT")
2699
- return defaultPolicyForNetwork(context.profileName);
3040
+ return defaultPolicyForNetwork(network);
2700
3041
  throw error;
2701
3042
  }
2702
3043
  }
3044
+ async function loadPolicyForRequestNetwork(context, network) {
3045
+ const explicitPath = context.options.policy ?? process.env.STELLAR_AGENT_POLICY;
3046
+ const policy = await loadPolicy(context, undefined, network);
3047
+ if (explicitPath && policy.network !== network) {
3048
+ throw new StellarAgentError({
3049
+ code: "INVALID_INPUT",
3050
+ message: `Policy network '${policy.network}' does not match Aquarius preflight network '${network}'.`,
3051
+ hint: `Use a ${network} policy file or omit --policy to use the default ${network} policy.`,
3052
+ docs: network === "mainnet" ? "docs/mainnet-safety.md#mainnet-defi" : "docs/defi-aquarius.md"
3053
+ });
3054
+ }
3055
+ return policy;
3056
+ }
3057
+ function defaultPolicyFilename(network) {
3058
+ if (network === "mainnet")
3059
+ return "default-mainnet.yaml";
3060
+ if (network === "local")
3061
+ return "default-local.yaml";
3062
+ return "default-testnet.yaml";
3063
+ }
2703
3064
  function resolveBlendNetworkOption(context, network) {
2704
3065
  if (!network)
2705
3066
  return blendNetworkForProfile(resolveNetworkProfile(context.profileName, context.config.profiles));
@@ -3310,6 +3671,503 @@ async function loadSpendHistory(context, request) {
3310
3671
  asset: request.asset
3311
3672
  });
3312
3673
  }
3674
+ async function evaluateApprovedPaymentBeforeSubmission(context, payment) {
3675
+ if (payment.network !== context.profileName) {
3676
+ throw new StellarAgentError({
3677
+ code: "INVALID_INPUT",
3678
+ message: "Approval payment metadata must match the active submission profile.",
3679
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
3680
+ });
3681
+ }
3682
+ const policy = await loadPolicy(context, undefined, payment.network);
3683
+ const history = await loadSpendHistory(context, payment);
3684
+ const policyDecision = evaluatePaymentRequest(policy, payment, history);
3685
+ if (policyDecision.status === "denied")
3686
+ throw policyDeniedError();
3687
+ const mainnetAgentWallet = await assertMainnetAgentWalletPaymentAllowed(context, payment);
3688
+ return {
3689
+ policyDecision,
3690
+ ...(mainnetAgentWallet === undefined ? {} : { mainnetAgentWallet })
3691
+ };
3692
+ }
3693
+ function parseMainnetAgentWalletRiskBudget(options) {
3694
+ const allowedAssets = options.asset.length > 0 ? options.asset : ["XLM"];
3695
+ for (const asset of allowedAssets)
3696
+ parseAssetForPolicy(asset);
3697
+ for (const destination of options.allowDestination)
3698
+ paymentRequestSchema.shape.destination.parse(destination);
3699
+ return {
3700
+ maxBalance: parseAmount(options.maxBalance, "XLM").value,
3701
+ perTxLimit: parseAmount(options.perTxLimit, "XLM").value,
3702
+ dailyLimit: parseAmount(options.dailyLimit, "XLM").value,
3703
+ ...(options.monthlyLimit === undefined ? {} : { monthlyLimit: parseAmount(options.monthlyLimit, "XLM").value }),
3704
+ allowedAssets: allowedAssets.map((asset) => asset.toUpperCase()),
3705
+ allowedDestinations: [...new Set(options.allowDestination)],
3706
+ allowedOperations: ["payment"]
3707
+ };
3708
+ }
3709
+ function mainnetAgentWalletAutosignConfig(options) {
3710
+ if (!options.acknowledged) {
3711
+ throw new StellarAgentError({
3712
+ code: "MAINNET_NOT_ENABLED",
3713
+ message: "Enabling Mainnet agent-wallet autosigning requires --i-understand-agent-wallet-autosign.",
3714
+ hint: mainnetAgentWalletAutosignWarning(),
3715
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3716
+ });
3717
+ }
3718
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(options.secretKeyEnvVar)) {
3719
+ throw new StellarAgentError({
3720
+ code: "INVALID_INPUT",
3721
+ message: "Agent-wallet autosign secret key env var must be an uppercase environment variable name.",
3722
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3723
+ });
3724
+ }
3725
+ const now = new Date().toISOString();
3726
+ return {
3727
+ enabled: true,
3728
+ secretKeyEnvVar: options.secretKeyEnvVar,
3729
+ enabledAt: now,
3730
+ warningAcknowledgedAt: now
3731
+ };
3732
+ }
3733
+ async function armMainnetAgentWallet(context, options) {
3734
+ if (!options.iUnderstandRealFunds) {
3735
+ throw new StellarAgentError({
3736
+ code: "MAINNET_NOT_ENABLED",
3737
+ message: "Arming the Mainnet agent wallet requires --i-understand-real-funds.",
3738
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3739
+ });
3740
+ }
3741
+ if (!context.config.profiles.mainnet?.enabled) {
3742
+ throw new StellarAgentError({
3743
+ code: "MAINNET_NOT_ENABLED",
3744
+ message: "Arming the Mainnet agent wallet requires Mainnet enablement.",
3745
+ hint: "Run stellar-agent mainnet enable --i-understand-real-funds first.",
3746
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3747
+ });
3748
+ }
3749
+ const current = requireMainnetAgentWallet(context.config);
3750
+ if (!current.publicKey) {
3751
+ throw new StellarAgentError({
3752
+ code: "WALLET_NOT_FOUND",
3753
+ message: "Mainnet agent wallet does not have a public key.",
3754
+ hint: "Run stellar-agent mainnet agent-wallet create --address G...",
3755
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3756
+ });
3757
+ }
3758
+ if (current.riskBudget.allowedDestinations.length === 0) {
3759
+ throw new StellarAgentError({
3760
+ code: "POLICY_DENIED",
3761
+ message: "Mainnet agent wallet requires an explicit destination allowlist before arming.",
3762
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3763
+ });
3764
+ }
3765
+ await assertMainnetAgentWalletBalanceWithinBudget(context, current);
3766
+ const now = new Date().toISOString();
3767
+ const armedWallet = {
3768
+ ...current,
3769
+ status: "armed",
3770
+ updatedAt: now,
3771
+ spendCounters: await mainnetAgentWalletSpendCounters(context, current),
3772
+ arming: undefined
3773
+ };
3774
+ const policyFingerprint = await mainnetAgentWalletPolicyFingerprint(context);
3775
+ armedWallet.arming = {
3776
+ armedAt: now,
3777
+ configPath: configFingerprintPath(context),
3778
+ configFingerprint: coreMainnetAgentWalletConfigFingerprint({ ...context.config, mainnetAgentWallet: armedWallet }),
3779
+ policyPath: policyFingerprint.path,
3780
+ policyFingerprint: policyFingerprint.fingerprint
3781
+ };
3782
+ return armedWallet;
3783
+ }
3784
+ function requireMainnetAgentWallet(config) {
3785
+ const wallet = config.mainnetAgentWallet;
3786
+ if (!wallet) {
3787
+ throw new StellarAgentError({
3788
+ code: "WALLET_NOT_FOUND",
3789
+ message: "Mainnet agent wallet is not configured.",
3790
+ hint: "Run stellar-agent mainnet agent-wallet create --address G...",
3791
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3792
+ });
3793
+ }
3794
+ return wallet;
3795
+ }
3796
+ function mainnetAgentWalletView(wallet) {
3797
+ return {
3798
+ walletName: wallet.walletName,
3799
+ publicKey: wallet.publicKey,
3800
+ armed: wallet.status === "armed",
3801
+ status: wallet.status,
3802
+ realFunds: true,
3803
+ riskBudget: wallet.riskBudget,
3804
+ spendCounters: wallet.spendCounters,
3805
+ autosign: mainnetAgentWalletAutosignView(wallet),
3806
+ warning: mainnetAgentWalletWarning(),
3807
+ arming: wallet.arming === undefined
3808
+ ? undefined
3809
+ : {
3810
+ armedAt: wallet.arming.armedAt,
3811
+ configPath: wallet.arming.configPath,
3812
+ policyPath: wallet.arming.policyPath
3813
+ }
3814
+ };
3815
+ }
3816
+ function mainnetAgentWalletWarning() {
3817
+ return MAINNET_AGENT_WALLET_WARNING;
3818
+ }
3819
+ function mainnetAgentWalletAutosignWarning() {
3820
+ return MAINNET_AGENT_WALLET_AUTOSIGN_WARNING;
3821
+ }
3822
+ function mainnetAgentWalletAutosignView(wallet) {
3823
+ return {
3824
+ enabled: Boolean(wallet.autosign?.enabled),
3825
+ secretKeyEnvVar: wallet.autosign?.secretKeyEnvVar,
3826
+ enabledAt: wallet.autosign?.enabledAt,
3827
+ warning: wallet.autosign?.enabled ? mainnetAgentWalletAutosignWarning() : undefined
3828
+ };
3829
+ }
3830
+ async function mainnetAgentWalletIntegrity(context, wallet) {
3831
+ if (wallet.status !== "armed")
3832
+ return { integrity: { ok: true, checked: false, reason: "wallet_not_armed" } };
3833
+ const failures = await mainnetAgentWalletIntegrityFailures(context, wallet);
3834
+ return { integrity: { ok: failures.length === 0, checked: true, failures } };
3835
+ }
3836
+ async function mainnetAgentWalletIntegrityFailures(context, wallet) {
3837
+ if (!wallet.arming) {
3838
+ return ["arming_metadata_missing"];
3839
+ }
3840
+ const currentPolicy = await mainnetAgentWalletPolicyFingerprint(context);
3841
+ return coreMainnetAgentWalletIntegrityFailures({
3842
+ wallet,
3843
+ config: context.config,
3844
+ configPath: configFingerprintPath(context),
3845
+ policyPath: currentPolicy.path,
3846
+ policyFingerprint: currentPolicy.fingerprint
3847
+ });
3848
+ }
3849
+ async function assertMainnetAgentWalletPaymentAllowed(context, request) {
3850
+ const wallet = context.config.mainnetAgentWallet;
3851
+ if (!wallet || wallet.publicKey !== request.source || request.network !== "mainnet")
3852
+ return undefined;
3853
+ const spendHistory = await loadSpendHistory(context, request);
3854
+ const policyFingerprint = await mainnetAgentWalletPolicyFingerprint(context);
3855
+ assertMainnetAgentWalletPaymentPreflight({
3856
+ wallet,
3857
+ request,
3858
+ config: context.config,
3859
+ configPath: configFingerprintPath(context),
3860
+ policyPath: policyFingerprint.path,
3861
+ policyFingerprint: policyFingerprint.fingerprint,
3862
+ spendHistory
3863
+ });
3864
+ assertCoreMainnetAgentWalletBalanceWithinBudget(wallet, await readMainnetAgentWalletBalances(context, wallet));
3865
+ return { warning: mainnetAgentWalletWarning(), spendHistory };
3866
+ }
3867
+ async function assertMainnetAgentWalletAutosignPayment(context, request, options) {
3868
+ if (!options.allowRealFunds || !options.iUnderstandRealFunds || !options.iUnderstandAgentWalletAutosign) {
3869
+ throw new StellarAgentError({
3870
+ code: "MAINNET_NOT_ENABLED",
3871
+ message: "Mainnet agent-wallet autosigning requires --allow-real-funds, --i-understand-real-funds, and --i-understand-agent-wallet-autosign.",
3872
+ hint: mainnetAgentWalletAutosignWarning(),
3873
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3874
+ });
3875
+ }
3876
+ const wallet = requireMainnetAgentWallet(context.config);
3877
+ if (!wallet.autosign?.enabled) {
3878
+ throw new StellarAgentError({
3879
+ code: "MAINNET_NOT_ENABLED",
3880
+ message: "Mainnet agent-wallet autosigning is not enabled.",
3881
+ hint: "Run stellar-agent mainnet agent-wallet autosign enable --i-understand-agent-wallet-autosign, then arm the wallet.",
3882
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3883
+ });
3884
+ }
3885
+ const allowed = await assertMainnetAgentWalletPaymentAllowed(context, request);
3886
+ if (!allowed) {
3887
+ throw new StellarAgentError({
3888
+ code: "MAINNET_NOT_ENABLED",
3889
+ message: "Mainnet auto-signing is blocked outside the dedicated agent-wallet path.",
3890
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3891
+ });
3892
+ }
3893
+ const secretKey = process.env[wallet.autosign.secretKeyEnvVar];
3894
+ if (!secretKey) {
3895
+ throw new StellarAgentError({
3896
+ code: "SECRET_KEY_BLOCKED",
3897
+ message: `Mainnet agent-wallet secret key env var '${wallet.autosign.secretKeyEnvVar}' is not set.`,
3898
+ hint: "Set the env var only in the process that should autosign; the secret is never stored in config or receipts.",
3899
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3900
+ });
3901
+ }
3902
+ const { publicKeyFromSecret } = await import("@stellar-agent/stellar");
3903
+ let publicKey;
3904
+ try {
3905
+ publicKey = publicKeyFromSecret(secretKey);
3906
+ }
3907
+ catch {
3908
+ throw new StellarAgentError({
3909
+ code: "WALLET_INVALID",
3910
+ message: "Mainnet agent-wallet autosign secret key is invalid.",
3911
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3912
+ });
3913
+ }
3914
+ if (publicKey !== wallet.publicKey) {
3915
+ throw new StellarAgentError({
3916
+ code: "WALLET_INVALID",
3917
+ message: "Mainnet agent-wallet autosign secret key does not match the configured public key.",
3918
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3919
+ });
3920
+ }
3921
+ return {
3922
+ wallet,
3923
+ source: { publicKey, secretKey },
3924
+ warning: mainnetAgentWalletAutosignWarning(),
3925
+ spendHistory: allowed.spendHistory
3926
+ };
3927
+ }
3928
+ async function sendMainnetAgentWalletPayment(context, options) {
3929
+ const configured = requireMainnetAgentWallet(context.config);
3930
+ if (options.from !== configured.walletName && options.from !== configured.publicKey) {
3931
+ throw new StellarAgentError({
3932
+ code: "MAINNET_NOT_ENABLED",
3933
+ message: "Mainnet auto-signing is blocked outside the configured agent-wallet account.",
3934
+ hint: `Use --from ${configured.walletName} for the dedicated agent-wallet flow.`,
3935
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning"
3936
+ });
3937
+ }
3938
+ if (!configured.publicKey) {
3939
+ throw new StellarAgentError({
3940
+ code: "WALLET_NOT_FOUND",
3941
+ message: "Mainnet agent wallet does not have a public key.",
3942
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3943
+ });
3944
+ }
3945
+ const policy = await loadPolicy(context, undefined, "mainnet");
3946
+ const request = paymentRequestSchema.parse({
3947
+ source: configured.publicKey,
3948
+ destination: options.to,
3949
+ amount: options.amount,
3950
+ asset: options.asset,
3951
+ memo: options.memo,
3952
+ network: "mainnet",
3953
+ agentWalletAutosign: true
3954
+ });
3955
+ const history = await loadSpendHistory(context, request);
3956
+ if (history.unreadable) {
3957
+ throw new StellarAgentError({
3958
+ code: "POLICY_DENIED",
3959
+ message: "Mainnet agent-wallet spend history could not be read, so payment fails closed.",
3960
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
3961
+ });
3962
+ }
3963
+ const policyDecision = evaluatePaymentRequest(policy, request, history);
3964
+ if (options.dryRun)
3965
+ return { request, policyDecision, spendHistory: history, dryRun: true, autosign: mainnetAgentWalletAutosignView(configured) };
3966
+ await assertMainnetAgentWalletPaymentAllowed(context, request);
3967
+ if (policyDecision.status === "denied")
3968
+ throw policyDeniedError();
3969
+ if (policyDecision.status === "requires_approval") {
3970
+ throw new StellarAgentError({
3971
+ code: "APPROVAL_REQUIRED",
3972
+ message: "Mainnet agent-wallet autosigning requires policy status 'allowed'.",
3973
+ hint: "Use an external approval flow or add an explicit policy exception for risk-budgeted agent-wallet autosigning.",
3974
+ docs: "docs/mainnet-safety.md#agent-wallet-autosigning",
3975
+ details: { policyDecision }
3976
+ });
3977
+ }
3978
+ const autosign = await assertMainnetAgentWalletAutosignPayment(context, request, options);
3979
+ const { sendPayment } = await import("@stellar-agent/stellar");
3980
+ const mainnetProfile = resolveNetworkProfile("mainnet", context.config.profiles);
3981
+ const transaction = await sendPayment({
3982
+ source: autosign.source,
3983
+ destination: options.to,
3984
+ amount: options.amount,
3985
+ asset: options.asset,
3986
+ ...(options.memo === undefined ? {} : { memo: options.memo }),
3987
+ profile: mainnetProfile,
3988
+ allowRealFunds: true,
3989
+ feeStrategy: parseFeeStrategy(options.feeStrategy),
3990
+ ...(context.options.noCache === undefined ? {} : { noCache: context.options.noCache })
3991
+ });
3992
+ const eventLog = join(context.config.storage.logsDir, "events.jsonl");
3993
+ await appendEvent(eventLog, {
3994
+ event: "transaction_confirmed",
3995
+ status: "successful",
3996
+ command: "pay send",
3997
+ profile: "mainnet",
3998
+ data: {
3999
+ transaction,
4000
+ source: configured.walletName,
4001
+ sourcePublicKey: configured.publicKey,
4002
+ destination: options.to,
4003
+ asset: options.asset,
4004
+ mainnetAgentWalletAutosign: true,
4005
+ warning: autosign.warning
4006
+ }
4007
+ });
4008
+ const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
4009
+ command: "pay send",
4010
+ profile: "mainnet",
4011
+ networkPassphrase: mainnetProfile.networkPassphrase,
4012
+ realFunds: true,
4013
+ payment: {
4014
+ source: configured.publicKey,
4015
+ destination: options.to,
4016
+ asset: options.asset,
4017
+ amount: request.amount,
4018
+ ...(options.memo === undefined ? {} : { memo: options.memo })
4019
+ },
4020
+ operation: {
4021
+ type: "pay.send",
4022
+ source: configured.publicKey,
4023
+ destination: options.to,
4024
+ asset: options.asset,
4025
+ amount: request.amount,
4026
+ details: {
4027
+ mainnetAgentWallet: {
4028
+ autosign: true,
4029
+ walletName: configured.walletName,
4030
+ publicKey: configured.publicKey,
4031
+ secretKeyEnvVar: configured.autosign?.secretKeyEnvVar,
4032
+ riskBudgetState: coreMainnetAgentWalletRiskBudgetState(configured, autosign.spendHistory, request),
4033
+ warning: autosign.warning
4034
+ },
4035
+ realFundsAcknowledged: true
4036
+ }
4037
+ },
4038
+ policyDecision: {
4039
+ status: policyDecision.status,
4040
+ matchedRules: [...policyDecision.matchedRules, "mainnet_agent_wallet_autosign_acknowledged"]
4041
+ },
4042
+ transaction,
4043
+ ...(transaction.ledger === undefined ? {} : { ledger: { confirmedLedger: transaction.ledger } }),
4044
+ eventLog
4045
+ });
4046
+ await appendEvent(eventLog, {
4047
+ event: "receipt_written",
4048
+ status: "success",
4049
+ command: "pay send",
4050
+ profile: "mainnet",
4051
+ data: { receiptPath }
4052
+ });
4053
+ if (context.config.mainnetAgentWallet) {
4054
+ context.config.mainnetAgentWallet = {
4055
+ ...context.config.mainnetAgentWallet,
4056
+ spendCounters: await mainnetAgentWalletSpendCounters(context, context.config.mainnetAgentWallet)
4057
+ };
4058
+ await writeConfig(context.config, context.options);
4059
+ }
4060
+ return {
4061
+ request,
4062
+ policyDecision,
4063
+ autosign: {
4064
+ enabled: true,
4065
+ warning: autosign.warning
4066
+ },
4067
+ transaction,
4068
+ receiptPath,
4069
+ realFunds: true
4070
+ };
4071
+ }
4072
+ async function assertMainnetAgentWalletBalanceWithinBudget(context, wallet) {
4073
+ assertCoreMainnetAgentWalletBalanceWithinBudget(wallet, await readMainnetAgentWalletBalances(context, wallet));
4074
+ }
4075
+ async function readMainnetAgentWalletBalances(context, wallet) {
4076
+ if (!wallet.publicKey) {
4077
+ throw new StellarAgentError({ code: "WALLET_NOT_FOUND", message: "Mainnet agent wallet public key is missing." });
4078
+ }
4079
+ try {
4080
+ const profile = resolveNetworkProfile("mainnet", context.config.profiles);
4081
+ return await fetchMainnetAgentWalletBalances(wallet.publicKey, profile);
4082
+ }
4083
+ catch (error) {
4084
+ throw new StellarAgentError({
4085
+ code: "HORIZON_UNAVAILABLE",
4086
+ message: "Mainnet agent-wallet balance could not be read, so arming and spend fail closed.",
4087
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet",
4088
+ details: error instanceof Error ? error.message : String(error)
4089
+ });
4090
+ }
4091
+ }
4092
+ async function fetchMainnetAgentWalletBalances(publicKey, profile) {
4093
+ if (!profile.horizonUrl) {
4094
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Mainnet Horizon is not configured." });
4095
+ }
4096
+ const response = await fetch(`${profile.horizonUrl.replace(/\/$/, "")}/accounts/${publicKey}`, {
4097
+ signal: AbortSignal.timeout(15_000)
4098
+ });
4099
+ const body = await response.text();
4100
+ const parsed = safeJsonParse(body);
4101
+ if (!response.ok) {
4102
+ throw new StellarAgentError({
4103
+ code: "HORIZON_UNAVAILABLE",
4104
+ message: "Could not load Mainnet agent-wallet balances from Horizon.",
4105
+ details: parsed ?? body
4106
+ });
4107
+ }
4108
+ return (parsed?.balances ?? []).map((balance) => ({
4109
+ asset: balance.asset_type === "native" ? "XLM" : `${balance.asset_code}:${balance.asset_issuer}`,
4110
+ balance: String(balance.balance)
4111
+ }));
4112
+ }
4113
+ async function mainnetAgentWalletPolicyFingerprint(context) {
4114
+ const policyPath = context.options.policy ??
4115
+ process.env.STELLAR_AGENT_POLICY ??
4116
+ join(context.config.storage.policiesDir, defaultPolicyFilename("mainnet"));
4117
+ const resolvedPath = resolvePath(policyPath);
4118
+ let source;
4119
+ try {
4120
+ source = await readFile(resolvedPath, "utf8");
4121
+ }
4122
+ catch (error) {
4123
+ if (error?.code !== "ENOENT")
4124
+ throw error;
4125
+ source = policyToYaml(DEFAULT_MAINNET_POLICY);
4126
+ }
4127
+ return { path: resolvedPath, fingerprint: sha256(source) };
4128
+ }
4129
+ async function mainnetAgentWalletSpendCounters(context, wallet) {
4130
+ const entries = await Promise.all(wallet.riskBudget.allowedAssets.map(async (asset) => [
4131
+ asset,
4132
+ await spendHistoryFromReceipts(context.config.storage.receiptsDir, {
4133
+ profile: "mainnet",
4134
+ asset
4135
+ })
4136
+ ]));
4137
+ return {
4138
+ updatedAt: new Date().toISOString(),
4139
+ source: "receipts",
4140
+ assets: Object.fromEntries(entries)
4141
+ };
4142
+ }
4143
+ function configFingerprintPath(context) {
4144
+ return resolvePath(context.options.config ?? process.env.STELLAR_AGENT_CONFIG ?? join(context.config.storage.rootDir, "config.yaml"));
4145
+ }
4146
+ function sha256(value) {
4147
+ return createHash("sha256").update(value).digest("hex");
4148
+ }
4149
+ function addAmountValues(left, right, asset) {
4150
+ return formatStroops(parseNonnegativeStroops(left, asset) + parseNonnegativeStroops(right, asset));
4151
+ }
4152
+ function parseAssetForPolicy(asset) {
4153
+ if (asset.toUpperCase() === "XLM")
4154
+ return;
4155
+ if (!/^[A-Z0-9]{1,12}(:G[A-Z2-7]{55})?$/.test(asset.toUpperCase())) {
4156
+ throw new StellarAgentError({
4157
+ code: "INVALID_ASSET",
4158
+ message: "Allowed asset must be XLM, CODE, or CODE:G... issuer format.",
4159
+ docs: "docs/mainnet-safety.md#risk-budgeted-mainnet-agent-wallet"
4160
+ });
4161
+ }
4162
+ }
4163
+ function safeJsonParse(value) {
4164
+ try {
4165
+ return JSON.parse(value);
4166
+ }
4167
+ catch {
4168
+ return undefined;
4169
+ }
4170
+ }
3313
4171
  function parseFeeStrategy(value) {
3314
4172
  if (value === "base" || value === "low" || value === "medium" || value === "high" || value === "p95")
3315
4173
  return value;
@@ -3514,6 +4372,44 @@ function printError(options, error) {
3514
4372
  if (serialized.docs)
3515
4373
  process.stderr.write(`Docs: ${serialized.docs}\n`);
3516
4374
  }
4375
+ function commanderErrorToStellarAgentError(error) {
4376
+ if (error instanceof StellarAgentError)
4377
+ return error;
4378
+ if (!isCommanderError(error))
4379
+ return error;
4380
+ const message = normalizeCommanderMessage(error.message);
4381
+ return new StellarAgentError({
4382
+ code: "INVALID_INPUT",
4383
+ message,
4384
+ hint: "Run the command with --help and correct the input.",
4385
+ docs: docsForCommanderMessage(message),
4386
+ exitCode: typeof error.exitCode === "number" ? error.exitCode : EXIT_CODES.usage
4387
+ });
4388
+ }
4389
+ function isCommanderError(error) {
4390
+ return (typeof error === "object" &&
4391
+ error !== null &&
4392
+ "code" in error &&
4393
+ typeof error.code === "string" &&
4394
+ error.code.startsWith("commander.") &&
4395
+ "message" in error &&
4396
+ typeof error.message === "string");
4397
+ }
4398
+ function isCommanderHelpOrVersionExit(error) {
4399
+ return (isCommanderError(error) &&
4400
+ (error.code === "commander.helpDisplayed" || error.code === "commander.version") &&
4401
+ (error.exitCode === undefined || error.exitCode === EXIT_CODES.success));
4402
+ }
4403
+ function normalizeCommanderMessage(message) {
4404
+ return message.replace(/^error:\s*/i, "");
4405
+ }
4406
+ function docsForCommanderMessage(message) {
4407
+ if (message.includes("--slippage-bps"))
4408
+ return "docs/defi-aquarius.md#swap-quoting-and-preflight";
4409
+ if (message.includes("aquarius"))
4410
+ return "docs/defi-aquarius.md";
4411
+ return "docs/troubleshooting.md";
4412
+ }
3517
4413
  function fixtureRequest(amount) {
3518
4414
  return {
3519
4415
  destination: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
@@ -3676,12 +4572,26 @@ async function writeBlendReceipt(context, args) {
3676
4572
  }
3677
4573
  async function writeSubmittedXdrReceipt(context, args) {
3678
4574
  const eventLog = join(context.config.storage.logsDir, "events.jsonl");
4575
+ const operation = args.profile.realFunds && context.config.mainnetAgentWallet?.status === "armed"
4576
+ ? {
4577
+ ...args.operation,
4578
+ details: {
4579
+ ...(typeof args.operation.details === "object" && args.operation.details !== null ? args.operation.details : {}),
4580
+ mainnetAgentWallet: {
4581
+ armed: true,
4582
+ walletName: context.config.mainnetAgentWallet.walletName,
4583
+ publicKey: context.config.mainnetAgentWallet.publicKey,
4584
+ warning: mainnetAgentWalletWarning()
4585
+ }
4586
+ }
4587
+ }
4588
+ : args.operation;
3679
4589
  const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
3680
4590
  command: args.command,
3681
4591
  profile: args.profile.name,
3682
4592
  networkPassphrase: args.profile.networkPassphrase,
3683
4593
  realFunds: args.profile.realFunds,
3684
- operation: args.operation,
4594
+ operation,
3685
4595
  policyDecision: {
3686
4596
  status: args.profile.realFunds ? "requires_approval" : "allowed",
3687
4597
  matchedRules: args.profile.realFunds
@@ -3701,6 +4611,69 @@ async function writeSubmittedXdrReceipt(context, args) {
3701
4611
  });
3702
4612
  return receiptPath;
3703
4613
  }
4614
+ async function writeSubmittedPaymentApprovalReceipt(context, args) {
4615
+ const eventLog = join(context.config.storage.logsDir, "events.jsonl");
4616
+ const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
4617
+ command: args.command,
4618
+ profile: args.profile.name,
4619
+ networkPassphrase: args.profile.networkPassphrase,
4620
+ realFunds: args.profile.realFunds,
4621
+ payment: {
4622
+ source: args.payment.source ?? "",
4623
+ destination: args.payment.destination,
4624
+ asset: args.payment.asset,
4625
+ amount: args.payment.amount,
4626
+ ...(args.payment.memo === undefined ? {} : { memo: args.payment.memo }),
4627
+ ...(args.payment.domain === undefined ? {} : { domain: args.payment.domain }),
4628
+ ...(args.payment.url === undefined ? {} : { url: args.payment.url })
4629
+ },
4630
+ operation: {
4631
+ type: "tx.submit_payment_approval",
4632
+ ...(args.payment.source === undefined ? {} : { source: args.payment.source }),
4633
+ destination: args.payment.destination,
4634
+ asset: args.payment.asset,
4635
+ amount: args.payment.amount,
4636
+ details: {
4637
+ approvalId: args.approvalId,
4638
+ signerPublicKey: args.signerPublicKey,
4639
+ realFundsAcknowledged: args.profile.realFunds,
4640
+ ...(args.mainnetAgentWallet === undefined
4641
+ ? {}
4642
+ : {
4643
+ mainnetAgentWallet: {
4644
+ armed: true,
4645
+ walletName: context.config.mainnetAgentWallet?.walletName,
4646
+ publicKey: context.config.mainnetAgentWallet?.publicKey,
4647
+ riskBudgetState: coreMainnetAgentWalletRiskBudgetState(context.config.mainnetAgentWallet, args.mainnetAgentWallet.spendHistory, args.payment),
4648
+ warning: args.mainnetAgentWallet.warning
4649
+ }
4650
+ })
4651
+ }
4652
+ },
4653
+ policyDecision: {
4654
+ status: args.policyDecision.status,
4655
+ matchedRules: args.policyDecision.matchedRules
4656
+ },
4657
+ transaction: args.transaction,
4658
+ ...(args.transaction.ledger === undefined ? {} : { ledger: { confirmedLedger: args.transaction.ledger } }),
4659
+ eventLog
4660
+ });
4661
+ await appendEvent(eventLog, {
4662
+ event: "receipt_written",
4663
+ status: "success",
4664
+ command: args.command,
4665
+ profile: args.profile.name,
4666
+ data: { receiptPath }
4667
+ });
4668
+ if (context.config.mainnetAgentWallet && args.mainnetAgentWallet) {
4669
+ context.config.mainnetAgentWallet = {
4670
+ ...context.config.mainnetAgentWallet,
4671
+ spendCounters: await mainnetAgentWalletSpendCounters(context, context.config.mainnetAgentWallet)
4672
+ };
4673
+ await writeConfig(context.config, context.options);
4674
+ }
4675
+ return receiptPath;
4676
+ }
3704
4677
  async function attachContractSubmissionReceipt(context, args) {
3705
4678
  if (args.network.toLowerCase() !== "testnet")
3706
4679
  return args.result;
@@ -3755,6 +4728,121 @@ async function buildLocalReport(context) {
3755
4728
  }
3756
4729
  };
3757
4730
  }
4731
+ async function receiptSummary(context, options = {}) {
4732
+ const paths = await listReceipts(context.config.storage.receiptsDir);
4733
+ const summary = {
4734
+ receiptCount: 0,
4735
+ unreadableCount: 0,
4736
+ paymentCount: 0,
4737
+ realFundsCount: 0,
4738
+ totals: {},
4739
+ profiles: {},
4740
+ latest: null
4741
+ };
4742
+ for (const path of paths) {
4743
+ try {
4744
+ const receipt = await readReceipt(path);
4745
+ if (options.profile && receipt.profile !== options.profile)
4746
+ continue;
4747
+ if (options.asset && receipt.payment?.asset.toUpperCase() !== options.asset.toUpperCase())
4748
+ continue;
4749
+ summary.receiptCount += 1;
4750
+ const profile = (summary.profiles[receipt.profile] ??= { count: 0, realFundsCount: 0 });
4751
+ profile.count += 1;
4752
+ if (receipt.network.realFunds) {
4753
+ summary.realFundsCount += 1;
4754
+ profile.realFundsCount += 1;
4755
+ }
4756
+ if (!summary.latest) {
4757
+ summary.latest = {
4758
+ path,
4759
+ id: receipt.id,
4760
+ command: receipt.command,
4761
+ createdAt: receipt.createdAt,
4762
+ profile: receipt.profile
4763
+ };
4764
+ }
4765
+ if (!receipt.payment || receipt.transaction.successful !== true || receipt.policyDecision.status === "denied")
4766
+ continue;
4767
+ summary.paymentCount += 1;
4768
+ const key = `${receipt.profile}:${receipt.payment.asset.toUpperCase()}`;
4769
+ const current = summary.totals[key] ?? { amount: "0.0000000", count: 0 };
4770
+ summary.totals[key] = {
4771
+ amount: addAmountValues(current.amount, receipt.payment.amount, receipt.payment.asset),
4772
+ count: current.count + 1
4773
+ };
4774
+ }
4775
+ catch {
4776
+ summary.unreadableCount += 1;
4777
+ }
4778
+ }
4779
+ return summary;
4780
+ }
4781
+ function x402ServerScaffoldFiles() {
4782
+ return {
4783
+ "package.json": `${JSON.stringify({
4784
+ type: "module",
4785
+ scripts: {
4786
+ start: "node server.mjs"
4787
+ },
4788
+ dependencies: {}
4789
+ }, null, 2)}\n`,
4790
+ "server.mjs": `import { createServer } from "node:http";
4791
+
4792
+ const port = Number(process.env.PORT ?? 8787);
4793
+ const price = process.env.X402_PRICE ?? "0.0000001 XLM";
4794
+ const destination = process.env.X402_DESTINATION ?? "set-testnet-merchant-public-key";
4795
+
4796
+ const server = createServer((request, response) => {
4797
+ if (request.url === "/health") {
4798
+ response.writeHead(200, { "content-type": "application/json" });
4799
+ response.end(JSON.stringify({ ok: true }));
4800
+ return;
4801
+ }
4802
+
4803
+ const payment = request.headers["x-payment"];
4804
+ if (!payment) {
4805
+ response.writeHead(402, {
4806
+ "content-type": "application/json",
4807
+ "x-accepts-payment": JSON.stringify({
4808
+ network: "testnet",
4809
+ asset: "XLM",
4810
+ amount: price,
4811
+ destination
4812
+ })
4813
+ });
4814
+ response.end(JSON.stringify({ error: "payment_required", network: "testnet", price, destination }));
4815
+ return;
4816
+ }
4817
+
4818
+ response.writeHead(200, { "content-type": "application/json" });
4819
+ response.end(JSON.stringify({ ok: true, paid: true, message: "Replace this demo verifier before production." }));
4820
+ });
4821
+
4822
+ server.listen(port, () => {
4823
+ console.log(\`x402 demo server listening on http://127.0.0.1:\${port}\`);
4824
+ });
4825
+ `,
4826
+ "README.md": `# Stellar Agent x402 Testnet Server
4827
+
4828
+ This scaffold is a local Testnet demo target for \`stellar-agent pay x402\`.
4829
+
4830
+ Run it:
4831
+
4832
+ \`\`\`sh
4833
+ npm start
4834
+ \`\`\`
4835
+
4836
+ Configure the destination with a Testnet merchant public key:
4837
+
4838
+ \`\`\`sh
4839
+ X402_DESTINATION=G... npm start
4840
+ \`\`\`
4841
+
4842
+ This scaffold does not verify payments. Replace the demo verifier before using it outside a local Testnet experiment.
4843
+ `
4844
+ };
4845
+ }
3758
4846
  function enableX402ForUrl(policy, url) {
3759
4847
  const host = new URL(url).host;
3760
4848
  if (!/^localhost(?::\d+)?$/.test(host) && !/^127\.0\.0\.1(?::\d+)?$/.test(host)) {