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