@stellar-agent/stellar 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1547 @@
1
+ import { EXIT_CODES, StellarAgentError, TESTNET_PROFILE, formatStroops, parseAmount, parseAsset, redactWallet } from "@stellar-agent/core";
2
+ import { Account, Asset, BASE_FEE, Claimant, Horizon, Keypair, LiquidityPoolAsset, LiquidityPoolFeeV18, Memo, Operation, TransactionBuilder, getLiquidityPoolId, xdr } from "@stellar/stellar-sdk";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ const execFileAsync = promisify(execFile);
6
+ const sessionCache = new Map();
7
+ export function clearStellarSessionCache() {
8
+ sessionCache.clear();
9
+ }
10
+ export function stellarSessionCacheSnapshot(now = Date.now()) {
11
+ return Array.from(sessionCache.entries()).map(([key, entry]) => ({
12
+ key,
13
+ expiresAt: new Date(entry.expiresAt).toISOString(),
14
+ ttlMs: Math.max(0, entry.expiresAt - now)
15
+ }));
16
+ }
17
+ export function resolveNetworkProfile(profileName, profiles) {
18
+ const profile = profiles[profileName];
19
+ if (!profile) {
20
+ throw new StellarAgentError({
21
+ code: "PROFILE_NOT_FOUND",
22
+ message: `Profile '${profileName}' was not found.`,
23
+ hint: "Run stellar-agent profile list to see configured profiles."
24
+ });
25
+ }
26
+ return profile;
27
+ }
28
+ export function createTestnetWallet(name) {
29
+ const pair = Keypair.random();
30
+ return {
31
+ schemaVersion: "stellar-agent.wallet.v1",
32
+ name,
33
+ network: "testnet",
34
+ publicKey: pair.publicKey(),
35
+ secretKey: pair.secret(),
36
+ createdAt: new Date().toISOString(),
37
+ source: "generated-testnet"
38
+ };
39
+ }
40
+ export function publicWalletView(wallet) {
41
+ return redactWallet(wallet);
42
+ }
43
+ export async function fundWithFriendbot(address, profile = TESTNET_PROFILE, fetchImpl = fetch) {
44
+ if (!profile.friendbotUrl) {
45
+ throw new StellarAgentError({
46
+ code: "FRIENDBOT_UNAVAILABLE",
47
+ message: "Friendbot is not configured for this profile.",
48
+ docs: "docs/troubleshooting.md#friendbot-unavailable"
49
+ });
50
+ }
51
+ const url = new URL(profile.friendbotUrl);
52
+ url.searchParams.set("addr", address);
53
+ let lastError;
54
+ for (let attempt = 0; attempt < 2; attempt += 1) {
55
+ try {
56
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
57
+ const body = await response.text();
58
+ if (response.ok) {
59
+ const parsed = safeJson(body);
60
+ return {
61
+ funded: true,
62
+ hash: parsed?.hash ?? parsed?._links?.transaction?.href?.split("/").pop()
63
+ };
64
+ }
65
+ if (/already|exist|funded/i.test(body)) {
66
+ return { funded: true, alreadyFunded: true };
67
+ }
68
+ lastError = body;
69
+ }
70
+ catch (error) {
71
+ lastError = error;
72
+ }
73
+ }
74
+ throw new StellarAgentError({
75
+ code: "FRIENDBOT_UNAVAILABLE",
76
+ message: "Could not fund the Testnet account with Friendbot.",
77
+ hint: "Check your internet connection or try again later.",
78
+ docs: "docs/troubleshooting.md#friendbot-unavailable",
79
+ details: String(lastError)
80
+ });
81
+ }
82
+ export async function getBalances(address, profile = TESTNET_PROFILE) {
83
+ if (!profile.horizonUrl) {
84
+ throw new StellarAgentError({
85
+ code: "HORIZON_UNAVAILABLE",
86
+ message: "Horizon is not configured for this profile.",
87
+ docs: "docs/troubleshooting.md#rpc-unavailable"
88
+ });
89
+ }
90
+ const server = new Horizon.Server(profile.horizonUrl);
91
+ try {
92
+ const account = await server.loadAccount(address);
93
+ return account.balances.map((balance) => ({
94
+ asset: balance.asset_type === "native" ? "XLM" : `${balance.asset_code}:${balance.asset_issuer}`,
95
+ balance: balance.balance
96
+ }));
97
+ }
98
+ catch (error) {
99
+ throw new StellarAgentError({
100
+ code: "HORIZON_UNAVAILABLE",
101
+ message: "Could not load account balances from Horizon.",
102
+ hint: "Confirm the account exists and Horizon is reachable.",
103
+ docs: "docs/troubleshooting.md#rpc-unavailable",
104
+ details: String(error)
105
+ });
106
+ }
107
+ }
108
+ export async function sendNativePayment(args) {
109
+ return sendPayment({ ...args, asset: "XLM" });
110
+ }
111
+ export async function sendPayment(args) {
112
+ const profile = args.profile ?? TESTNET_PROFILE;
113
+ const amount = parseAmount(args.amount).value;
114
+ const asset = stellarSdkAsset(args.asset ?? "XLM");
115
+ try {
116
+ return await submitOperation({
117
+ source: args.source,
118
+ profile,
119
+ ...(args.memo === undefined ? {} : { memo: args.memo }),
120
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
121
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
122
+ operation: Operation.payment({
123
+ destination: args.destination,
124
+ asset,
125
+ amount
126
+ })
127
+ });
128
+ }
129
+ catch (error) {
130
+ throw new StellarAgentError({
131
+ code: "TRANSACTION_SUBMIT_FAILED",
132
+ message: "Stellar payment could not be submitted or confirmed.",
133
+ hint: "Check wallet funding, destination address, and Horizon availability.",
134
+ docs: "docs/troubleshooting.md#transaction-timeout",
135
+ details: String(error)
136
+ });
137
+ }
138
+ }
139
+ export async function changeTrustline(args) {
140
+ const asset = stellarSdkAsset(args.asset);
141
+ if (asset.isNative()) {
142
+ throw new StellarAgentError({
143
+ code: "INVALID_ASSET",
144
+ message: "Native XLM does not use trustlines.",
145
+ docs: "docs/quickstart-testnet.md#trustlines"
146
+ });
147
+ }
148
+ const limit = normalizeTrustlineLimit(args.limit ?? "922337203685.4775807");
149
+ const submitted = await submitOperation({
150
+ source: args.source,
151
+ profile: args.profile ?? TESTNET_PROFILE,
152
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
153
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
154
+ operation: Operation.changeTrust({ asset, limit })
155
+ });
156
+ return { ...submitted, asset: args.asset.toUpperCase(), limit };
157
+ }
158
+ export async function removeTrustline(args) {
159
+ const submitted = await changeTrustline({
160
+ source: args.source,
161
+ asset: args.asset,
162
+ limit: "0",
163
+ ...(args.profile === undefined ? {} : { profile: args.profile })
164
+ });
165
+ return { ...submitted, limit: "0" };
166
+ }
167
+ export async function createClaimableBalance(args) {
168
+ const profile = args.profile ?? TESTNET_PROFILE;
169
+ const asset = stellarSdkAsset(args.asset ?? "XLM");
170
+ const amount = parseAmount(args.amount, args.asset ?? "XLM").value;
171
+ const claimants = resolveClaimableBalanceClaimants(args.claimant, args.claimants);
172
+ const claimantPredicate = buildClaimableBalancePredicate({
173
+ ...(args.claimableAfter === undefined ? {} : { claimableAfter: args.claimableAfter }),
174
+ ...(args.claimableBefore === undefined ? {} : { claimableBefore: args.claimableBefore })
175
+ });
176
+ const submitted = await submitOperation({
177
+ source: args.source,
178
+ profile,
179
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
180
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
181
+ operation: Operation.createClaimableBalance({
182
+ asset,
183
+ amount,
184
+ claimants: claimants.map((claimant) => new Claimant(claimant, claimantPredicate.predicate))
185
+ })
186
+ });
187
+ const claimableBalanceEntries = await Promise.all(claimants.map(async (claimant) => [claimant, await listClaimableBalances(claimant, profile)]));
188
+ const claimableBalancesByClaimant = Object.fromEntries(claimableBalanceEntries);
189
+ return {
190
+ ...submitted,
191
+ asset: args.asset ?? "XLM",
192
+ amount,
193
+ claimant: args.claimant,
194
+ claimants,
195
+ predicate: claimantPredicate.summary,
196
+ claimableBalances: claimableBalancesByClaimant[args.claimant] ?? [],
197
+ claimableBalancesByClaimant
198
+ };
199
+ }
200
+ export function buildClaimableBalancePredicate(options = {}) {
201
+ const claimableAfter = options.claimableAfter === undefined ? undefined : parseClaimableTimestamp(options.claimableAfter, "claimableAfter");
202
+ const claimableBefore = options.claimableBefore === undefined ? undefined : parseClaimableTimestamp(options.claimableBefore, "claimableBefore");
203
+ if (claimableAfter && claimableBefore && BigInt(claimableAfter.epochSeconds) >= BigInt(claimableBefore.epochSeconds)) {
204
+ throw new StellarAgentError({
205
+ code: "INVALID_INPUT",
206
+ message: "claimableAfter must be earlier than claimableBefore.",
207
+ docs: "docs/claimable-balances.md#time-bound-claiming"
208
+ });
209
+ }
210
+ if (claimableAfter && claimableBefore) {
211
+ return {
212
+ predicate: Claimant.predicateAnd(Claimant.predicateNot(Claimant.predicateBeforeAbsoluteTime(claimableAfter.epochSeconds)), Claimant.predicateBeforeAbsoluteTime(claimableBefore.epochSeconds)),
213
+ summary: { type: "time_window", claimableAfter, claimableBefore }
214
+ };
215
+ }
216
+ if (claimableAfter) {
217
+ return {
218
+ predicate: Claimant.predicateNot(Claimant.predicateBeforeAbsoluteTime(claimableAfter.epochSeconds)),
219
+ summary: { type: "not_before_absolute_time", claimableAfter }
220
+ };
221
+ }
222
+ if (claimableBefore) {
223
+ return {
224
+ predicate: Claimant.predicateBeforeAbsoluteTime(claimableBefore.epochSeconds),
225
+ summary: { type: "before_absolute_time", claimableBefore }
226
+ };
227
+ }
228
+ return {
229
+ predicate: Claimant.predicateUnconditional(),
230
+ summary: { type: "unconditional" }
231
+ };
232
+ }
233
+ export function resolveClaimableBalanceClaimants(claimant, claimants = []) {
234
+ return uniqueClaimants([claimant, ...claimants]);
235
+ }
236
+ export async function claimClaimableBalance(args) {
237
+ const submitted = await submitOperation({
238
+ source: args.source,
239
+ profile: args.profile ?? TESTNET_PROFILE,
240
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
241
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
242
+ operation: Operation.claimClaimableBalance({ balanceId: args.balanceId })
243
+ });
244
+ return { ...submitted, balanceId: args.balanceId };
245
+ }
246
+ export async function listLiquidityPools(args = {}) {
247
+ const profile = args.profile ?? TESTNET_PROFILE;
248
+ if (!profile.horizonUrl) {
249
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
250
+ }
251
+ const url = new URL(`${profile.horizonUrl.replace(/\/$/, "")}/liquidity_pools`);
252
+ if (args.assetA && args.assetB) {
253
+ url.searchParams.set("reserves", `${stellarSdkAsset(args.assetA).toString()},${stellarSdkAsset(args.assetB).toString()}`);
254
+ }
255
+ if (args.account)
256
+ url.searchParams.set("account", args.account);
257
+ url.searchParams.set("order", "desc");
258
+ url.searchParams.set("limit", String(args.limit ?? 10));
259
+ const page = await horizonGet(url, "Could not list liquidity pools from Horizon.");
260
+ return collectionResult(horizonRecords(page).map(normalizeLiquidityPoolRecord), page);
261
+ }
262
+ export async function inspectLiquidityPool(args) {
263
+ const profile = args.profile ?? TESTNET_PROFILE;
264
+ if (!profile.horizonUrl) {
265
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
266
+ }
267
+ try {
268
+ return normalizeLiquidityPoolRecord(await horizonGet(new URL(`${profile.horizonUrl.replace(/\/$/, "")}/liquidity_pools/${args.poolId}`), "Could not inspect liquidity pool from Horizon."));
269
+ }
270
+ catch (error) {
271
+ if (error instanceof StellarAgentError)
272
+ throw error;
273
+ throw new StellarAgentError({
274
+ code: "LEDGER_LOOKUP_FAILED",
275
+ message: "Could not inspect liquidity pool from Horizon.",
276
+ docs: "docs/market-liquidity.md#core-pool-inspection",
277
+ details: String(error)
278
+ });
279
+ }
280
+ }
281
+ export async function liquidityPoolTrades(args) {
282
+ const profile = args.profile ?? TESTNET_PROFILE;
283
+ if (!profile.horizonUrl) {
284
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
285
+ }
286
+ const url = new URL(`${profile.horizonUrl.replace(/\/$/, "")}/liquidity_pools/${args.poolId}/trades`);
287
+ url.searchParams.set("order", "desc");
288
+ url.searchParams.set("limit", String(args.limit ?? 10));
289
+ const page = await horizonGet(url, "Could not fetch liquidity pool trades from Horizon.");
290
+ return collectionResult(horizonRecords(page), page);
291
+ }
292
+ export async function inspectLiquidityPoolPosition(args) {
293
+ const profile = args.profile ?? TESTNET_PROFILE;
294
+ if (!profile.horizonUrl) {
295
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
296
+ }
297
+ const account = await horizonGet(new URL(`${profile.horizonUrl.replace(/\/$/, "")}/accounts/${args.account}`), "Could not load account liquidity pool positions from Horizon.");
298
+ const poolShareBalances = account.balances.filter((balance) => balance.asset_type === "liquidity_pool_shares");
299
+ const positions = await Promise.all(poolShareBalances
300
+ .filter((balance) => !args.poolId || balance.liquidity_pool_id === args.poolId)
301
+ .map(async (balance) => {
302
+ const pool = await inspectLiquidityPool({ poolId: balance.liquidity_pool_id, profile });
303
+ const shareOfPool = liquidityPoolShareRatio(balance.balance, pool.totalShares);
304
+ return {
305
+ account: args.account,
306
+ poolId: balance.liquidity_pool_id,
307
+ shares: balance.balance,
308
+ ...(balance.limit === undefined ? {} : { limit: balance.limit }),
309
+ ...(shareOfPool === undefined ? {} : { shareOfPool }),
310
+ ...(shareOfPool === undefined ? {} : { estimatedReserves: estimatePoolReserves(pool, shareOfPool) }),
311
+ pool
312
+ };
313
+ }));
314
+ return { account: args.account, positions };
315
+ }
316
+ export async function preflightLiquidityPoolDeposit(args) {
317
+ const profile = args.profile ?? TESTNET_PROFILE;
318
+ const pool = await inspectLiquidityPool({ poolId: args.poolId, profile });
319
+ const reserves = requireTwoPoolReserves(pool);
320
+ const maxAmountA = parseAmount(args.maxAmountA, reserves[0].asset).value;
321
+ const maxAmountB = parseAmount(args.maxAmountB, reserves[1].asset).value;
322
+ const currentPrice = poolPrice(pool);
323
+ const estimatedShares = estimateDepositShares(pool, maxAmountA, maxAmountB);
324
+ return {
325
+ action: "deposit",
326
+ pool,
327
+ ...(args.account === undefined ? {} : { account: args.account }),
328
+ assets: reserves.map((reserve) => reserve.asset),
329
+ ...(currentPrice === undefined ? {} : { currentPrice }),
330
+ ...(args.account === undefined ? {} : { trustlines: await liquidityPoolTrustlineStatus(args.account, pool, profile) }),
331
+ deposit: {
332
+ maxAmountA,
333
+ maxAmountB,
334
+ minPrice: args.minPrice,
335
+ maxPrice: args.maxPrice,
336
+ ...(estimatedShares === undefined ? {} : { estimatedShares })
337
+ },
338
+ risk: liquidityPoolRiskNotes(pool)
339
+ };
340
+ }
341
+ export async function preflightLiquidityPoolWithdraw(args) {
342
+ const profile = args.profile ?? TESTNET_PROFILE;
343
+ const pool = await inspectLiquidityPool({ poolId: args.poolId, profile });
344
+ const reserves = requireTwoPoolReserves(pool);
345
+ const shares = parseAmount(args.shares, "pool_shares").value;
346
+ const shareOfPool = liquidityPoolShareRatio(shares, pool.totalShares);
347
+ return {
348
+ action: "withdraw",
349
+ pool,
350
+ ...(args.account === undefined ? {} : { account: args.account }),
351
+ assets: reserves.map((reserve) => reserve.asset),
352
+ ...(args.account === undefined ? {} : { trustlines: await liquidityPoolTrustlineStatus(args.account, pool, profile) }),
353
+ withdraw: {
354
+ shares,
355
+ minAmountA: normalizeAmountAllowZero(args.minAmountA),
356
+ minAmountB: normalizeAmountAllowZero(args.minAmountB),
357
+ ...(shareOfPool === undefined ? {} : { estimatedReserves: estimatePoolReserves(pool, shareOfPool) })
358
+ },
359
+ risk: liquidityPoolRiskNotes(pool)
360
+ };
361
+ }
362
+ export async function changeLiquidityPoolTrustline(args) {
363
+ const poolAsset = await resolveLiquidityPoolAssetForTrustline(args);
364
+ const limit = normalizeTrustlineLimit(args.limit ?? "922337203685.4775807");
365
+ const submitted = await submitOperation({
366
+ source: args.source,
367
+ profile: args.profile ?? TESTNET_PROFILE,
368
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
369
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
370
+ operation: Operation.changeTrust({ asset: poolAsset.asset, limit })
371
+ });
372
+ return { ...submitted, poolId: poolAsset.poolId, limit };
373
+ }
374
+ export async function submitLiquidityPoolDeposit(args) {
375
+ const profile = args.profile ?? TESTNET_PROFILE;
376
+ const preflight = await preflightLiquidityPoolDeposit({
377
+ poolId: args.poolId,
378
+ maxAmountA: args.maxAmountA,
379
+ maxAmountB: args.maxAmountB,
380
+ minPrice: args.minPrice,
381
+ maxPrice: args.maxPrice,
382
+ account: args.source.publicKey,
383
+ profile
384
+ });
385
+ const submitted = await submitOperation({
386
+ source: args.source,
387
+ profile,
388
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
389
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
390
+ operation: Operation.liquidityPoolDeposit({
391
+ liquidityPoolId: args.poolId,
392
+ maxAmountA: preflight.deposit.maxAmountA,
393
+ maxAmountB: preflight.deposit.maxAmountB,
394
+ minPrice: args.minPrice,
395
+ maxPrice: args.maxPrice
396
+ })
397
+ });
398
+ return { ...submitted, preflight };
399
+ }
400
+ export async function submitLiquidityPoolWithdraw(args) {
401
+ const profile = args.profile ?? TESTNET_PROFILE;
402
+ const preflight = await preflightLiquidityPoolWithdraw({
403
+ poolId: args.poolId,
404
+ shares: args.shares,
405
+ minAmountA: args.minAmountA,
406
+ minAmountB: args.minAmountB,
407
+ account: args.source.publicKey,
408
+ profile
409
+ });
410
+ const submitted = await submitOperation({
411
+ source: args.source,
412
+ profile,
413
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
414
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
415
+ operation: Operation.liquidityPoolWithdraw({
416
+ liquidityPoolId: args.poolId,
417
+ amount: preflight.withdraw.shares,
418
+ minAmountA: preflight.withdraw.minAmountA,
419
+ minAmountB: preflight.withdraw.minAmountB
420
+ })
421
+ });
422
+ return { ...submitted, preflight };
423
+ }
424
+ export async function buildPaymentTransactionXdr(args) {
425
+ const profile = args.profile ?? TESTNET_PROFILE;
426
+ if (profile.realFunds && !args.allowRealFunds) {
427
+ throw new StellarAgentError({
428
+ code: "MAINNET_NOT_ENABLED",
429
+ message: "Mainnet payment-XDR building requires explicit real-funds approval.",
430
+ docs: "docs/mainnet-safety.md"
431
+ });
432
+ }
433
+ if (!profile.horizonUrl && !args.sourceSequence) {
434
+ throw new StellarAgentError({
435
+ code: "HORIZON_UNAVAILABLE",
436
+ message: "Horizon is not configured for this profile."
437
+ });
438
+ }
439
+ const amount = parseAmount(args.amount, args.asset ?? "XLM").value;
440
+ const asset = stellarSdkAsset(args.asset ?? "XLM");
441
+ const sourceAccount = args.sourceSequence
442
+ ? new Account(args.sourcePublicKey, args.sourceSequence)
443
+ : await new Horizon.Server(profile.horizonUrl).loadAccount(args.sourcePublicKey);
444
+ const fee = await estimateTransactionFee({
445
+ profile,
446
+ operationCount: 1,
447
+ strategy: args.feeStrategy ?? "medium",
448
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
449
+ });
450
+ let builder = new TransactionBuilder(sourceAccount, {
451
+ fee: fee.perOperationFee,
452
+ networkPassphrase: profile.networkPassphrase
453
+ }).addOperation(Operation.payment({
454
+ destination: args.destination,
455
+ asset,
456
+ amount
457
+ }));
458
+ if (args.memo)
459
+ builder = builder.addMemo(Memo.text(args.memo));
460
+ const transaction = builder.setTimeout(60).build();
461
+ return {
462
+ xdr: transaction.toXDR(),
463
+ source: args.sourcePublicKey,
464
+ destination: args.destination,
465
+ amount,
466
+ asset: args.asset ?? "XLM",
467
+ networkPassphrase: profile.networkPassphrase,
468
+ fee
469
+ };
470
+ }
471
+ export async function sendPaymentBatch(args) {
472
+ const payments = normalizeBatchPayments(args.payments);
473
+ const operations = payments.map((payment) => Operation.payment({
474
+ destination: payment.destination,
475
+ asset: stellarSdkAsset(payment.asset),
476
+ amount: payment.amount
477
+ }));
478
+ const submitted = await submitOperation({
479
+ source: args.source,
480
+ profile: args.profile ?? TESTNET_PROFILE,
481
+ operations,
482
+ ...(args.memo === undefined ? {} : { memo: args.memo }),
483
+ ...(args.feeStrategy === undefined ? {} : { feeStrategy: args.feeStrategy }),
484
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
485
+ });
486
+ return {
487
+ ...submitted,
488
+ operationCount: payments.length,
489
+ payments
490
+ };
491
+ }
492
+ export async function submitTransactionXdr(args) {
493
+ const profile = args.profile ?? TESTNET_PROFILE;
494
+ if (profile.realFunds && !args.allowRealFunds) {
495
+ throw new StellarAgentError({
496
+ code: "MAINNET_NOT_ENABLED",
497
+ message: "Mainnet signed-XDR submission requires explicit real-funds approval.",
498
+ hint: "Use a signed transaction from a human-controlled Mainnet wallet.",
499
+ docs: "docs/mainnet-safety.md"
500
+ });
501
+ }
502
+ if (!profile.horizonUrl) {
503
+ throw new StellarAgentError({
504
+ code: "HORIZON_UNAVAILABLE",
505
+ message: "Horizon is not configured for this profile."
506
+ });
507
+ }
508
+ const xdr = args.xdr.trim();
509
+ if (!xdr) {
510
+ throw new StellarAgentError({
511
+ code: "INVALID_INPUT",
512
+ message: "Signed transaction XDR is required.",
513
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
514
+ });
515
+ }
516
+ if (profile.realFunds)
517
+ assertSignedTransactionXdr(xdr);
518
+ const fetchImpl = args.fetchImpl ?? fetch;
519
+ const response = await fetchImpl(`${profile.horizonUrl.replace(/\/$/, "")}/transactions`, {
520
+ method: "POST",
521
+ body: new URLSearchParams({ tx: xdr }),
522
+ signal: AbortSignal.timeout(30_000)
523
+ });
524
+ const body = await response.text();
525
+ const parsed = safeJson(body);
526
+ if (!response.ok) {
527
+ throw new StellarAgentError({
528
+ code: "TRANSACTION_SUBMIT_FAILED",
529
+ message: "Signed transaction XDR could not be submitted to Horizon.",
530
+ hint: "Confirm the transaction is signed for the selected network and has not already been submitted.",
531
+ docs: "docs/troubleshooting.md#transaction-timeout",
532
+ details: parsed ?? body
533
+ });
534
+ }
535
+ if (!parsed?.hash) {
536
+ throw new StellarAgentError({
537
+ code: "TRANSACTION_SUBMIT_FAILED",
538
+ message: "Horizon accepted the signed transaction response but did not return a transaction hash.",
539
+ docs: "docs/troubleshooting.md#transaction-timeout",
540
+ details: parsed ?? body
541
+ });
542
+ }
543
+ if (parsed.successful === false) {
544
+ throw new StellarAgentError({
545
+ code: "TRANSACTION_SUBMIT_FAILED",
546
+ message: "Signed transaction was submitted but Horizon marked it unsuccessful.",
547
+ hint: "Inspect the transaction result codes and retry only if the transaction hash is known.",
548
+ docs: "docs/troubleshooting.md#transaction-timeout",
549
+ details: parsed
550
+ });
551
+ }
552
+ if (parsed.ledger === undefined || parsed.successful === undefined) {
553
+ return confirmSubmittedTransaction({
554
+ hash: parsed.hash,
555
+ profile,
556
+ fetchImpl
557
+ });
558
+ }
559
+ return {
560
+ hash: parsed?.hash,
561
+ ledger: parsed?.ledger,
562
+ successful: parsed?.successful ?? true,
563
+ feeCharged: parsed?.fee_charged?.toString()
564
+ };
565
+ }
566
+ async function confirmSubmittedTransaction(args) {
567
+ const horizonUrl = args.profile.horizonUrl?.replace(/\/$/, "");
568
+ if (!horizonUrl) {
569
+ throw new StellarAgentError({
570
+ code: "HORIZON_UNAVAILABLE",
571
+ message: "Horizon is not configured for transaction confirmation.",
572
+ docs: "docs/troubleshooting.md#rpc-or-horizon-unavailable"
573
+ });
574
+ }
575
+ const deadline = Date.now() + 60_000;
576
+ let lastStatus;
577
+ while (Date.now() < deadline) {
578
+ const response = await args.fetchImpl(`${horizonUrl}/transactions/${args.hash}`, {
579
+ signal: AbortSignal.timeout(15_000)
580
+ });
581
+ lastStatus = response.status;
582
+ if (response.status === 404) {
583
+ await sleep(2_000);
584
+ continue;
585
+ }
586
+ const body = await response.text();
587
+ const parsed = safeJson(body);
588
+ if (!response.ok) {
589
+ throw new StellarAgentError({
590
+ code: "LEDGER_LOOKUP_FAILED",
591
+ message: "Could not confirm submitted transaction on Horizon.",
592
+ docs: "docs/troubleshooting.md#transaction-timeout",
593
+ details: parsed ?? body
594
+ });
595
+ }
596
+ if (parsed?.successful === false) {
597
+ throw new StellarAgentError({
598
+ code: "TRANSACTION_SUBMIT_FAILED",
599
+ message: "Submitted transaction was confirmed unsuccessful.",
600
+ hint: "Inspect the transaction result codes before retrying.",
601
+ docs: "docs/troubleshooting.md#transaction-timeout",
602
+ details: parsed
603
+ });
604
+ }
605
+ return {
606
+ hash: parsed?.hash ?? args.hash,
607
+ ledger: parsed?.ledger,
608
+ successful: parsed?.successful ?? true,
609
+ feeCharged: parsed?.fee_charged?.toString()
610
+ };
611
+ }
612
+ throw new StellarAgentError({
613
+ code: "TRANSACTION_TIMEOUT",
614
+ message: "Submitted transaction was not confirmed before the timeout.",
615
+ hint: `Check the transaction hash ${args.hash} on the selected Horizon endpoint before retrying.`,
616
+ docs: "docs/troubleshooting.md#transaction-timeout",
617
+ details: { hash: args.hash, lastStatus }
618
+ });
619
+ }
620
+ function assertSignedTransactionXdr(rawXdr) {
621
+ let envelope;
622
+ try {
623
+ envelope = xdr.TransactionEnvelope.fromXDR(rawXdr, "base64");
624
+ }
625
+ catch {
626
+ throw new StellarAgentError({
627
+ code: "INVALID_INPUT",
628
+ message: "Signed transaction XDR is not a valid Stellar transaction envelope.",
629
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
630
+ });
631
+ }
632
+ const type = envelope.switch().name;
633
+ const signatures = type === "envelopeTypeTx"
634
+ ? envelope.v1().signatures().length
635
+ : type === "envelopeTypeTxV0"
636
+ ? envelope.v0().signatures().length
637
+ : type === "envelopeTypeTxFeeBump"
638
+ ? envelope.feeBump().signatures().length
639
+ : 0;
640
+ if (signatures < 1) {
641
+ throw new StellarAgentError({
642
+ code: "INVALID_INPUT",
643
+ message: "Mainnet signed-XDR submission requires at least one transaction signature.",
644
+ docs: "docs/mainnet-safety.md#signed-xdr-submission"
645
+ });
646
+ }
647
+ }
648
+ export async function listClaimableBalances(claimant, profile = TESTNET_PROFILE) {
649
+ if (!profile.horizonUrl) {
650
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
651
+ }
652
+ const url = new URL(`${profile.horizonUrl.replace(/\/$/, "")}/claimable_balances`);
653
+ url.searchParams.set("claimant", claimant);
654
+ url.searchParams.set("order", "desc");
655
+ url.searchParams.set("limit", "10");
656
+ const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
657
+ if (!response.ok) {
658
+ throw new StellarAgentError({
659
+ code: "LEDGER_LOOKUP_FAILED",
660
+ message: "Could not fetch claimable balances.",
661
+ docs: "docs/troubleshooting.md#transaction-timeout"
662
+ });
663
+ }
664
+ const parsed = await response.json();
665
+ return (parsed?._embedded?.records ?? []).map((record) => ({
666
+ id: record.id,
667
+ asset: record.asset,
668
+ amount: record.amount,
669
+ sponsor: record.sponsor,
670
+ lastModifiedLedger: record.last_modified_ledger,
671
+ claimants: (record.claimants ?? []).map((claimantRecord) => ({
672
+ destination: claimantRecord.destination,
673
+ predicate: claimantRecord.predicate
674
+ }))
675
+ }));
676
+ }
677
+ export async function invokeContractWithStellarCli(args) {
678
+ const command = [
679
+ "contract",
680
+ "invoke",
681
+ "--id",
682
+ args.contractId,
683
+ "--source-account",
684
+ args.source,
685
+ "--network",
686
+ args.network ?? "testnet"
687
+ ];
688
+ addRpcArgs(command, args);
689
+ command.push("--", args.functionName);
690
+ for (const [key, value] of Object.entries(args.contractArgs ?? {})) {
691
+ command.push(`--${key}`, value);
692
+ }
693
+ return runStellarCli({
694
+ command,
695
+ action: "contract invocation",
696
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
697
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
698
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
699
+ });
700
+ }
701
+ export function parseStellarCliTransactionHash(output) {
702
+ const signingMatch = /Signing transaction:\s*([a-f0-9]{64})/i.exec(output);
703
+ if (signingMatch?.[1])
704
+ return signingMatch[1];
705
+ const hashMatch = /\btransaction(?:\s+hash)?\b[^\n\r:]*:\s*([a-f0-9]{64})/i.exec(output);
706
+ return hashMatch?.[1];
707
+ }
708
+ export async function deployContractWithStellarCli(args) {
709
+ const command = ["contract", "deploy", "--source-account", args.source, "--network", args.network ?? "testnet"];
710
+ if (args.wasm)
711
+ command.push("--wasm", args.wasm);
712
+ if (args.wasmHash)
713
+ command.push("--wasm-hash", args.wasmHash);
714
+ if (args.alias)
715
+ command.push("--alias", args.alias);
716
+ addRpcArgs(command, args);
717
+ const constructorArgs = Object.entries(args.constructorArgs ?? {});
718
+ if (constructorArgs.length > 0) {
719
+ command.push("--");
720
+ for (const [key, value] of constructorArgs) {
721
+ command.push(`--${key}`, value);
722
+ }
723
+ }
724
+ return runStellarCli({
725
+ command,
726
+ action: "contract deployment",
727
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
728
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
729
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
730
+ });
731
+ }
732
+ export async function uploadContractWasmWithStellarCli(args) {
733
+ const command = [
734
+ "contract",
735
+ "upload",
736
+ "--source-account",
737
+ args.source,
738
+ "--network",
739
+ args.network ?? "testnet",
740
+ "--wasm",
741
+ args.wasm
742
+ ];
743
+ addRpcArgs(command, args);
744
+ return runStellarCli({
745
+ command,
746
+ action: "contract Wasm upload",
747
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
748
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
749
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
750
+ });
751
+ }
752
+ export async function deployAssetContractWithStellarCli(args) {
753
+ const command = [
754
+ "contract",
755
+ "asset",
756
+ "deploy",
757
+ "--source-account",
758
+ args.source,
759
+ "--network",
760
+ args.network ?? "testnet",
761
+ "--asset",
762
+ args.asset
763
+ ];
764
+ if (args.alias)
765
+ command.push("--alias", args.alias);
766
+ addRpcArgs(command, args);
767
+ return runStellarCli({
768
+ command,
769
+ action: "asset contract deployment",
770
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
771
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
772
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
773
+ });
774
+ }
775
+ export async function assetContractIdWithStellarCli(args) {
776
+ const command = ["contract", "id", "asset", "--asset", args.asset, "--network", args.network ?? "testnet"];
777
+ addRpcArgs(command, args);
778
+ return runStellarCli({
779
+ command,
780
+ action: "asset contract id lookup",
781
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
782
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
783
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
784
+ });
785
+ }
786
+ export async function contractInfoWithStellarCli(args) {
787
+ const command = ["contract", "info", args.kind];
788
+ if (args.contractId)
789
+ command.push("--contract-id", args.contractId);
790
+ if (args.wasm)
791
+ command.push("--wasm", args.wasm);
792
+ if (args.wasmHash)
793
+ command.push("--wasm-hash", args.wasmHash);
794
+ if (args.network)
795
+ command.push("--network", args.network);
796
+ addRpcArgs(command, args);
797
+ return runStellarCli({
798
+ command,
799
+ action: "contract info lookup",
800
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
801
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
802
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
803
+ });
804
+ }
805
+ export async function readContractWithStellarCli(args) {
806
+ validateContractFootprintArgs(args, "read");
807
+ const command = ["contract", "read", "--network", args.network ?? "testnet"];
808
+ addFootprintArgs(command, args);
809
+ if (args.output)
810
+ command.push("--output", args.output);
811
+ addRpcArgs(command, args);
812
+ return runStellarCli({
813
+ command,
814
+ action: "contract storage read",
815
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
816
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
817
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
818
+ });
819
+ }
820
+ export async function fetchContractWasmWithStellarCli(args) {
821
+ validateContractFetchArgs(args);
822
+ const command = ["contract", "fetch", "--network", args.network ?? "testnet"];
823
+ if (args.contractId)
824
+ command.push("--id", args.contractId);
825
+ if (args.wasmHash)
826
+ command.push("--wasm-hash", args.wasmHash);
827
+ if (args.outFile)
828
+ command.push("--out-file", args.outFile);
829
+ addRpcArgs(command, args);
830
+ return runStellarCli({
831
+ command,
832
+ action: "contract Wasm fetch",
833
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
834
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
835
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
836
+ });
837
+ }
838
+ export async function extendContractWithStellarCli(args) {
839
+ validateLedgersToExtend(args.ledgersToExtend);
840
+ validateContractFootprintArgs(args, "extend");
841
+ const command = [
842
+ "contract",
843
+ "extend",
844
+ "--source-account",
845
+ args.source,
846
+ "--network",
847
+ args.network ?? "testnet",
848
+ "--ledgers-to-extend",
849
+ String(args.ledgersToExtend)
850
+ ];
851
+ addFootprintArgs(command, args);
852
+ if (args.ttlLedgerOnly)
853
+ command.push("--ttl-ledger-only");
854
+ addRpcArgs(command, args);
855
+ return runStellarCli({
856
+ command,
857
+ action: "contract TTL extension",
858
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
859
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
860
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
861
+ });
862
+ }
863
+ export async function restoreContractWithStellarCli(args) {
864
+ validateContractFootprintArgs(args, "restore");
865
+ const command = ["contract", "restore", "--source-account", args.source, "--network", args.network ?? "testnet"];
866
+ addFootprintArgs(command, args);
867
+ addRpcArgs(command, args);
868
+ return runStellarCli({
869
+ command,
870
+ action: "contract restore",
871
+ ...(args.stellarBinary === undefined ? {} : { binary: args.stellarBinary }),
872
+ ...(args.stellarConfigDir === undefined ? {} : { configDir: args.stellarConfigDir }),
873
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
874
+ });
875
+ }
876
+ function addFootprintArgs(command, args) {
877
+ if (args.contractId)
878
+ command.push("--id", args.contractId);
879
+ if (args.key)
880
+ command.push("--key", args.key);
881
+ if (args.keyXdr)
882
+ command.push("--key-xdr", args.keyXdr);
883
+ if (args.wasm)
884
+ command.push("--wasm", args.wasm);
885
+ if (args.wasmHash)
886
+ command.push("--wasm-hash", args.wasmHash);
887
+ if (args.durability)
888
+ command.push("--durability", args.durability);
889
+ }
890
+ function addRpcArgs(command, args) {
891
+ if (args.rpcUrl)
892
+ command.push("--rpc-url", args.rpcUrl);
893
+ if (args.networkPassphrase)
894
+ command.push("--network-passphrase", args.networkPassphrase);
895
+ }
896
+ function validateLedgersToExtend(value) {
897
+ if (!Number.isInteger(value) || value < 1) {
898
+ throw new StellarAgentError({
899
+ code: "INVALID_INPUT",
900
+ message: "Ledgers to extend must be a positive integer.",
901
+ docs: "docs/smart-contracts.md#extend-and-restore"
902
+ });
903
+ }
904
+ }
905
+ function validateContractFetchArgs(args) {
906
+ if (Boolean(args.contractId) === Boolean(args.wasmHash)) {
907
+ throw new StellarAgentError({
908
+ code: "INVALID_INPUT",
909
+ message: "Provide exactly one of --id or --wasm-hash for contract fetch.",
910
+ docs: "docs/smart-contracts.md#fetch"
911
+ });
912
+ }
913
+ }
914
+ function validateContractFootprintArgs(args, action) {
915
+ const hasContractTarget = Boolean(args.contractId);
916
+ const hasStorageKey = Boolean(args.key || args.keyXdr);
917
+ const hasWasmTarget = Boolean(args.wasm || args.wasmHash);
918
+ if (!hasContractTarget && !hasWasmTarget) {
919
+ throw new StellarAgentError({
920
+ code: "INVALID_INPUT",
921
+ message: `Provide --id, --wasm, or --wasm-hash for contract ${action}.`,
922
+ docs: "docs/smart-contracts.md#extend-and-restore"
923
+ });
924
+ }
925
+ if (args.key && args.keyXdr) {
926
+ throw new StellarAgentError({
927
+ code: "INVALID_INPUT",
928
+ message: "Provide only one of --key or --key-xdr.",
929
+ docs: "docs/smart-contracts.md#extend-and-restore"
930
+ });
931
+ }
932
+ if (args.wasm && args.wasmHash) {
933
+ throw new StellarAgentError({
934
+ code: "INVALID_INPUT",
935
+ message: "Provide only one of --wasm or --wasm-hash.",
936
+ docs: "docs/smart-contracts.md#extend-and-restore"
937
+ });
938
+ }
939
+ if (hasStorageKey && !hasContractTarget) {
940
+ throw new StellarAgentError({
941
+ code: "INVALID_INPUT",
942
+ message: "Provide --id when using --key or --key-xdr.",
943
+ docs: "docs/smart-contracts.md#extend-and-restore"
944
+ });
945
+ }
946
+ if (hasContractTarget && hasWasmTarget) {
947
+ throw new StellarAgentError({
948
+ code: "INVALID_INPUT",
949
+ message: "Choose either a contract/storage target or a Wasm target, not both.",
950
+ docs: "docs/smart-contracts.md#extend-and-restore"
951
+ });
952
+ }
953
+ }
954
+ async function runStellarCli(args) {
955
+ const binary = args.binary ?? "stellar";
956
+ const command = [...args.command];
957
+ const runtimeArgs = [
958
+ ...(args.configDir ? ["--config-dir", args.configDir] : []),
959
+ ...(args.noCache ? ["--no-cache"] : [])
960
+ ];
961
+ if (runtimeArgs.length > 0) {
962
+ const separator = command.indexOf("--");
963
+ command.splice(separator < 0 ? command.length : separator, 0, ...runtimeArgs);
964
+ }
965
+ try {
966
+ const { stdout, stderr } = await execFileAsync(binary, command, {
967
+ timeout: 120_000,
968
+ maxBuffer: 10 * 1024 * 1024
969
+ });
970
+ return { command: [binary, ...command], stdout, stderr };
971
+ }
972
+ catch (error) {
973
+ if (error?.code === "ENOENT") {
974
+ throw new StellarAgentError({
975
+ code: "STELLAR_CLI_UNAVAILABLE",
976
+ message: "The Stellar CLI binary was not found.",
977
+ hint: "Install it from https://developers.stellar.org/docs/tools/cli/install-cli or pass --stellar-binary.",
978
+ docs: "docs/smart-contracts.md#stellar-cli",
979
+ exitCode: EXIT_CODES.usage
980
+ });
981
+ }
982
+ throw new StellarAgentError({
983
+ code: "TRANSACTION_SUBMIT_FAILED",
984
+ message: `Stellar CLI ${args.action} failed.`,
985
+ hint: "Run the same stellar command with --help to inspect supported arguments.",
986
+ docs: "docs/smart-contracts.md",
987
+ details: { stderr: error?.stderr, stdout: error?.stdout, message: error?.message }
988
+ });
989
+ }
990
+ }
991
+ export async function checkStellarCli(binary = "stellar") {
992
+ try {
993
+ const { stdout, stderr } = await execFileAsync(binary, ["--version"], {
994
+ timeout: 10_000,
995
+ maxBuffer: 1024 * 1024
996
+ });
997
+ return {
998
+ available: true,
999
+ binary,
1000
+ version: (stdout || stderr).trim()
1001
+ };
1002
+ }
1003
+ catch (error) {
1004
+ return {
1005
+ available: false,
1006
+ binary,
1007
+ error: error?.code === "ENOENT" ? "not_found" : String(error?.message ?? error)
1008
+ };
1009
+ }
1010
+ }
1011
+ async function submitOperation(args) {
1012
+ const profile = args.profile;
1013
+ if (profile.realFunds) {
1014
+ throw new StellarAgentError({
1015
+ code: "MAINNET_NOT_ENABLED",
1016
+ message: "Mainnet auto-signing is blocked.",
1017
+ hint: "Use an explicit human approval flow for Mainnet payments.",
1018
+ docs: "docs/mainnet-safety.md"
1019
+ });
1020
+ }
1021
+ if (!profile.horizonUrl) {
1022
+ throw new StellarAgentError({
1023
+ code: "HORIZON_UNAVAILABLE",
1024
+ message: "Horizon is not configured for this profile."
1025
+ });
1026
+ }
1027
+ const keypair = Keypair.fromSecret(args.source.secretKey);
1028
+ const server = new Horizon.Server(profile.horizonUrl);
1029
+ try {
1030
+ const operations = args.operations ?? (args.operation ? [args.operation] : []);
1031
+ if (operations.length < 1 || operations.length > 100) {
1032
+ throw new StellarAgentError({
1033
+ code: "INVALID_INPUT",
1034
+ message: "A Stellar transaction must contain between 1 and 100 operations.",
1035
+ docs: "docs/troubleshooting.md#batch-transaction-failed",
1036
+ exitCode: EXIT_CODES.usage
1037
+ });
1038
+ }
1039
+ const sourceAccount = await server.loadAccount(args.source.publicKey);
1040
+ const fee = await estimateTransactionFee({
1041
+ profile,
1042
+ operationCount: operations.length,
1043
+ strategy: args.feeStrategy ?? "medium",
1044
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache })
1045
+ });
1046
+ let builder = new TransactionBuilder(sourceAccount, {
1047
+ fee: fee.perOperationFee,
1048
+ networkPassphrase: profile.networkPassphrase
1049
+ });
1050
+ for (const operation of operations) {
1051
+ builder = builder.addOperation(operation);
1052
+ }
1053
+ if (args.memo)
1054
+ builder = builder.addMemo(Memo.text(args.memo));
1055
+ const tx = builder.setTimeout(60).build();
1056
+ tx.sign(keypair);
1057
+ const result = await server.submitTransaction(tx);
1058
+ if (result.successful === false) {
1059
+ throw new StellarAgentError({
1060
+ code: "TRANSACTION_SUBMIT_FAILED",
1061
+ message: "Stellar transaction was submitted but Horizon marked it unsuccessful.",
1062
+ hint: "Inspect the transaction result codes and retry only if the transaction hash is known.",
1063
+ docs: "docs/troubleshooting.md#transaction-timeout",
1064
+ details: result
1065
+ });
1066
+ }
1067
+ return {
1068
+ hash: result.hash,
1069
+ ledger: result.ledger,
1070
+ successful: result.successful ?? true,
1071
+ feeCharged: result.fee_charged?.toString(),
1072
+ feeBid: fee.transactionFee,
1073
+ feeStrategy: fee.strategy
1074
+ };
1075
+ }
1076
+ catch (error) {
1077
+ if (error instanceof StellarAgentError)
1078
+ throw error;
1079
+ if (isHorizonNotFound(error)) {
1080
+ throw new StellarAgentError({
1081
+ code: "ACCOUNT_NOT_FOUND",
1082
+ message: "The source account was not found on the selected Stellar network.",
1083
+ hint: "Fund the account on Testnet before submitting transactions.",
1084
+ docs: "docs/quickstart-testnet.md"
1085
+ });
1086
+ }
1087
+ throw new StellarAgentError({
1088
+ code: "TRANSACTION_SUBMIT_FAILED",
1089
+ message: "Stellar transaction could not be submitted or confirmed.",
1090
+ hint: "Check wallet funding, trustlines, balance predicates, destination address, and Horizon availability.",
1091
+ docs: "docs/troubleshooting.md#transaction-timeout",
1092
+ details: normalizeHorizonError(error)
1093
+ });
1094
+ }
1095
+ }
1096
+ function stellarSdkAsset(input) {
1097
+ const parsed = parseAsset(input);
1098
+ return parsed.kind === "native" ? Asset.native() : new Asset(parsed.code, parsed.issuer);
1099
+ }
1100
+ function normalizeLiquidityPoolRecord(record) {
1101
+ return {
1102
+ id: record.id,
1103
+ ...(record.paging_token === undefined ? {} : { pagingToken: record.paging_token }),
1104
+ feeBp: Number(record.fee_bp),
1105
+ type: record.type,
1106
+ totalTrustlines: String(record.total_trustlines ?? "0"),
1107
+ totalShares: String(record.total_shares ?? "0"),
1108
+ reserves: (record.reserves ?? []).map((reserve) => ({
1109
+ asset: reserve.asset === "native" ? "XLM" : String(reserve.asset),
1110
+ amount: String(reserve.amount)
1111
+ }))
1112
+ };
1113
+ }
1114
+ function collectionResult(records, page) {
1115
+ return {
1116
+ records,
1117
+ ...(page?._links?.next?.href === undefined ? {} : { next: page._links.next.href }),
1118
+ ...(page?._links?.prev?.href === undefined ? {} : { previous: page._links.prev.href })
1119
+ };
1120
+ }
1121
+ function horizonRecords(page) {
1122
+ return page?._embedded?.records ?? page?.records ?? [];
1123
+ }
1124
+ async function horizonGet(url, message) {
1125
+ const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
1126
+ const body = await response.text();
1127
+ const parsed = safeJson(body);
1128
+ if (!response.ok) {
1129
+ throw new StellarAgentError({
1130
+ code: "LEDGER_LOOKUP_FAILED",
1131
+ message,
1132
+ docs: "docs/troubleshooting.md#transaction-timeout",
1133
+ details: parsed ?? body
1134
+ });
1135
+ }
1136
+ return parsed ?? {};
1137
+ }
1138
+ function requireTwoPoolReserves(pool) {
1139
+ if (pool.reserves.length !== 2 || !pool.reserves[0] || !pool.reserves[1]) {
1140
+ throw new StellarAgentError({
1141
+ code: "LEDGER_LOOKUP_FAILED",
1142
+ message: "Liquidity pool record did not include exactly two reserves.",
1143
+ docs: "docs/market-liquidity.md#core-pool-inspection"
1144
+ });
1145
+ }
1146
+ return [pool.reserves[0], pool.reserves[1]];
1147
+ }
1148
+ function liquidityPoolShareRatio(shares, totalShares) {
1149
+ const sharesValue = Number(shares);
1150
+ const totalValue = Number(totalShares);
1151
+ if (!Number.isFinite(sharesValue) || !Number.isFinite(totalValue) || totalValue <= 0)
1152
+ return undefined;
1153
+ return sharesValue / totalValue;
1154
+ }
1155
+ function estimatePoolReserves(pool, shareOfPool) {
1156
+ return pool.reserves.map((reserve) => ({
1157
+ asset: reserve.asset,
1158
+ amount: formatEstimatedAmount(Number(reserve.amount) * shareOfPool)
1159
+ }));
1160
+ }
1161
+ function estimateDepositShares(pool, maxAmountA, maxAmountB) {
1162
+ const [reserveA, reserveB] = requireTwoPoolReserves(pool);
1163
+ const reserveAValue = Number(reserveA.amount);
1164
+ const reserveBValue = Number(reserveB.amount);
1165
+ const maxAValue = Number(maxAmountA);
1166
+ const maxBValue = Number(maxAmountB);
1167
+ const totalShares = Number(pool.totalShares);
1168
+ if (!Number.isFinite(reserveAValue) ||
1169
+ !Number.isFinite(reserveBValue) ||
1170
+ !Number.isFinite(maxAValue) ||
1171
+ !Number.isFinite(maxBValue) ||
1172
+ !Number.isFinite(totalShares) ||
1173
+ reserveAValue <= 0 ||
1174
+ reserveBValue <= 0 ||
1175
+ totalShares <= 0) {
1176
+ return undefined;
1177
+ }
1178
+ return formatEstimatedAmount(Math.min(maxAValue / reserveAValue, maxBValue / reserveBValue) * totalShares);
1179
+ }
1180
+ function poolPrice(pool) {
1181
+ const [reserveA, reserveB] = requireTwoPoolReserves(pool);
1182
+ const reserveAValue = Number(reserveA.amount);
1183
+ const reserveBValue = Number(reserveB.amount);
1184
+ if (!Number.isFinite(reserveAValue) || !Number.isFinite(reserveBValue) || reserveAValue <= 0)
1185
+ return undefined;
1186
+ return formatEstimatedAmount(reserveBValue / reserveAValue);
1187
+ }
1188
+ async function liquidityPoolTrustlineStatus(accountId, pool, profile) {
1189
+ if (!profile.horizonUrl) {
1190
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
1191
+ }
1192
+ const account = await horizonGet(new URL(`${profile.horizonUrl.replace(/\/$/, "")}/accounts/${accountId}`), "Could not load account trustlines from Horizon.");
1193
+ const reserveAssets = requireTwoPoolReserves(pool).map((reserve) => reserve.asset);
1194
+ const reserveAssetsSatisfied = reserveAssets.every((asset) => asset === "XLM" || hasAssetBalanceOrTrustline(account, asset));
1195
+ const poolShareSatisfied = account.balances.some((balance) => balance.asset_type === "liquidity_pool_shares" && balance.liquidity_pool_id === pool.id);
1196
+ return {
1197
+ reserveAssetsSatisfied,
1198
+ poolShareSatisfied,
1199
+ missing: [
1200
+ ...reserveAssets.filter((asset) => asset !== "XLM" && !hasAssetBalanceOrTrustline(account, asset)),
1201
+ ...(poolShareSatisfied ? [] : [`pool_shares:${pool.id}`])
1202
+ ]
1203
+ };
1204
+ }
1205
+ function hasAssetBalanceOrTrustline(account, asset) {
1206
+ return account.balances.some((balance) => balanceLineAssetString(balance).toUpperCase() === asset.toUpperCase());
1207
+ }
1208
+ function balanceLineAssetString(balance) {
1209
+ if (balance.asset_type === "native")
1210
+ return "XLM";
1211
+ if (balance.asset_code && balance.asset_issuer)
1212
+ return `${balance.asset_code}:${balance.asset_issuer}`;
1213
+ if (balance.asset_type === "liquidity_pool_shares")
1214
+ return `pool_shares:${balance.liquidity_pool_id}`;
1215
+ return String(balance.asset_type ?? "unknown");
1216
+ }
1217
+ function liquidityPoolRiskNotes(pool) {
1218
+ return {
1219
+ notes: [
1220
+ "Liquidity pool fees are not guaranteed profit.",
1221
+ "Pool share value can underperform simply holding the reserve assets.",
1222
+ "Quotes, estimates, and Horizon snapshots can change before transaction submission.",
1223
+ `Core pool fee is ${pool.feeBp} bps.`
1224
+ ]
1225
+ };
1226
+ }
1227
+ async function resolveLiquidityPoolAssetForTrustline(args) {
1228
+ if (args.poolId) {
1229
+ const pool = await inspectLiquidityPool({
1230
+ poolId: args.poolId,
1231
+ ...(args.profile === undefined ? {} : { profile: args.profile })
1232
+ });
1233
+ const [reserveA, reserveB] = requireTwoPoolReserves(pool);
1234
+ const asset = new LiquidityPoolAsset(stellarSdkAsset(reserveA.asset), stellarSdkAsset(reserveB.asset), LiquidityPoolFeeV18);
1235
+ return { poolId: pool.id, asset };
1236
+ }
1237
+ if (!args.assetA || !args.assetB) {
1238
+ throw new StellarAgentError({
1239
+ code: "INVALID_INPUT",
1240
+ message: "Provide --pool or both --asset-a and --asset-b for a pool-share trustline.",
1241
+ docs: "docs/market-liquidity.md#pool-share-trustlines"
1242
+ });
1243
+ }
1244
+ const [assetA, assetB] = sortedLiquidityPoolAssets(args.assetA, args.assetB);
1245
+ const asset = new LiquidityPoolAsset(assetA, assetB, LiquidityPoolFeeV18);
1246
+ return { poolId: getLiquidityPoolId("constant_product", { assetA, assetB, fee: LiquidityPoolFeeV18 }).toString("hex"), asset };
1247
+ }
1248
+ function sortedLiquidityPoolAssets(assetA, assetB) {
1249
+ const left = stellarSdkAsset(assetA);
1250
+ const right = stellarSdkAsset(assetB);
1251
+ return Asset.compare(left, right) <= 0 ? [left, right] : [right, left];
1252
+ }
1253
+ function normalizeAmountAllowZero(input) {
1254
+ const trimmed = input.trim();
1255
+ if (/^0(?:\.0{0,7})?$/.test(trimmed))
1256
+ return "0.0000000";
1257
+ return parseAmount(trimmed).value;
1258
+ }
1259
+ function formatEstimatedAmount(value) {
1260
+ if (!Number.isFinite(value) || value < 0)
1261
+ return "0.0000000";
1262
+ return value.toFixed(7);
1263
+ }
1264
+ function normalizeTrustlineLimit(input) {
1265
+ if (input === "0")
1266
+ return "0";
1267
+ const parsed = parseAmount(input);
1268
+ if (parsed.stroops > 9223372036854775807n) {
1269
+ throw new StellarAgentError({
1270
+ code: "INVALID_AMOUNT",
1271
+ message: "Trustline limit exceeds Stellar's maximum signed 64-bit stroop amount.",
1272
+ exitCode: EXIT_CODES.usage
1273
+ });
1274
+ }
1275
+ return formatStroops(parsed.stroops);
1276
+ }
1277
+ function uniqueClaimants(claimants) {
1278
+ const unique = [...new Set(claimants.map((claimant) => claimant.trim()))].filter(Boolean);
1279
+ if (unique.length === 0) {
1280
+ throw new StellarAgentError({
1281
+ code: "INVALID_INPUT",
1282
+ message: "At least one claimable balance claimant is required.",
1283
+ docs: "docs/claimable-balances.md"
1284
+ });
1285
+ }
1286
+ return unique;
1287
+ }
1288
+ const MAX_DATE_EPOCH_SECONDS = 8640000000000n;
1289
+ function parseClaimableTimestamp(input, field) {
1290
+ const invalid = () => new StellarAgentError({
1291
+ code: "INVALID_INPUT",
1292
+ message: `${field} must be a Unix timestamp in seconds or an ISO-8601 date/time.`,
1293
+ docs: "docs/claimable-balances.md#time-bound-claiming"
1294
+ });
1295
+ let epochSeconds;
1296
+ if (input instanceof Date) {
1297
+ const millis = input.getTime();
1298
+ if (!Number.isFinite(millis))
1299
+ throw invalid();
1300
+ epochSeconds = BigInt(Math.floor(millis / 1000));
1301
+ }
1302
+ else if (typeof input === "number") {
1303
+ if (!Number.isFinite(input) || !Number.isInteger(input) || input < 0)
1304
+ throw invalid();
1305
+ epochSeconds = BigInt(input);
1306
+ }
1307
+ else {
1308
+ const value = input.trim();
1309
+ if (!value)
1310
+ throw invalid();
1311
+ if (/^\d+$/.test(value)) {
1312
+ epochSeconds = BigInt(value);
1313
+ }
1314
+ else {
1315
+ const millis = Date.parse(value);
1316
+ if (!Number.isFinite(millis))
1317
+ throw invalid();
1318
+ epochSeconds = BigInt(Math.floor(millis / 1000));
1319
+ }
1320
+ }
1321
+ if (epochSeconds < 0n || epochSeconds > MAX_DATE_EPOCH_SECONDS)
1322
+ throw invalid();
1323
+ return {
1324
+ epochSeconds: epochSeconds.toString(),
1325
+ iso: new Date(Number(epochSeconds) * 1000).toISOString()
1326
+ };
1327
+ }
1328
+ export async function estimateTransactionFee(args) {
1329
+ const profile = args.profile ?? TESTNET_PROFILE;
1330
+ const operationCount = args.operationCount ?? 1;
1331
+ if (!Number.isInteger(operationCount) || operationCount < 1 || operationCount > 100) {
1332
+ throw new StellarAgentError({
1333
+ code: "INVALID_INPUT",
1334
+ message: "Fee estimation requires an operation count between 1 and 100.",
1335
+ docs: "docs/troubleshooting.md#batch-transaction-failed",
1336
+ exitCode: EXIT_CODES.usage
1337
+ });
1338
+ }
1339
+ const strategy = args.strategy ?? "medium";
1340
+ const stats = await horizonFeeStats(profile, {
1341
+ ...(args.noCache === undefined ? {} : { noCache: args.noCache }),
1342
+ ...(args.fetchImpl === undefined ? {} : { fetchImpl: args.fetchImpl })
1343
+ });
1344
+ const perOperationFee = selectFeeForStrategy(stats, strategy);
1345
+ return {
1346
+ source: stats.source,
1347
+ strategy,
1348
+ perOperationFee,
1349
+ transactionFee: (BigInt(perOperationFee) * BigInt(operationCount)).toString(),
1350
+ operationCount,
1351
+ cached: stats.cached,
1352
+ ...(stats.ledgerCapacityUsage === undefined ? {} : { ledgerCapacityUsage: stats.ledgerCapacityUsage })
1353
+ };
1354
+ }
1355
+ function normalizeBatchPayments(payments) {
1356
+ if (!Array.isArray(payments) || payments.length < 1 || payments.length > 100) {
1357
+ throw new StellarAgentError({
1358
+ code: "INVALID_INPUT",
1359
+ message: "Batch payments must include between 1 and 100 payment operations.",
1360
+ hint: "Use pay send for a single payment or provide a JSON array to pay batch.",
1361
+ docs: "docs/troubleshooting.md#batch-transaction-failed",
1362
+ exitCode: EXIT_CODES.usage
1363
+ });
1364
+ }
1365
+ return payments.map((payment, index) => {
1366
+ if (!payment || typeof payment !== "object") {
1367
+ throw new StellarAgentError({
1368
+ code: "INVALID_INPUT",
1369
+ message: `Batch payment ${index + 1} must be an object.`,
1370
+ docs: "docs/troubleshooting.md#batch-transaction-failed",
1371
+ exitCode: EXIT_CODES.usage
1372
+ });
1373
+ }
1374
+ const asset = payment.asset ?? "XLM";
1375
+ parseAsset(asset);
1376
+ return {
1377
+ destination: payment.destination,
1378
+ amount: parseAmount(payment.amount, asset).value,
1379
+ asset
1380
+ };
1381
+ });
1382
+ }
1383
+ async function horizonFeeStats(profile, options = {}) {
1384
+ if (!profile.horizonUrl) {
1385
+ return { source: "base_fee_fallback", cached: false, lastLedgerBaseFee: BASE_FEE };
1386
+ }
1387
+ const key = `feeStats:${profile.horizonUrl}`;
1388
+ const now = Date.now();
1389
+ if (!options.noCache) {
1390
+ const cached = sessionCache.get(key);
1391
+ if (cached && cached.expiresAt > now) {
1392
+ return { ...cached.value, cached: true };
1393
+ }
1394
+ }
1395
+ try {
1396
+ const fetchImpl = options.fetchImpl ?? fetch;
1397
+ const response = await fetchImpl(`${profile.horizonUrl.replace(/\/$/, "")}/fee_stats`, {
1398
+ signal: AbortSignal.timeout(15_000)
1399
+ });
1400
+ if (!response.ok)
1401
+ throw new Error(`fee_stats returned ${response.status}`);
1402
+ const parsed = await response.json();
1403
+ const feeCharged = normalizeFeeCharged(parsed.fee_charged);
1404
+ const value = {
1405
+ source: "horizon",
1406
+ cached: false,
1407
+ lastLedgerBaseFee: String(parsed.last_ledger_base_fee ?? BASE_FEE),
1408
+ ...(parsed.ledger_capacity_usage === undefined
1409
+ ? {}
1410
+ : { ledgerCapacityUsage: String(parsed.ledger_capacity_usage) }),
1411
+ ...(feeCharged === undefined ? {} : { feeCharged })
1412
+ };
1413
+ sessionCache.set(key, { value, expiresAt: now + 30_000 });
1414
+ return value;
1415
+ }
1416
+ catch {
1417
+ return { source: "base_fee_fallback", cached: false, lastLedgerBaseFee: BASE_FEE };
1418
+ }
1419
+ }
1420
+ function normalizeFeeCharged(input) {
1421
+ if (!input || typeof input !== "object")
1422
+ return undefined;
1423
+ const output = {};
1424
+ for (const [key, value] of Object.entries(input)) {
1425
+ if (value !== undefined && value !== null && /^\d+$/.test(String(value)))
1426
+ output[key] = String(value);
1427
+ }
1428
+ return output;
1429
+ }
1430
+ function selectFeeForStrategy(stats, strategy) {
1431
+ if (strategy === "base")
1432
+ return maxFeeString(BASE_FEE, stats.lastLedgerBaseFee);
1433
+ const charged = stats.feeCharged ?? {};
1434
+ const candidate = strategy === "low"
1435
+ ? charged.p50 ?? charged.mode ?? charged.min
1436
+ : strategy === "medium"
1437
+ ? charged.p80 ?? charged.p70 ?? charged.p60 ?? charged.p50
1438
+ : strategy === "high"
1439
+ ? charged.p90 ?? charged.p95 ?? charged.max
1440
+ : charged.p95 ?? charged.p90 ?? charged.max;
1441
+ return maxFeeString(BASE_FEE, stats.lastLedgerBaseFee, candidate ?? BASE_FEE);
1442
+ }
1443
+ function maxFeeString(...values) {
1444
+ return values.reduce((max, value) => {
1445
+ if (!/^\d+$/.test(value))
1446
+ return max;
1447
+ return BigInt(value) > BigInt(max) ? value : max;
1448
+ }, BASE_FEE);
1449
+ }
1450
+ export async function lookupTransaction(hash, profile) {
1451
+ if (!profile.horizonUrl) {
1452
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
1453
+ }
1454
+ const response = await fetch(`${profile.horizonUrl.replace(/\/$/, "")}/transactions/${hash}`, {
1455
+ signal: AbortSignal.timeout(15_000)
1456
+ });
1457
+ if (!response.ok) {
1458
+ throw new StellarAgentError({
1459
+ code: "LEDGER_LOOKUP_FAILED",
1460
+ message: "Could not look up transaction.",
1461
+ docs: "docs/troubleshooting.md#transaction-timeout"
1462
+ });
1463
+ }
1464
+ return response.json();
1465
+ }
1466
+ export async function accountPayments(args) {
1467
+ return horizonCollection({
1468
+ profile: args.profile,
1469
+ path: `/accounts/${args.address}/payments`,
1470
+ ...(args.limit === undefined ? {} : { limit: args.limit })
1471
+ });
1472
+ }
1473
+ export async function accountEffects(args) {
1474
+ return horizonCollection({
1475
+ profile: args.profile,
1476
+ path: `/accounts/${args.address}/effects`,
1477
+ ...(args.limit === undefined ? {} : { limit: args.limit })
1478
+ });
1479
+ }
1480
+ export async function transactionEffects(args) {
1481
+ return horizonCollection({
1482
+ profile: args.profile,
1483
+ path: `/transactions/${args.hash}/effects`,
1484
+ ...(args.limit === undefined ? {} : { limit: args.limit })
1485
+ });
1486
+ }
1487
+ export async function latestLedger(profile) {
1488
+ if (!profile.horizonUrl) {
1489
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
1490
+ }
1491
+ const response = await fetch(`${profile.horizonUrl.replace(/\/$/, "")}/ledgers?order=desc&limit=1`, {
1492
+ signal: AbortSignal.timeout(15_000)
1493
+ });
1494
+ if (!response.ok) {
1495
+ throw new StellarAgentError({
1496
+ code: "LEDGER_LOOKUP_FAILED",
1497
+ message: "Could not fetch latest ledger.",
1498
+ docs: "docs/troubleshooting.md#rpc-unavailable"
1499
+ });
1500
+ }
1501
+ return response.json();
1502
+ }
1503
+ async function horizonCollection(args) {
1504
+ if (!args.profile.horizonUrl) {
1505
+ throw new StellarAgentError({ code: "HORIZON_UNAVAILABLE", message: "Horizon is not configured." });
1506
+ }
1507
+ const url = new URL(`${args.profile.horizonUrl.replace(/\/$/, "")}${args.path}`);
1508
+ url.searchParams.set("order", "desc");
1509
+ url.searchParams.set("limit", String(args.limit ?? 10));
1510
+ const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
1511
+ if (!response.ok) {
1512
+ throw new StellarAgentError({
1513
+ code: "LEDGER_LOOKUP_FAILED",
1514
+ message: "Could not fetch ledger collection from Horizon.",
1515
+ docs: "docs/troubleshooting.md#transaction-timeout",
1516
+ details: { path: args.path, status: response.status }
1517
+ });
1518
+ }
1519
+ const parsed = await response.json();
1520
+ return {
1521
+ records: parsed?._embedded?.records ?? [],
1522
+ next: parsed?._links?.next?.href,
1523
+ previous: parsed?._links?.prev?.href
1524
+ };
1525
+ }
1526
+ function safeJson(raw) {
1527
+ try {
1528
+ return JSON.parse(raw);
1529
+ }
1530
+ catch {
1531
+ return undefined;
1532
+ }
1533
+ }
1534
+ function sleep(ms) {
1535
+ return new Promise((resolve) => setTimeout(resolve, ms));
1536
+ }
1537
+ function isHorizonNotFound(error) {
1538
+ return (error?.response?.status === 404 ||
1539
+ error?.response?.statusCode === 404 ||
1540
+ error?.status === 404 ||
1541
+ error?.statusCode === 404 ||
1542
+ error?.message === "Not Found");
1543
+ }
1544
+ function normalizeHorizonError(error) {
1545
+ return error?.response?.data ?? error?.response?.body ?? error?.message ?? String(error);
1546
+ }
1547
+ //# sourceMappingURL=index.js.map