@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/LICENSE +21 -0
- package/dist/index.d.ts +494 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1547 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
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
|