@stellar-agent/cli 0.4.4 → 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";
@@ -62,6 +63,7 @@ Common commands:
62
63
  addApprovalCommands(program);
63
64
  addTransactionCommands(program);
64
65
  addPayCommands(program);
66
+ addX402Commands(program);
65
67
  addClaimableCommands(program);
66
68
  addContractCommands(program);
67
69
  addMarketCommands(program);
@@ -712,6 +714,7 @@ function addTransactionCommands(program) {
712
714
  const policyDecision = evaluatePaymentRequest(policy, request, history);
713
715
  if (policyDecision.status === "denied")
714
716
  throw policyDeniedError();
717
+ const mainnetAgentWallet = await assertMainnetAgentWalletPaymentAllowed(context, request);
715
718
  if (profile.realFunds && policyDecision.status === "requires_approval") {
716
719
  throw new StellarAgentError({
717
720
  code: "APPROVAL_REQUIRED",
@@ -738,7 +741,13 @@ function addTransactionCommands(program) {
738
741
  profile: profile.name,
739
742
  data: { source: sourcePublicKey, destination: options.to, asset: options.asset, amount: built.amount, policyDecision }
740
743
  });
741
- 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
+ };
742
751
  }, "Unsigned payment transaction built."));
743
752
  tx
744
753
  .command("request-payment-signature")
@@ -775,6 +784,7 @@ function addTransactionCommands(program) {
775
784
  const policyDecision = evaluatePaymentRequest(policy, request, history);
776
785
  if (policyDecision.status === "denied")
777
786
  throw policyDeniedError();
787
+ const mainnetAgentWallet = await assertMainnetAgentWalletPaymentAllowed(context, request);
778
788
  const built = await buildPaymentTransactionXdr({
779
789
  sourcePublicKey,
780
790
  destination: options.to,
@@ -790,7 +800,8 @@ function addTransactionCommands(program) {
790
800
  approvalsDir: context.config.storage.approvalsDir,
791
801
  network: profile.name,
792
802
  transactionXdr: built.xdr,
793
- 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
794
805
  });
795
806
  await appendEvent(join(context.config.storage.logsDir, "events.jsonl"), {
796
807
  event: "approval_requested",
@@ -800,7 +811,14 @@ function addTransactionCommands(program) {
800
811
  requestId: approval.id,
801
812
  data: { approval, source: sourcePublicKey, destination: options.to, asset: options.asset, amount: built.amount, policyDecision }
802
813
  });
803
- 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
+ };
804
822
  }, "Payment signature approval request created."));
805
823
  tx
806
824
  .command("submit-xdr")
@@ -879,6 +897,10 @@ function addTransactionCommands(program) {
879
897
  acknowledgeRealFunds: Boolean(options.iUnderstandRealFunds),
880
898
  action: "signed approval submission"
881
899
  });
900
+ const approvalPayment = approval.payment;
901
+ const approvedPayment = approvalPayment === undefined
902
+ ? undefined
903
+ : await evaluateApprovedPaymentBeforeSubmission(context, approvalPayment);
882
904
  const { submitTransactionXdr } = await import("@stellar-agent/stellar");
883
905
  const transaction = await submitTransactionXdr({
884
906
  xdr: approval.decision.signedTransactionXdr,
@@ -897,19 +919,38 @@ function addTransactionCommands(program) {
897
919
  transaction
898
920
  }
899
921
  });
900
- const receiptPath = await writeSubmittedXdrReceipt(context, {
901
- command: "tx submit-approval",
902
- profile,
903
- operation: {
904
- type: "tx.submit_approval",
905
- details: {
906
- approvalId: approval.id,
907
- signerPublicKey: approval.decision.signerPublicKey,
908
- realFundsAcknowledged: profile.realFunds
909
- }
910
- },
911
- transaction
912
- });
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
+ }
913
954
  return { approvalId: approval.id, signerPublicKey: approval.decision.signerPublicKey, transaction, receiptPath, realFunds: profile.realFunds };
914
955
  }, "Signed approval transaction submitted."));
915
956
  }
@@ -950,7 +991,7 @@ function addPayCommands(program) {
950
991
  }, "Payment quote complete."));
951
992
  pay
952
993
  .command("send")
953
- .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.")
954
995
  .requiredOption("--to <address>", "Destination public key")
955
996
  .requiredOption("--amount <amount>", "Payment amount")
956
997
  .option("--asset <asset>", "Asset", "XLM")
@@ -958,14 +999,13 @@ function addPayCommands(program) {
958
999
  .option("--memo <memo>", "Memo")
959
1000
  .option("--fee-strategy <strategy>", "Fee strategy: base, low, medium, high, p95", "medium")
960
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")
961
1005
  .option("--dry-run", "Evaluate locally without submitting")
962
1006
  .action(withContext(async (context, options) => {
963
1007
  if (context.profileName === "mainnet") {
964
- throw new StellarAgentError({
965
- code: "MAINNET_NOT_ENABLED",
966
- message: "Mainnet payment submission is blocked in v0.",
967
- docs: "docs/mainnet-safety.md"
968
- });
1008
+ return sendMainnetAgentWalletPayment(context, options);
969
1009
  }
970
1010
  const policy = await loadPolicy(context);
971
1011
  const source = await loadWallet(context.config, options.from);
@@ -1233,6 +1273,45 @@ function addPayCommands(program) {
1233
1273
  });
1234
1274
  }, "MPP session complete."));
1235
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
+ }
1236
1315
  function addClaimableCommands(program) {
1237
1316
  const claimable = program.command("claimable").description("Create, list, and claim claimable balances.");
1238
1317
  claimable
@@ -2582,6 +2661,15 @@ function addReceiptCommands(program) {
2582
2661
  return { receipt: null };
2583
2662
  return latest;
2584
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."));
2585
2673
  receipts
2586
2674
  .command("show")
2587
2675
  .description("Show a receipt file.")
@@ -2654,6 +2742,228 @@ function addMainnetCommands(program) {
2654
2742
  "Receipts must mark realFunds: true."
2655
2743
  ]
2656
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."));
2657
2967
  }
2658
2968
  function withContext(handler, humanMessage) {
2659
2969
  return async (...args) => {
@@ -3361,6 +3671,503 @@ async function loadSpendHistory(context, request) {
3361
3671
  asset: request.asset
3362
3672
  });
3363
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
+ }
3364
4171
  function parseFeeStrategy(value) {
3365
4172
  if (value === "base" || value === "low" || value === "medium" || value === "high" || value === "p95")
3366
4173
  return value;
@@ -3765,12 +4572,26 @@ async function writeBlendReceipt(context, args) {
3765
4572
  }
3766
4573
  async function writeSubmittedXdrReceipt(context, args) {
3767
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;
3768
4589
  const { path: receiptPath } = await writeReceipt(context.config.storage.receiptsDir, {
3769
4590
  command: args.command,
3770
4591
  profile: args.profile.name,
3771
4592
  networkPassphrase: args.profile.networkPassphrase,
3772
4593
  realFunds: args.profile.realFunds,
3773
- operation: args.operation,
4594
+ operation,
3774
4595
  policyDecision: {
3775
4596
  status: args.profile.realFunds ? "requires_approval" : "allowed",
3776
4597
  matchedRules: args.profile.realFunds
@@ -3790,6 +4611,69 @@ async function writeSubmittedXdrReceipt(context, args) {
3790
4611
  });
3791
4612
  return receiptPath;
3792
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
+ }
3793
4677
  async function attachContractSubmissionReceipt(context, args) {
3794
4678
  if (args.network.toLowerCase() !== "testnet")
3795
4679
  return args.result;
@@ -3844,6 +4728,121 @@ async function buildLocalReport(context) {
3844
4728
  }
3845
4729
  };
3846
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
+ }
3847
4846
  function enableX402ForUrl(policy, url) {
3848
4847
  const host = new URL(url).host;
3849
4848
  if (!/^localhost(?::\d+)?$/.test(host) && !/^127\.0\.0\.1(?::\d+)?$/.test(host)) {