@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/README.md +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1023 -24
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
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 {
|
|
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 {
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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)) {
|