@evergreen-stellar/cli 0.1.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.
@@ -0,0 +1,2051 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin.ts
4
+ import process from "node:process";
5
+ import console from "node:console";
6
+ import { readFile } from "node:fs/promises";
7
+ import { rpc as rpc5, Networks as Networks4 } from "@stellar/stellar-sdk";
8
+
9
+ // ../core/dist/format.js
10
+ var EVIDENCE_LOCALE = "en-US";
11
+ function formatCount(value) {
12
+ return value.toLocaleString(EVIDENCE_LOCALE);
13
+ }
14
+
15
+ // ../core/dist/ttl.js
16
+ var SECONDS_PER_LEDGER = 5;
17
+ function observeTTL(args) {
18
+ if (args.liveUntilLedgerSeq === void 0)
19
+ return { status: "unavailable" };
20
+ return {
21
+ status: "known",
22
+ endsAtLedger: args.liveUntilLedgerSeq,
23
+ remainingLedgers: args.liveUntilLedgerSeq - args.observedAtLedger
24
+ };
25
+ }
26
+ function hasExpired(remainingLedgers) {
27
+ return remainingLedgers < 0;
28
+ }
29
+ function isLive(ttl) {
30
+ if (ttl.status === "unavailable")
31
+ return void 0;
32
+ return !hasExpired(ttl.remainingLedgers);
33
+ }
34
+ function isValidThreshold(thresholdLedgers) {
35
+ return Number.isInteger(thresholdLedgers) && thresholdLedgers >= 0;
36
+ }
37
+ function needsAction(remainingLedgers, thresholdLedgers) {
38
+ return remainingLedgers <= thresholdLedgers;
39
+ }
40
+ function estimateEndsAt(ttl, now) {
41
+ if (ttl.status === "unavailable")
42
+ return void 0;
43
+ return new Date(now.getTime() + ttl.remainingLedgers * SECONDS_PER_LEDGER * 1e3);
44
+ }
45
+
46
+ // ../core/dist/rpc.js
47
+ import { Contract, Networks, rpc, xdr } from "@stellar/stellar-sdk";
48
+ var NotTestnetError = class extends Error {
49
+ constructor(actual) {
50
+ super(`Refusing to run: RPC network is "${actual}", not Stellar testnet.`);
51
+ this.name = "NotTestnetError";
52
+ }
53
+ };
54
+ function isValidContractId(contractId) {
55
+ try {
56
+ new Contract(contractId);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ function instanceKey(contractId) {
63
+ return new Contract(contractId).getFootprint().toXDR("base64");
64
+ }
65
+ function codeKey(wasmHash) {
66
+ return xdr.LedgerKey.contractCode(new xdr.LedgerKeyContractCode({ hash: wasmHash })).toXDR("base64");
67
+ }
68
+ function createRpcReader(server) {
69
+ return {
70
+ async read(keys) {
71
+ const res = await server.getLedgerEntries(...keys.map((k) => xdr.LedgerKey.fromXDR(k, "base64")));
72
+ return {
73
+ latestLedger: res.latestLedger,
74
+ entries: res.entries.map((e) => ({
75
+ key: e.key.toXDR("base64"),
76
+ entryXdr: e.val.toXDR("base64"),
77
+ liveUntilLedgerSeq: e.liveUntilLedgerSeq
78
+ }))
79
+ };
80
+ }
81
+ };
82
+ }
83
+ async function connectTestnet(rpcUrl) {
84
+ const server = new rpc.Server(rpcUrl);
85
+ const { passphrase } = await server.getNetwork();
86
+ if (passphrase !== Networks.TESTNET)
87
+ throw new NotTestnetError(passphrase);
88
+ return createRpcReader(server);
89
+ }
90
+
91
+ // ../core/dist/temporary-policy.js
92
+ import { Address, xdr as xdr2 } from "@stellar/stellar-sdk";
93
+
94
+ // ../core/dist/health.js
95
+ var DEFAULT_WARN_LEDGERS = 120960;
96
+ function assertHealthThresholds(thresholds) {
97
+ if (!Number.isSafeInteger(thresholds.criticalBelowLedgers) || !isValidThreshold(thresholds.criticalBelowLedgers)) {
98
+ throw new Error("criticalBelowLedgers must be a non-negative safe integer of ledgers");
99
+ }
100
+ if (!Number.isSafeInteger(thresholds.warnBelowLedgers) || !isValidThreshold(thresholds.warnBelowLedgers)) {
101
+ throw new Error("warnBelowLedgers must be a non-negative safe integer of ledgers");
102
+ }
103
+ if (!needsAction(thresholds.criticalBelowLedgers, thresholds.warnBelowLedgers)) {
104
+ throw new Error("warnBelowLedgers must be at least the action threshold (bumpWhenRemainingLedgersBelow)");
105
+ }
106
+ }
107
+ function resolveHealthThresholds(defaults, overrides = {}) {
108
+ const criticalBelowLedgers = overrides.bumpWhenRemainingLedgersBelow ?? defaults.bumpWhenRemainingLedgersBelow;
109
+ const warnBelowLedgers = overrides.warnBelowLedgers ?? defaults.warnBelowLedgers ?? Math.max(DEFAULT_WARN_LEDGERS, criticalBelowLedgers);
110
+ const resolved = { warnBelowLedgers, criticalBelowLedgers };
111
+ assertHealthThresholds(resolved);
112
+ return resolved;
113
+ }
114
+ function assessEntryWithThresholds(entry, thresholds) {
115
+ assertHealthThresholds(thresholds);
116
+ const assessment = assessEntry(entry, thresholds.warnBelowLedgers);
117
+ if (entry.ttl.status === "unavailable" || assessment.isExpired)
118
+ return assessment;
119
+ if (needsAction(entry.ttl.remainingLedgers, thresholds.criticalBelowLedgers)) {
120
+ return {
121
+ ...assessment,
122
+ health: "critical",
123
+ needsAction: true,
124
+ reason: `At or below action threshold (${thresholds.criticalBelowLedgers} ledgers). ${assessment.reason}`
125
+ };
126
+ }
127
+ if (assessment.health === "healthy")
128
+ return assessment;
129
+ return {
130
+ ...assessment,
131
+ needsAction: false,
132
+ reason: `${assessment.reason} Above action threshold (${thresholds.criticalBelowLedgers} ledgers); warning only, no bump needed.`
133
+ };
134
+ }
135
+ function assessEntry(entry, thresholdLedgers) {
136
+ if (!isValidThreshold(thresholdLedgers)) {
137
+ throw new Error("thresholdLedgers must be a non-negative integer of ledgers");
138
+ }
139
+ const observedContractCount = entry.contracts.length;
140
+ const sharingStatus = observedContractCount > 1 ? "shared" : entry.kind === "code" ? "undetermined" : "exclusive";
141
+ const shared = sharingStatus === "shared";
142
+ if (entry.ttl.status === "unavailable") {
143
+ return {
144
+ health: "unknown",
145
+ needsAction: false,
146
+ isExpired: false,
147
+ observedContractCount,
148
+ blastRadiusAtLeast: observedContractCount,
149
+ sharingStatus,
150
+ reason: "No TTL metadata was returned, so this entry\u2019s health is unread \u2014 not healthy."
151
+ };
152
+ }
153
+ const { remainingLedgers } = entry.ttl;
154
+ const isExpired = hasExpired(remainingLedgers);
155
+ const act = needsAction(remainingLedgers, thresholdLedgers);
156
+ if (isExpired) {
157
+ return {
158
+ health: "critical",
159
+ needsAction: true,
160
+ isExpired: true,
161
+ observedContractCount,
162
+ blastRadiusAtLeast: observedContractCount,
163
+ sharingStatus,
164
+ reason: entry.endBehavior === "deleted" ? "Already deleted. Temporary entries are not recoverable." : "Already archived. Restore it with RestoreFootprintOp \u2014 extendTTL cannot reach it."
165
+ };
166
+ }
167
+ if (!act) {
168
+ return {
169
+ health: "healthy",
170
+ needsAction: false,
171
+ isExpired: false,
172
+ observedContractCount,
173
+ blastRadiusAtLeast: observedContractCount,
174
+ sharingStatus,
175
+ reason: "Above threshold."
176
+ };
177
+ }
178
+ if (entry.endBehavior === "deleted") {
179
+ return {
180
+ health: "critical",
181
+ needsAction: true,
182
+ isExpired: false,
183
+ observedContractCount,
184
+ blastRadiusAtLeast: observedContractCount,
185
+ sharingStatus,
186
+ reason: "Low, and temporary \u2014 this data is DELETED at expiry, not archived. Unrecoverable."
187
+ };
188
+ }
189
+ if (shared) {
190
+ return {
191
+ health: "critical",
192
+ needsAction: true,
193
+ isExpired: false,
194
+ observedContractCount,
195
+ blastRadiusAtLeast: observedContractCount,
196
+ sharingStatus,
197
+ reason: `Low, and shared by ${observedContractCount} contracts \u2014 every one of them fails together.`
198
+ };
199
+ }
200
+ return {
201
+ health: "warning",
202
+ needsAction: true,
203
+ isExpired: false,
204
+ observedContractCount,
205
+ blastRadiusAtLeast: observedContractCount,
206
+ sharingStatus,
207
+ reason: sharingStatus === "undetermined" ? "Low. This scan saw one contract on it, but a code entry may serve others it cannot see." : "Low, recoverable, and affects only this contract."
208
+ };
209
+ }
210
+ var HEALTH_RANK = {
211
+ critical: 0,
212
+ unknown: 1,
213
+ warning: 2,
214
+ healthy: 3
215
+ };
216
+ function worstHealth(assessments) {
217
+ if (assessments.length === 0)
218
+ return void 0;
219
+ return assessments.reduce((worst, a) => HEALTH_RANK[a.health] < HEALTH_RANK[worst] ? a.health : worst, "healthy");
220
+ }
221
+ function coverageIssues(scan) {
222
+ const issues = [];
223
+ for (const [entryKey, entry] of Object.entries(scan.entries)) {
224
+ if (assessEntry(entry, 0).sharingStatus !== "undetermined")
225
+ continue;
226
+ issues.push({
227
+ kind: "sharing-undetermined",
228
+ contracts: entry.contracts,
229
+ entryKey,
230
+ observedAtLedger: entry.observedAtLedger,
231
+ message: `Code entries are shared by every contract built from the same Wasm. This scan saw ${entry.contracts.length}. Whether others depend on this entry cannot be determined from a single-contract scan \u2014 pass them together to see the real blast radius.`
232
+ });
233
+ }
234
+ const supplied = scan.coverage?.dataKeysSuppliedByContract ?? {};
235
+ for (const [contract, count] of Object.entries(supplied)) {
236
+ if (count > 0)
237
+ continue;
238
+ if (scan.coverage?.noDataKeysDeclaredByContract?.[contract] === true)
239
+ continue;
240
+ issues.push({
241
+ kind: "coverage-limited",
242
+ contracts: [contract],
243
+ message: "No data keys were supplied, so any further entries are unread. A clean result covers only what was asked for, never the whole contract."
244
+ });
245
+ }
246
+ return issues;
247
+ }
248
+
249
+ // ../core/dist/ed25519-signer.js
250
+ import { Keypair, Networks as Networks2, StrKey, Transaction, TransactionBuilder } from "@stellar/stellar-sdk";
251
+
252
+ // ../core/dist/extend.js
253
+ import { Address as Address2, xdr as xdr4 } from "@stellar/stellar-sdk";
254
+
255
+ // ../core/dist/network-config.js
256
+ import { xdr as xdr3, rpc as rpc2 } from "@stellar/stellar-sdk";
257
+ var STATE_ARCHIVAL_CONFIG_KEY = "AAAACAAAAAo=";
258
+ function field(source, name) {
259
+ if (source === null || typeof source !== "object")
260
+ return void 0;
261
+ const value = source[name];
262
+ return typeof value === "function" ? value.call(source) : value;
263
+ }
264
+ function firstNumber(source, ...names) {
265
+ for (const name of names) {
266
+ const value = field(source, name);
267
+ if (typeof value === "number")
268
+ return value;
269
+ if (typeof value === "bigint")
270
+ return Number(value);
271
+ }
272
+ return void 0;
273
+ }
274
+ function parseStateArchivalSettings(entryXdr, observedAtLedger) {
275
+ const data = xdr3.LedgerEntryData.fromXDR(entryXdr, "base64");
276
+ const configSetting = field(data, "configSetting");
277
+ const settings = field(configSetting, "stateArchivalSettings");
278
+ const maxEntryTtl = firstNumber(settings, "max_entry_ttl", "maxEntryTtl");
279
+ if (maxEntryTtl === void 0) {
280
+ throw new Error("Entry is not a STATE_ARCHIVAL config setting");
281
+ }
282
+ const persistentDenominator = field(settings, "persistent_rent_rate_denominator") ?? field(settings, "persistentRentRateDenominator");
283
+ const temporaryDenominator = field(settings, "temp_rent_rate_denominator") ?? field(settings, "tempRentRateDenominator");
284
+ return {
285
+ maxEntryTtl,
286
+ minTemporaryTtl: firstNumber(settings, "min_temporary_ttl", "minTemporaryTtl") ?? 0,
287
+ minPersistentTtl: firstNumber(settings, "min_persistent_ttl", "minPersistentTtl") ?? 0,
288
+ persistentRentRateDenominator: String(persistentDenominator),
289
+ temporaryRentRateDenominator: String(temporaryDenominator),
290
+ observedAtLedger
291
+ };
292
+ }
293
+ async function readStateArchivalSettings(server) {
294
+ const response = await server.getLedgerEntries(xdr3.LedgerKey.fromXDR(STATE_ARCHIVAL_CONFIG_KEY, "base64"));
295
+ const entry = response.entries[0];
296
+ if (!entry)
297
+ throw new Error("Network returned no state-archival config entry");
298
+ return parseStateArchivalSettings(entry.val.toXDR("base64"), response.latestLedger);
299
+ }
300
+ function resolveExtendTarget(args) {
301
+ const { currentRemainingLedgers, additionalLedgers, maxEntryTtl } = args;
302
+ if (!Number.isInteger(additionalLedgers) || additionalLedgers <= 0) {
303
+ throw new Error("--ledgers must be a positive integer of ledgers");
304
+ }
305
+ if (!Number.isInteger(maxEntryTtl) || maxEntryTtl <= 0) {
306
+ throw new Error("maxEntryTtl must come from the network and be a positive integer");
307
+ }
308
+ const base = Math.max(0, currentRemainingLedgers);
309
+ const requested = base + additionalLedgers;
310
+ const capped = Math.min(requested, maxEntryTtl - 1);
311
+ return {
312
+ extendToLedgers: capped,
313
+ wasCapped: capped < requested,
314
+ requestedLedgers: requested
315
+ };
316
+ }
317
+
318
+ // ../core/dist/write-guard.js
319
+ var PROTECTED_ENTRIES = [
320
+ {
321
+ contractId: "CCYGO7KQ6FCAZBZAUWAPCAX4RBDIPZK4BJR2KGKISEIGARTJPB7KLTTQ",
322
+ label: "guinea-pig B",
323
+ alertThresholdOn: "2026-09-20",
324
+ expiresOn: "2026-09-21",
325
+ why: "Natural-decay proof. Extending it moves the crossing past the sprint and the ageing cannot be recreated."
326
+ },
327
+ {
328
+ contractId: "CCLW55OIEDHKS5DHDGEA3B2F2ZVOTRXZIOPO36SCMHNQV3VQEGRR33FL",
329
+ label: "guinea-pig C",
330
+ alertThresholdOn: "2026-09-25",
331
+ expiresOn: "2026-09-26",
332
+ why: "Backup natural-decay proof, the only second shot if B is missed."
333
+ }
334
+ ];
335
+ var SHARED_CODE_ENTRY_KEY = "AAAAB8flXwrYnvsGALwVBIsVUJn6TZfO4WRm+hJEs9y86Yv7";
336
+ var SHARED_CODE_UNTIL = "2026-09-26";
337
+ var ProtectedEntryError = class extends Error {
338
+ constructor(message) {
339
+ super(message);
340
+ this.name = "ProtectedEntryError";
341
+ }
342
+ };
343
+ function assertWriteAllowed(args) {
344
+ const acknowledged = new Set(args.options?.acknowledgeProtected ?? []);
345
+ const touched = /* @__PURE__ */ new Set([args.contractId]);
346
+ for (const entryKey of args.entryKeys) {
347
+ for (const consumer of args.scan.entries[entryKey]?.contracts ?? []) {
348
+ touched.add(consumer);
349
+ }
350
+ }
351
+ if (args.entryKeys.includes(SHARED_CODE_ENTRY_KEY) && !acknowledged.has(SHARED_CODE_ENTRY_KEY)) {
352
+ throw new ProtectedEntryError(`Refusing to write: this would extend the SHARED ContractCode entry.
353
+ Guinea-pigs A, B and C are built from one Wasm and share this single entry,
354
+ so extending it extends all three \u2014 including both natural-decay proofs,
355
+ which cross on 2026-09-20 and 2026-09-25 and cannot be re-armed.
356
+ A scan of one contract CANNOT show you this: it reports one consumer,
357
+ because the chain does not index reverse dependencies from one query.
358
+
359
+ It is scheduled for extension at W3-D18-02d, after ${SHARED_CODE_UNTIL}.
360
+ To override: --acknowledge-protected ${SHARED_CODE_ENTRY_KEY}`);
361
+ }
362
+ for (const subject of PROTECTED_ENTRIES) {
363
+ if (!touched.has(subject.contractId))
364
+ continue;
365
+ if (acknowledged.has(subject.contractId))
366
+ continue;
367
+ const viaShared = args.contractId !== subject.contractId;
368
+ throw new ProtectedEntryError(`Refusing to write: this would touch ${subject.label} (${subject.contractId}).
369
+ ` + (viaShared ? ` It is not the contract you named \u2014 it is reached through a SHARED ENTRY.
370
+ A, B and C are built from one Wasm, so extending code extends all three.
371
+ ` : "") + ` ${subject.why}
372
+ It crosses the alert threshold on ${subject.alertThresholdOn} and EXPIRES on
373
+ ${subject.expiresOn} \u2014 be present for the second one, that is the unrepeatable
374
+ event. Extending it now
375
+ moves that crossing past the sprint, and the ageing cannot be recreated.
376
+
377
+ If you genuinely intend this, pass the contract explicitly:
378
+ --acknowledge-protected ${subject.contractId}
379
+ The shared code entry is due to be extended at W3-D18-02d, after ${SHARED_CODE_UNTIL}.`);
380
+ }
381
+ }
382
+
383
+ // ../core/dist/extend.js
384
+ function extensionKey(text) {
385
+ if (!text || Buffer.from(text, "base64").toString("base64") !== text)
386
+ throw new Error("Invalid extension ledger key");
387
+ const key = xdr4.LedgerKey.fromXDR(text, "base64");
388
+ if (key.toXDR("base64") !== text)
389
+ throw new Error("Invalid extension ledger key");
390
+ return key;
391
+ }
392
+ function planExtension(scan, options) {
393
+ const { contractId, additionalLedgers, maxEntryTtl } = options;
394
+ if (scan.network !== "testnet" || !isValidContractId(contractId))
395
+ throw new Error("Extension requires a valid Testnet contract");
396
+ if (!Number.isSafeInteger(additionalLedgers) || additionalLedgers <= 0 || !Number.isSafeInteger(maxEntryTtl) || maxEntryTtl <= 0 || maxEntryTtl > 4294967295)
397
+ throw new Error("Invalid extension increment or network ceiling");
398
+ const keys = /* @__PURE__ */ new Map([[instanceKey(contractId), "instance"]]);
399
+ for (const supplied of options.dataKeys ?? []) {
400
+ const text = supplied.trim();
401
+ const key = extensionKey(text);
402
+ if (key.type !== "contractData" || Address2.fromScAddress(key.contractData.contract).toString() !== contractId || key.contractData.key.type === "scvLedgerKeyContractInstance")
403
+ throw new Error("Expected a data key belonging to this contract");
404
+ keys.set(text, key.contractData.durability.name === "temporary" ? "temporary" : "persistent");
405
+ }
406
+ if (options.includeCode) {
407
+ const code = Object.entries(scan.entries).filter(([, e]) => e.kind === "code" && e.contracts.includes(contractId));
408
+ if (code.length !== 1)
409
+ throw new Error("Cannot identify the selected code entry");
410
+ const key = code[0][0];
411
+ if (extensionKey(key).type !== "contractCode")
412
+ throw new Error("Invalid code entry");
413
+ keys.set(key, "code");
414
+ }
415
+ assertWriteAllowed({
416
+ contractId,
417
+ entryKeys: [...keys.keys()],
418
+ scan,
419
+ ...options.acknowledgeProtected === void 0 ? {} : { options: { acknowledgeProtected: options.acknowledgeProtected } }
420
+ });
421
+ const entries = [...keys].map(([entryKey, kind]) => {
422
+ const entry = scan.entries[entryKey];
423
+ if (!entry || entry.kind !== kind || !entry.contracts.includes(contractId) || entry.ttl.status !== "known" || scan.issues.some((i) => i.kind !== "coverage-limited" && i.kind !== "sharing-undetermined" && (i.entryKey === entryKey || !i.entryKey && i.contracts.includes(contractId))))
424
+ throw new Error("A selected entry is missing or unreadable");
425
+ const { endsAtLedger, remainingLedgers } = entry.ttl;
426
+ if (!Number.isSafeInteger(entry.observedAtLedger) || entry.observedAtLedger < 0 || !Number.isSafeInteger(endsAtLedger) || endsAtLedger > 4294967295 || remainingLedgers !== endsAtLedger - entry.observedAtLedger || hasExpired(remainingLedgers) || !Number.isSafeInteger(remainingLedgers + additionalLedgers))
427
+ throw new Error("A selected entry is expired or has invalid TTL metadata");
428
+ const target = resolveExtendTarget({
429
+ currentRemainingLedgers: remainingLedgers,
430
+ additionalLedgers,
431
+ maxEntryTtl
432
+ });
433
+ return {
434
+ entryKey,
435
+ kind,
436
+ contracts: [...new Set(entry.contracts)],
437
+ before: { observedAtLedger: entry.observedAtLedger, endsAtLedger },
438
+ extendToLedgers: target.extendToLedgers,
439
+ wasCapped: target.wasCapped,
440
+ skip: !needsAction(remainingLedgers, target.extendToLedgers - 1)
441
+ };
442
+ });
443
+ return {
444
+ contractId,
445
+ additionalLedgers,
446
+ entries,
447
+ warnings: [
448
+ "Only selected keys are extended; storage is not enumerated and whole-contract protection is not established.",
449
+ ...options.includeCode ? ["Code can serve other contracts outside this scan; extending it affects all consumers."] : [],
450
+ ...entries.some((e) => e.wasCapped) ? ["The network ceiling limits the requested additional lifetime."] : []
451
+ ]
452
+ };
453
+ }
454
+ function executeExtensions(plan, options, deps) {
455
+ return executeExtensionEntries(plan.entries, options, deps);
456
+ }
457
+ async function executeExtensionEntries(entries, options, deps) {
458
+ const live = options.submit === true;
459
+ if (!options.payer || options.maxFeeStroops !== void 0 && !/^[1-9]\d*$/.test(options.maxFeeStroops) || live && options.maxFeeStroops === void 0) {
460
+ throw new Error("Live extension needs an explicit payer and fee budget");
461
+ }
462
+ if (new Set(entries.map((e) => e.entryKey)).size !== entries.length) {
463
+ throw new Error("Duplicate execution entry keys");
464
+ }
465
+ const budget = options.maxFeeStroops === void 0 ? void 0 : BigInt(options.maxFeeStroops);
466
+ let committed = 0n;
467
+ const records = [];
468
+ const skipped = [];
469
+ const visited = /* @__PURE__ */ new Set();
470
+ let ok = true;
471
+ for (const original of entries) {
472
+ visited.add(original.entryKey);
473
+ if (original.skip) {
474
+ skipped.push(original.entryKey);
475
+ continue;
476
+ }
477
+ let entry = original;
478
+ const recordedAt = deps.now().toISOString();
479
+ const base = () => ({
480
+ entryKey: entry.entryKey,
481
+ contracts: entry.contracts,
482
+ payer: options.payer,
483
+ before: entry.before,
484
+ extendToLedgers: entry.extendToLedgers,
485
+ recordedAt,
486
+ reason: options.reason ?? "Explicit manual extension"
487
+ });
488
+ let sent;
489
+ let knownSigner;
490
+ let confirmed = false;
491
+ try {
492
+ if (deps.refresh) {
493
+ const fresh = await deps.refresh(original);
494
+ if (fresh.entryKey !== original.entryKey || fresh.kind !== original.kind || fresh.contracts.join("\0") !== original.contracts.join("\0")) {
495
+ throw new Error("Refresh changed execution scope");
496
+ }
497
+ entry = fresh;
498
+ if (entry.skip) {
499
+ skipped.push(entry.entryKey);
500
+ continue;
501
+ }
502
+ }
503
+ const prepared = await deps.prepare(entry);
504
+ if (prepared.entry.entryKey !== entry.entryKey || prepared.entry.extendToLedgers !== entry.extendToLedgers || !/^\d+$/.test(prepared.feeStroops) || budget !== void 0 && committed + BigInt(prepared.feeStroops) > budget) {
505
+ throw new Error("Prepared selection or fee budget mismatch");
506
+ }
507
+ await deps.preview(prepared);
508
+ if (!live) {
509
+ records.push({ ...base(), mode: "dry-run", outcome: "simulated" });
510
+ committed += BigInt(prepared.feeStroops);
511
+ continue;
512
+ }
513
+ const signer = deps.signer(prepared, (budget - committed).toString());
514
+ if (signer.payer !== options.payer || signer.identity.account !== prepared.sourceAccount) {
515
+ throw new Error("Signer identity mismatch");
516
+ }
517
+ knownSigner = signer.identity;
518
+ const signed = await signer.signExtendTTL({
519
+ networkPassphrase: "Test SDF Network ; September 2015",
520
+ transactionXdr: prepared.transactionXdr
521
+ });
522
+ await deps.beforeSubmit?.(prepared, signer.identity);
523
+ sent = { hash: prepared.transactionHash, signer: signer.identity };
524
+ committed += BigInt(prepared.feeStroops);
525
+ const response = await deps.submit(prepared, signed);
526
+ if (response.hash !== sent.hash)
527
+ throw new Error("Submission hash mismatch");
528
+ if (response.status === "ERROR") {
529
+ confirmed = true;
530
+ throw new Error("Submission rejected");
531
+ }
532
+ if (!["PENDING", "DUPLICATE"].includes(response.status))
533
+ throw new Error("Submission uncertain");
534
+ const confirmation = await deps.confirm(sent.hash);
535
+ if (confirmation.status === "unconfirmed")
536
+ throw new Error("Confirmation pending");
537
+ confirmed = true;
538
+ if (confirmation.status !== "confirmed")
539
+ throw new Error("Transaction failed");
540
+ const after = await deps.readAfter(entry.entryKey);
541
+ if (!Number.isSafeInteger(after.observedAtLedger) || after.observedAtLedger < confirmation.ledger || !Number.isSafeInteger(after.endsAtLedger) || after.endsAtLedger <= entry.before.endsAtLedger || after.endsAtLedger < confirmation.ledger + entry.extendToLedgers) {
542
+ throw new Error("TTL increase could not be verified");
543
+ }
544
+ records.push({
545
+ ...base(),
546
+ mode: "live",
547
+ outcome: "succeeded",
548
+ transactionHash: sent.hash,
549
+ signer: sent.signer,
550
+ after
551
+ });
552
+ } catch {
553
+ ok = false;
554
+ if (sent && !confirmed) {
555
+ records.push({
556
+ ...base(),
557
+ mode: "live",
558
+ outcome: "submitted",
559
+ transactionHash: sent.hash,
560
+ signer: sent.signer
561
+ });
562
+ } else if (live) {
563
+ records.push({
564
+ ...base(),
565
+ outcome: "failed",
566
+ mode: "live",
567
+ ...sent ? { transactionHash: sent.hash } : {},
568
+ ...knownSigner ? { signer: knownSigner } : {},
569
+ error: {
570
+ code: "EXTENSION_FAILED",
571
+ message: "Extension rejected or post-state unverified. No replacement was submitted."
572
+ }
573
+ });
574
+ } else {
575
+ records.push({
576
+ ...base(),
577
+ outcome: "failed",
578
+ mode: "dry-run",
579
+ error: {
580
+ code: "SIMULATION_FAILED",
581
+ message: "Extension preparation or fee validation failed. Nothing was submitted."
582
+ }
583
+ });
584
+ }
585
+ break;
586
+ }
587
+ }
588
+ return {
589
+ ok,
590
+ mode: live ? "live" : "dry-run",
591
+ records,
592
+ skipped,
593
+ unattempted: entries.filter((e) => !visited.has(e.entryKey)).map((e) => e.entryKey),
594
+ committedFeeStroops: live ? committed.toString() : "0",
595
+ estimatedFeeStroops: committed.toString()
596
+ };
597
+ }
598
+
599
+ // ../core/dist/ed25519-signer.js
600
+ function isValidPayerAccount(value) {
601
+ return StrKey.isValidEd25519PublicKey(value);
602
+ }
603
+ function stroopBudget(value) {
604
+ if (!/^[1-9]\d*$/.test(value))
605
+ throw new Error("Fee budget must be positive integer stroops");
606
+ return BigInt(value);
607
+ }
608
+ function validateExtensionEnvelope(transactionXdr, policy) {
609
+ const tx = TransactionBuilder.fromXDR(transactionXdr, Networks2.TESTNET);
610
+ if (!(tx instanceof Transaction) || !StrKey.isValidEd25519PublicKey(policy.sourceAccount) || tx.source !== policy.sourceAccount || tx.signatures.length !== 0 || tx.operations.length !== 1 || tx.memo.type !== "none" || Buffer.from(tx.hash()).toString("hex") !== policy.expectedHash)
611
+ throw new Error("Extension envelope does not match its approved identity");
612
+ const op = tx.operations[0];
613
+ const envelope = tx.toEnvelope();
614
+ if (op.type !== "extendFootprintTtl" || op.source !== void 0 || op.extendTo !== policy.extendToLedgers || !Number.isSafeInteger(op.extendTo) || op.extendTo <= 0 || envelope.type !== "envelopeTypeTx" || envelope.value.tx.ext.type !== "sorobanData")
615
+ throw new Error("Only the selected TTL extension is permitted");
616
+ const data = envelope.value.tx.ext.value;
617
+ const footprint = data.resources.footprint;
618
+ const key = extensionKey(policy.entryKey);
619
+ if (!["contractData", "contractCode"].includes(key.type) || footprint.readWrite.length !== 0 || footprint.readOnly.length !== 1 || footprint.readOnly[0].toXDR("base64") !== policy.entryKey || data.resourceFee < 0n || BigInt(tx.fee) < data.resourceFee + 100n || BigInt(tx.fee) > stroopBudget(policy.maxFeeStroops))
620
+ throw new Error("Extension footprint or fee is outside policy");
621
+ const now = Math.floor((policy.now ?? (() => Date.now() / 1e3))());
622
+ if (!tx.timeBounds || BigInt(tx.timeBounds.maxTime) <= BigInt(now) || BigInt(tx.timeBounds.maxTime) > BigInt(now + 60) || BigInt(tx.timeBounds.minTime) > BigInt(now) || tx.extraSigners?.length || tx.minAccountSequence !== void 0 || tx.ledgerBounds !== void 0)
623
+ throw new Error("Extension validity bounds are outside policy");
624
+ return tx;
625
+ }
626
+ function createEd25519Signer(options) {
627
+ return {
628
+ payer: options.payer,
629
+ identity: { kind: "ed25519", account: options.sourceAccount },
630
+ async signExtendTTL(request) {
631
+ if (request.networkPassphrase !== Networks2.TESTNET)
632
+ throw new Error("Only Testnet signing is permitted");
633
+ const tx = validateExtensionEnvelope(request.transactionXdr, options);
634
+ let accountMismatch = false;
635
+ try {
636
+ const key = Keypair.fromSecret(options.readSecret());
637
+ if (key.publicKey() !== options.sourceAccount) {
638
+ accountMismatch = true;
639
+ throw new Error("Wrong key");
640
+ }
641
+ tx.sign(key);
642
+ return tx.toXDR();
643
+ } catch {
644
+ throw new Error(accountMismatch ? "Unable to sign the validated extension: the secret does not match the expected source account" : "Unable to sign the validated extension");
645
+ }
646
+ }
647
+ };
648
+ }
649
+
650
+ // ../core/dist/config.js
651
+ var MIN_ACTION_RUNS_IN_WINDOW = 4;
652
+ var ACTION_WINDOW_GAP_BASIS_MINUTES = 331;
653
+ var MIN_SAFE_ACTION_WINDOW_LEDGERS = Math.ceil(ACTION_WINDOW_GAP_BASIS_MINUTES * MIN_ACTION_RUNS_IN_WINDOW * 60 / SECONDS_PER_LEDGER);
654
+
655
+ // ../core/dist/rent.js
656
+ function assertStroops(value, entryKey) {
657
+ if (!/^\d+$/.test(value)) {
658
+ throw new Error(`Quote for ${entryKey} is not a non-negative integer of stroops: ${value}`);
659
+ }
660
+ return value;
661
+ }
662
+ async function estimateRent(scan, args, quoter) {
663
+ const uniform = typeof args.extendToLedgers === "number" ? args.extendToLedgers : void 0;
664
+ const perEntry = typeof args.extendToLedgers === "number" ? void 0 : args.extendToLedgers;
665
+ const targetFor = (entryKey) => uniform ?? perEntry?.[entryKey];
666
+ if (uniform !== void 0 && (!Number.isInteger(uniform) || uniform <= 0)) {
667
+ throw new Error("extendToLedgers must be a positive integer of ledgers");
668
+ }
669
+ const excluded = [];
670
+ const quotable = [];
671
+ let estimatedAtLedger = 0;
672
+ for (const [entryKey, entry] of Object.entries(scan.entries)) {
673
+ estimatedAtLedger = Math.max(estimatedAtLedger, entry.observedAtLedger);
674
+ if (entry.ttl.status === "known" && hasExpired(entry.ttl.remainingLedgers)) {
675
+ excluded.push({
676
+ entryKey,
677
+ reason: "already-expired",
678
+ detail: "Already past its final live ledger \u2014 needs RestoreFootprintOp, not an extend."
679
+ });
680
+ continue;
681
+ }
682
+ quotable.push(entryKey);
683
+ }
684
+ const byTarget = /* @__PURE__ */ new Map();
685
+ for (const entryKey of quotable) {
686
+ const target = targetFor(entryKey);
687
+ if (target === void 0 || !Number.isInteger(target) || target <= 0) {
688
+ throw new Error(`No positive extend target supplied for ${entryKey}`);
689
+ }
690
+ const group = byTarget.get(target);
691
+ if (group)
692
+ group.push(entryKey);
693
+ else
694
+ byTarget.set(target, [entryKey]);
695
+ }
696
+ const quotes = [];
697
+ for (const [target, entryKeys] of byTarget) {
698
+ quotes.push(...await quoter.quote({ entryKeys, extendToLedgers: target }));
699
+ }
700
+ const byKey = /* @__PURE__ */ new Map();
701
+ for (const quote of quotes) {
702
+ if (!quotable.includes(quote.entryKey)) {
703
+ throw new Error(`Quoter returned an unrequested entry: ${quote.entryKey}`);
704
+ }
705
+ if (byKey.has(quote.entryKey)) {
706
+ throw new Error(`Quoter returned two prices for ${quote.entryKey}`);
707
+ }
708
+ byKey.set(quote.entryKey, assertStroops(quote.estimatedRentStroops, quote.entryKey));
709
+ }
710
+ const estimatedRentStroopsByEntry = {};
711
+ let total = 0n;
712
+ for (const entryKey of quotable) {
713
+ const price = byKey.get(entryKey);
714
+ if (price === void 0) {
715
+ excluded.push({
716
+ entryKey,
717
+ reason: "no-quote-returned",
718
+ detail: "The network returned no price for this entry; the total excludes it."
719
+ });
720
+ continue;
721
+ }
722
+ estimatedRentStroopsByEntry[entryKey] = price;
723
+ total += BigInt(price);
724
+ }
725
+ return {
726
+ estimate: {
727
+ estimatedAtLedger,
728
+ // The shared type carries one number; report the largest target when
729
+ // they differ, and the per-entry prices below are authoritative.
730
+ extendToLedgers: uniform ?? Math.max(...byTarget.keys(), 0),
731
+ estimatedRentStroopsByEntry,
732
+ totalEstimatedRentStroops: total.toString()
733
+ },
734
+ excluded
735
+ };
736
+ }
737
+ function stroopsToXlm(stroops) {
738
+ const value = BigInt(stroops);
739
+ const whole = value / 10000000n;
740
+ const fraction = (value % 10000000n).toString().padStart(7, "0");
741
+ return `${whole.toString()}.${fraction}`;
742
+ }
743
+
744
+ // ../core/dist/rent-quoter.js
745
+ import { Account, Keypair as Keypair2, Operation, SorobanDataBuilder, TransactionBuilder as TransactionBuilder2, rpc as rpc3, xdr as xdr5 } from "@stellar/stellar-sdk";
746
+ var NO_OP_TARGET = 1;
747
+ function createSimulatingQuoter(server, options) {
748
+ const sourceAccountId = options.sourceAccountId ?? Keypair2.random().publicKey();
749
+ async function simulateFee(entryKey, extendTo) {
750
+ const source = new Account(sourceAccountId, "0");
751
+ const sorobanData = new SorobanDataBuilder().setReadOnly([xdr5.LedgerKey.fromXDR(entryKey, "base64")]).build();
752
+ const tx = new TransactionBuilder2(source, {
753
+ fee: "100",
754
+ networkPassphrase: options.networkPassphrase
755
+ }).addOperation(Operation.extendFootprintTtl({ extendTo })).setSorobanData(sorobanData).setTimeout(30).build();
756
+ const simulated = await server.simulateTransaction(tx);
757
+ if (rpc3.Api.isSimulationError(simulated)) {
758
+ throw new Error(`Simulation refused to price this entry: ${simulated.error}`);
759
+ }
760
+ return BigInt(simulated.minResourceFee ?? "0");
761
+ }
762
+ async function quoteOne(entryKey, extendToLedgers) {
763
+ const baseline = await simulateFee(entryKey, NO_OP_TARGET);
764
+ const atTarget = await simulateFee(entryKey, extendToLedgers);
765
+ const rent = atTarget > baseline ? atTarget - baseline : 0n;
766
+ return {
767
+ entryKey,
768
+ estimatedRentStroops: rent.toString(),
769
+ minResourceFeeStroops: atTarget.toString(),
770
+ baselineFeeStroops: baseline.toString()
771
+ };
772
+ }
773
+ async function quoteDetailed(args) {
774
+ const out = [];
775
+ for (const entryKey of args.entryKeys) {
776
+ out.push(await quoteOne(entryKey, args.extendToLedgers));
777
+ }
778
+ return out;
779
+ }
780
+ return {
781
+ quoteDetailed,
782
+ quote: (args) => quoteDetailed(args)
783
+ };
784
+ }
785
+
786
+ // ../core/dist/scan-contract.js
787
+ import { Address as Address3, xdr as xdr6 } from "@stellar/stellar-sdk";
788
+ var MAX_KEYS_PER_READ = 200;
789
+ function object(value) {
790
+ return typeof value === "object" && value !== null && !Array.isArray(value);
791
+ }
792
+ function ledger(value) {
793
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4294967295;
794
+ }
795
+ function isCanonicalBase64(text) {
796
+ try {
797
+ return btoa(atob(text)) === text;
798
+ } catch {
799
+ return false;
800
+ }
801
+ }
802
+ function parseKey(value) {
803
+ if (typeof value !== "string")
804
+ throw new Error("Invalid ledger key");
805
+ const text = value.trim();
806
+ if (!text || !isCanonicalBase64(text))
807
+ throw new Error("Invalid ledger key");
808
+ const key = xdr6.LedgerKey.fromXDR(text, "base64");
809
+ if (key.toXDR("base64") !== text)
810
+ throw new Error("Invalid ledger key");
811
+ return key;
812
+ }
813
+ function payloadKey(value) {
814
+ if (value.type === "contractData") {
815
+ const data = value.contractData;
816
+ return xdr6.LedgerKey.contractData(new xdr6.LedgerKeyContractData({
817
+ contract: data.contract,
818
+ key: data.key,
819
+ durability: data.durability
820
+ })).toXDR("base64");
821
+ }
822
+ if (value.type === "contractCode")
823
+ return codeKey(value.contractCode.hash.value);
824
+ throw new Error("Unsupported entry type");
825
+ }
826
+ function scanContract(reader, contract, dataKeys = [], options = {}) {
827
+ return scanContracts(reader, [{ contract, dataKeys, ...options }]);
828
+ }
829
+ async function scanContracts(reader, requests) {
830
+ const entries = {};
831
+ const issues = [];
832
+ const supplied = {};
833
+ const declarations = {};
834
+ const contracts = /* @__PURE__ */ new Map();
835
+ const result = {
836
+ network: "testnet",
837
+ contracts: [],
838
+ entries,
839
+ issues,
840
+ coverage: { mode: "known-keys", dataKeysSuppliedByContract: supplied }
841
+ };
842
+ function issue(kind, message, consumers, key, observedAtLedger) {
843
+ issues.push({
844
+ kind,
845
+ message,
846
+ contracts: [...new Set(consumers)],
847
+ ...key === void 0 ? {} : { entryKey: key },
848
+ ...observedAtLedger === void 0 ? {} : { observedAtLedger }
849
+ });
850
+ }
851
+ const groups = /* @__PURE__ */ new Map();
852
+ if (!Array.isArray(requests)) {
853
+ issue("invalid-response", "Scan requests must be an array.", []);
854
+ return result;
855
+ }
856
+ for (const request of requests) {
857
+ if (!object(request) || !object(request.contract) || typeof request.contract.id !== "string") {
858
+ issue("invalid-response", "Each scan request must contain a contract ID.", []);
859
+ continue;
860
+ }
861
+ const { contract } = request;
862
+ const id = request.contract.id;
863
+ const previous = contracts.get(id);
864
+ const label = typeof contract.label === "string" ? contract.label : void 0;
865
+ contracts.set(id, {
866
+ id,
867
+ ...previous?.label !== void 0 ? { label: previous.label } : label !== void 0 ? { label } : {}
868
+ });
869
+ let instance;
870
+ try {
871
+ instance = instanceKey(id);
872
+ } catch {
873
+ issue("invalid-response", "Invalid contract ID. Expected a Stellar contract address.", [id]);
874
+ continue;
875
+ }
876
+ let group = groups.get(id);
877
+ if (!group) {
878
+ group = { instance, data: /* @__PURE__ */ new Map(), hasData: false, noDataKeys: false, invalid: false };
879
+ groups.set(id, group);
880
+ }
881
+ if (request.noDataKeys !== void 0 && typeof request.noDataKeys !== "boolean") {
882
+ issue("invalid-response", "noDataKeys must be a boolean caller assertion.", [id]);
883
+ group.invalid = true;
884
+ }
885
+ group.noDataKeys ||= request.noDataKeys === true;
886
+ const dataKeys = request.dataKeys === void 0 ? [] : request.dataKeys;
887
+ if (!Array.isArray(dataKeys)) {
888
+ issue("invalid-response", "dataKeys must be an array of serialized LedgerKeys.", [id]);
889
+ group.invalid = true;
890
+ continue;
891
+ }
892
+ group.hasData ||= dataKeys.length > 0;
893
+ for (const input of dataKeys) {
894
+ try {
895
+ const key = parseKey(input);
896
+ if (key.type !== "contractData" || key.contractData.key.type === "scvLedgerKeyContractInstance" || Address3.fromScAddress(key.contractData.contract).toString() !== id)
897
+ throw new Error("Wrong data key");
898
+ const durability = key.contractData.durability.name;
899
+ if (durability !== "persistent" && durability !== "temporary")
900
+ throw new Error("Wrong durability");
901
+ group.data.set(key.toXDR("base64"), durability);
902
+ } catch {
903
+ issue("invalid-response", "Invalid data key: expected persistent/temporary ContractData for this contract.", [id]);
904
+ }
905
+ }
906
+ }
907
+ const instances = /* @__PURE__ */ new Map();
908
+ const data = /* @__PURE__ */ new Map();
909
+ function expectEntry(target, key, kind, id) {
910
+ const existing = target.get(key);
911
+ if (!existing)
912
+ target.set(key, { kind, contracts: [id] });
913
+ else if (!existing.contracts.includes(id))
914
+ existing.contracts.push(id);
915
+ }
916
+ for (const [id, group] of groups) {
917
+ supplied[id] = group.data.size;
918
+ if (group.noDataKeys)
919
+ declarations[id] = true;
920
+ if (group.noDataKeys && group.hasData) {
921
+ issue("invalid-response", "Cannot declare no data keys while supplying data keys for the same contract.", [id]);
922
+ group.invalid = true;
923
+ }
924
+ if (group.invalid)
925
+ continue;
926
+ expectEntry(instances, group.instance, "instance", id);
927
+ for (const [key, kind] of group.data)
928
+ expectEntry(data, key, kind, id);
929
+ }
930
+ async function read(expected) {
931
+ const decoded = /* @__PURE__ */ new Map();
932
+ const keys = [...expected.keys()];
933
+ for (let start = 0; start < keys.length; start += MAX_KEYS_PER_READ) {
934
+ const batch = keys.slice(start, start + MAX_KEYS_PER_READ);
935
+ const consumers = [...new Set(batch.flatMap((key) => expected.get(key).contracts))];
936
+ let response;
937
+ try {
938
+ response = await reader.read(batch);
939
+ } catch {
940
+ issue("rpc-error", "RPC read failed for a batch; retry the scan. Successful batches are retained.", consumers);
941
+ continue;
942
+ }
943
+ if (!object(response) || !ledger(response.latestLedger) || !Array.isArray(response.entries)) {
944
+ issue("invalid-response", "RPC response must contain a valid latestLedger and entries array.", consumers);
945
+ continue;
946
+ }
947
+ const observedAtLedger = response.latestLedger;
948
+ const seen = /* @__PURE__ */ new Set();
949
+ const requested = new Set(batch);
950
+ for (const row of response.entries) {
951
+ let key;
952
+ try {
953
+ if (!object(row))
954
+ throw new Error("Invalid row");
955
+ key = parseKey(row.key).toXDR("base64");
956
+ } catch {
957
+ issue("invalid-response", "RPC returned a malformed ledger key.", consumers, void 0, observedAtLedger);
958
+ continue;
959
+ }
960
+ if (!requested.has(key)) {
961
+ issue("invalid-response", "RPC returned an unrequested entry.", consumers, void 0, observedAtLedger);
962
+ continue;
963
+ }
964
+ if (seen.has(key)) {
965
+ delete entries[key];
966
+ decoded.delete(key);
967
+ issue("invalid-response", "RPC returned a duplicate entry; its observation was discarded.", expected.get(key).contracts, key, observedAtLedger);
968
+ continue;
969
+ }
970
+ seen.add(key);
971
+ try {
972
+ if (!object(row) || typeof row.entryXdr !== "string" || row.liveUntilLedgerSeq !== void 0 && !ledger(row.liveUntilLedgerSeq))
973
+ throw new Error("Invalid row");
974
+ const value = xdr6.LedgerEntryData.fromXDR(row.entryXdr, "base64");
975
+ if (value.toXDR("base64") !== row.entryXdr || payloadKey(value) !== key)
976
+ throw new Error("Mismatched payload");
977
+ const kind = expected.get(key)?.kind;
978
+ if (kind === void 0)
979
+ throw new Error("Unexpected key");
980
+ if (kind === "instance" && (value.type !== "contractData" || value.contractData.val.type !== "scvContractInstance")) {
981
+ throw new Error("Invalid instance payload");
982
+ }
983
+ const lifecycle = kind === "temporary" ? { kind, endBehavior: "deleted" } : {
984
+ kind,
985
+ endBehavior: "archived"
986
+ };
987
+ entries[key] = {
988
+ ...lifecycle,
989
+ contracts: [...expected.get(key).contracts],
990
+ observedAtLedger,
991
+ ttl: observeTTL({ liveUntilLedgerSeq: row.liveUntilLedgerSeq, observedAtLedger })
992
+ };
993
+ decoded.set(key, value);
994
+ } catch {
995
+ issue("invalid-response", "RPC entry payload or TTL is invalid or does not match its requested key.", expected.get(key).contracts, key, observedAtLedger);
996
+ }
997
+ }
998
+ for (const key of batch) {
999
+ if (!seen.has(key))
1000
+ issue("entry-not-found", "No entry returned. Absence is not proof of archival or deletion.", expected.get(key).contracts, key, observedAtLedger);
1001
+ }
1002
+ }
1003
+ return decoded;
1004
+ }
1005
+ const observed = await read(instances);
1006
+ for (const [instance] of instances) {
1007
+ const instanceValue = observed.get(instance);
1008
+ if (!instanceValue)
1009
+ continue;
1010
+ if (instanceValue.type !== "contractData" || instanceValue.contractData.val.type !== "scvContractInstance")
1011
+ continue;
1012
+ const executable = instanceValue.contractData.val.instance.executable;
1013
+ const consumers = instances.get(instance).contracts;
1014
+ if (executable.type === "contractExecutableWasm") {
1015
+ for (const id of consumers)
1016
+ expectEntry(data, codeKey(executable.wasmHash.value), "code", id);
1017
+ } else {
1018
+ issue("unsupported-executable", "Instance has a non-Wasm executable; this scanner does not discover its code.", consumers, instance, entries[instance]?.observedAtLedger);
1019
+ }
1020
+ }
1021
+ await read(data);
1022
+ return {
1023
+ ...result,
1024
+ contracts: [...contracts.values()],
1025
+ coverage: {
1026
+ mode: "known-keys",
1027
+ dataKeysSuppliedByContract: supplied,
1028
+ ...Object.keys(declarations).length ? { noDataKeysDeclaredByContract: declarations } : {}
1029
+ }
1030
+ };
1031
+ }
1032
+
1033
+ // ../core/dist/extend-rpc.js
1034
+ import { Account as Account2, Networks as Networks3, Operation as Operation2, SorobanDataBuilder as SorobanDataBuilder2, StrKey as StrKey2, Transaction as Transaction2, TransactionBuilder as TransactionBuilder3, rpc as rpc4, xdr as xdr7 } from "@stellar/stellar-sdk";
1035
+ async function prepareExtension(server, entry, sourceAccount) {
1036
+ if (!StrKey2.isValidEd25519PublicKey(sourceAccount) || entry.skip)
1037
+ throw new Error("Invalid extension payer or no-op");
1038
+ if ((await server.getNetwork()).passphrase !== Networks3.TESTNET)
1039
+ throw new Error("RPC is not Stellar Testnet");
1040
+ const account = await server.getAccount(sourceAccount);
1041
+ if (account.accountId() !== sourceAccount)
1042
+ throw new Error("RPC returned another payer");
1043
+ const tx = new TransactionBuilder3(new Account2(sourceAccount, account.sequenceNumber()), {
1044
+ fee: "100",
1045
+ networkPassphrase: Networks3.TESTNET
1046
+ }).addOperation(Operation2.extendFootprintTtl({ extendTo: entry.extendToLedgers })).setSorobanData(new SorobanDataBuilder2().setReadOnly([extensionKey(entry.entryKey)]).build()).setTimeout(60).build();
1047
+ const simulation = await server.simulateTransaction(tx);
1048
+ if (!rpc4.Api.isSimulationSuccess(simulation) || "restorePreamble" in simulation || typeof simulation.minResourceFee !== "string" || !/^\d+$/.test(simulation.minResourceFee) || !Number.isSafeInteger(simulation.latestLedger) || simulation.latestLedger < entry.before.observedAtLedger || simulation.latestLedger > entry.before.endsAtLedger || simulation.transactionData.build().resourceFee !== BigInt(simulation.minResourceFee))
1049
+ throw new Error("Extension simulation failed or returned invalid resources");
1050
+ const prepared = rpc4.assembleTransaction(tx, simulation).build();
1051
+ const transactionXdr = prepared.toXDR();
1052
+ const transactionHash = Buffer.from(prepared.hash()).toString("hex");
1053
+ validateExtensionEnvelope(transactionXdr, {
1054
+ sourceAccount,
1055
+ entryKey: entry.entryKey,
1056
+ extendToLedgers: entry.extendToLedgers,
1057
+ expectedHash: transactionHash,
1058
+ maxFeeStroops: prepared.fee
1059
+ });
1060
+ return {
1061
+ entry,
1062
+ sourceAccount,
1063
+ transactionXdr,
1064
+ transactionHash,
1065
+ feeStroops: prepared.fee,
1066
+ simulatedAtLedger: simulation.latestLedger
1067
+ };
1068
+ }
1069
+ async function confirmExtension(server, hash, options = {}) {
1070
+ const attempts = options.attempts ?? 12;
1071
+ if (!Number.isSafeInteger(attempts) || attempts < 1 || attempts > 60)
1072
+ throw new Error("Invalid confirmation bound");
1073
+ for (let i = 0; i < attempts; i++) {
1074
+ const response = await server.getTransaction(hash);
1075
+ if (response.txHash !== hash)
1076
+ throw new Error("Transaction confirmation hash mismatch");
1077
+ if (response.status === "SUCCESS" || response.status === "FAILED") {
1078
+ if (!response.envelopeXdr)
1079
+ throw new Error("Missing transaction confirmation envelope");
1080
+ const transaction = TransactionBuilder3.fromXDR(response.envelopeXdr.toXDR("base64"), Networks3.TESTNET);
1081
+ if (Buffer.from(transaction.hash()).toString("hex") !== hash)
1082
+ throw new Error("Transaction confirmation envelope hash mismatch");
1083
+ }
1084
+ if (response.status === "FAILED")
1085
+ return { status: "failed" };
1086
+ if (response.status === "SUCCESS") {
1087
+ if (!Number.isSafeInteger(response.ledger) || response.ledger <= 0)
1088
+ throw new Error("Invalid inclusion ledger");
1089
+ return { status: "confirmed", ledger: response.ledger };
1090
+ }
1091
+ if (response.status !== "NOT_FOUND")
1092
+ throw new Error("Unknown transaction status");
1093
+ if (i + 1 < attempts)
1094
+ await (options.sleep ?? (() => new Promise((resolve) => setTimeout(resolve, 1e3))))();
1095
+ }
1096
+ return { status: "unconfirmed" };
1097
+ }
1098
+ async function submitExtension(server, prepared, signedXdr) {
1099
+ const signed = TransactionBuilder3.fromXDR(signedXdr, Networks3.TESTNET);
1100
+ if (!(signed instanceof Transaction2) || signed.source !== prepared.sourceAccount || Buffer.from(signed.hash()).toString("hex") !== prepared.transactionHash || signed.signatures.length !== 1)
1101
+ throw new Error("Signed envelope differs from the prepared transaction");
1102
+ if ((await server.getNetwork()).passphrase !== Networks3.TESTNET)
1103
+ throw new Error("RPC is not Stellar Testnet");
1104
+ const response = await server.sendTransaction(signed);
1105
+ if (response.hash !== prepared.transactionHash)
1106
+ throw new Error("Submission hash mismatch");
1107
+ return response;
1108
+ }
1109
+
1110
+ // ../core/dist/optimizer-evidence.js
1111
+ var STORAGE_ADVICE_EVIDENCE = {
1112
+ rent: {
1113
+ id: "guinea-pig-a-2026-09-09",
1114
+ recordedAt: "2026-09-09T10:58:52Z",
1115
+ source: "https://github.com/Fatihmaull/evergreen/blob/main/packages/core/test/fixtures/extendTTL-fees-guinea-pig-a.json",
1116
+ persistent: {
1117
+ rentStroops: "103849",
1118
+ totalFeeStroops: "106308",
1119
+ dataBytes: 88,
1120
+ keyBytes: 76,
1121
+ ledgersExtended: 1312937
1122
+ },
1123
+ temporary: {
1124
+ rentStroops: "53196",
1125
+ totalFeeStroops: "55655",
1126
+ dataBytes: 88,
1127
+ keyBytes: 76,
1128
+ ledgersExtended: 1312939
1129
+ },
1130
+ qualification: "Historical measured rent at equal encoded sizes over durations differing by two ledgers; not a quote or savings forecast for this entry."
1131
+ },
1132
+ settings: {
1133
+ recordedOn: "2026-09-05",
1134
+ observedAtLedger: 4519665,
1135
+ minTemporaryTtl: 720,
1136
+ minPersistentTtl: 120960,
1137
+ source: "https://github.com/Fatihmaull/evergreen/blob/main/docs/evidence/2026-09-05-ttl-boundary/state-archival-settings.json"
1138
+ },
1139
+ deletion: {
1140
+ recordedOn: "2026-09-06",
1141
+ subject: "isolated temporary-entry experiment",
1142
+ lastLiveLedger: 4529810,
1143
+ firstAbsentLedger: 4529811,
1144
+ source: "https://github.com/Fatihmaull/evergreen/blob/main/docs/evidence/2026-09-06-ttl-boundary/README.md"
1145
+ }
1146
+ };
1147
+
1148
+ // ../core/dist/optimizer.js
1149
+ function ledger2(value) {
1150
+ return Number.isSafeInteger(value) && value >= 0 && value <= 4294967295;
1151
+ }
1152
+ function lifetime(value) {
1153
+ return ledger2(value) && value > 0;
1154
+ }
1155
+ function analyzeStorage(scan, context = {}) {
1156
+ if (scan.network !== "testnet")
1157
+ throw new Error("Storage advice requires Testnet observations");
1158
+ const limitations = [
1159
+ "Only observed keys were analyzed; storage was not enumerated. No size, duplicate-content or global-consumer inference is supported.",
1160
+ "Advice is conditional on application requirements; it does not authorize a transaction or change scan health exits."
1161
+ ];
1162
+ const settings = context.settings;
1163
+ const validSettings = settings != null && lifetime(settings.minTemporaryTtl) && lifetime(settings.minPersistentTtl) && ledger2(settings.observedAtLedger);
1164
+ if (!validSettings)
1165
+ limitations.push("Current minimum lifetimes are unavailable. Historical settings below are a dated reference, not current configuration or this entry's expiry.");
1166
+ const quote = context.quote;
1167
+ const validQuote = quote != null && typeof quote.rentByEntry === "object" && quote.rentByEntry !== null && !Array.isArray(quote.rentByEntry) && ledger2(quote.pricedAtLedger) && Number.isSafeInteger(quote.additionalLedgers) && quote.additionalLedgers > 0;
1168
+ const rents = {};
1169
+ if (validQuote) {
1170
+ for (const [key, entry] of Object.entries(scan.entries)) {
1171
+ const amount = quote.rentByEntry[key];
1172
+ if (typeof amount === "string" && /^(0|[1-9]\d*)$/.test(amount) && quote.pricedAtLedger >= entry.observedAtLedger)
1173
+ rents[key] = amount;
1174
+ }
1175
+ }
1176
+ const cleanContext = {
1177
+ ...validSettings ? {
1178
+ settings: {
1179
+ minTemporaryTtl: settings.minTemporaryTtl,
1180
+ minPersistentTtl: settings.minPersistentTtl,
1181
+ observedAtLedger: settings.observedAtLedger
1182
+ }
1183
+ } : {},
1184
+ ...validQuote ? {
1185
+ quote: {
1186
+ rentByEntry: rents,
1187
+ pricedAtLedger: quote.pricedAtLedger,
1188
+ additionalLedgers: quote.additionalLedgers
1189
+ }
1190
+ } : {}
1191
+ };
1192
+ const findings = [];
1193
+ const readIssues = scan.issues.filter((i) => i.kind !== "coverage-limited" && i.kind !== "sharing-undetermined");
1194
+ let unreadable = readIssues.length > 0;
1195
+ for (const [entryKey, entry] of Object.entries(scan.entries)) {
1196
+ if (entry.ttl.status !== "known" || !ledger2(entry.observedAtLedger) || !ledger2(entry.ttl.endsAtLedger) || !Number.isSafeInteger(entry.ttl.remainingLedgers) || entry.ttl.remainingLedgers !== entry.ttl.endsAtLedger - entry.observedAtLedger || readIssues.some((i) => i.entryKey === entryKey)) {
1197
+ unreadable = true;
1198
+ continue;
1199
+ }
1200
+ if (entry.kind === "instance")
1201
+ continue;
1202
+ const base = {
1203
+ entryKey,
1204
+ knownConsumers: [...new Set(entry.contracts)],
1205
+ observedAtLedger: entry.observedAtLedger,
1206
+ ttl: { ...entry.ttl },
1207
+ currentRent: rents[entryKey] === void 0 ? { status: "unavailable" } : { status: "quoted", stroops: rents[entryKey] }
1208
+ };
1209
+ if (entry.kind === "code") {
1210
+ findings.push({
1211
+ ...base,
1212
+ code: "shared-code-dependency",
1213
+ action: "Monitor this code key once with its known consumers; checking their instances alone does not establish protection.",
1214
+ rationale: "Every contract built from the same Wasm uses this code entry. Other consumers may exist outside this scan; sharing is not duplicated storage or a measured saving."
1215
+ });
1216
+ } else if (entry.kind === "temporary") {
1217
+ findings.push({
1218
+ ...base,
1219
+ code: "temporary-retention",
1220
+ benchmarkId: "guinea-pig-a-2026-09-09",
1221
+ action: "Compare the observed TTL with intended retention. If data must survive expiry, evaluate persistent storage in contract source; otherwise permit deliberate expiry.",
1222
+ rationale: "Temporary state is deleted, not archived or restorable. The isolated 2026-09-06 experiment observed the final live ledger and absence at the next ledger; it does not measure this entry's deletion."
1223
+ });
1224
+ } else if (entry.kind === "persistent") {
1225
+ findings.push({
1226
+ ...base,
1227
+ code: "durability-review",
1228
+ benchmarkId: "guinea-pig-a-2026-09-09",
1229
+ action: "Only if this data is disposable or recomputable, evaluate temporary storage in contract source. Keep balances, required configuration and durable state persistent.",
1230
+ rationale: "Historical A persistent rent was about 1.95 times the temporary rent at equal encoded sizes. This is a measured comparison, not guaranteed savings or evidence of equal contents."
1231
+ });
1232
+ }
1233
+ }
1234
+ if (unreadable)
1235
+ limitations.push("Some requested entries are missing or unreadable; they receive no inferred retention or cost advice.");
1236
+ if (findings.some((f) => f.currentRent.status === "unavailable"))
1237
+ limitations.push("Current rent is unavailable for some findings. Use --cost for quotes where supported; historical benchmark amounts are not per-entry quotes.");
1238
+ if (findings.length === 0)
1239
+ limitations.push("No supported recommendation for these observations; this does not mean the contract is fully optimized.");
1240
+ return {
1241
+ scope: "observed-keys-only",
1242
+ context: cleanContext,
1243
+ findings,
1244
+ limitations,
1245
+ evidence: STORAGE_ADVICE_EVIDENCE
1246
+ };
1247
+ }
1248
+
1249
+ // ../core/dist/engine-execution-plan.js
1250
+ import { Address as Address4 } from "@stellar/stellar-sdk";
1251
+
1252
+ // src/extend.ts
1253
+ var EXTEND_HELP = `usage: evergreen extend <contract-id> --ledgers N
1254
+ [--source-account G...] [--keys-file path] [--include-code] [--json]
1255
+ [--submit --secret-env NAME --max-fee-stroops N]
1256
+ --dry-run simulate only and say so. This is already the default; the flag
1257
+ exists so a script can state its own safety rather than rely on
1258
+ an absence. Mutually exclusive with --submit.
1259
+
1260
+ Testnet only. Default: simulate, never sign or submit. Supply a public payer
1261
+ with --source-account or EVERGREEN_SOURCE_ACCOUNT; there is no fallback payer.
1262
+ --ledgers N adds N ledgers to each selected entry's current remaining TTL.
1263
+ The operation target is capped at max_entry_ttl - 1; capping is reported.
1264
+ Selects instance by default. A keys file adds explicit data keys:
1265
+ { "dataKeys": ["base64 XDR LedgerKey", ...] }. Storage is not enumerated.
1266
+ --include-code explicitly includes Wasm shared with potentially unseen consumers.
1267
+ --submit requires an exported secret variable NAME and an aggregate fee cap in
1268
+ integer stroops. Never put the secret itself in arguments. No .env auto-loading.
1269
+ No automatic restore or funding. No replacement send after uncertain results.
1270
+ Exit 0: complete simulation, no-op, or verified live result; 2: error or partial/
1271
+ unconfirmed result. Simulation success does not mean TTL changed.`;
1272
+ function extensionPreview(p) {
1273
+ return `Selected ${p.entry.kind} ${p.entry.entryKey}
1274
+ Known consumers: ${p.entry.contracts.join(", ")}
1275
+ Payer: ${p.sourceAccount}
1276
+ Prepared hash (not yet sent): ${p.transactionHash}
1277
+ Before: ledger ${p.entry.before.observedAtLedger}, live until ${p.entry.before.endsAtLedger}
1278
+ Target remaining: ${p.entry.extendToLedgers}${p.entry.wasCapped ? " (CAPPED)" : ""}; prepared fee cap: ${p.feeStroops} stroops
1279
+ Selected scope only. Code may have consumers outside this scan.`;
1280
+ }
1281
+ async function runExtendCli(args, deps) {
1282
+ const fail = (message) => ({ stdout: "", stderr: message, exitCode: 2 });
1283
+ if (args.length === 2 && args[1] === "--help")
1284
+ return { stdout: EXTEND_HELP, stderr: "", exitCode: 0 };
1285
+ const contractId = args[1];
1286
+ if (args[0] !== "extend" || !contractId || !isValidContractId(contractId))
1287
+ return fail("Expected a valid contract ID.\n" + EXTEND_HELP);
1288
+ const values = /* @__PURE__ */ new Map();
1289
+ const flags = /* @__PURE__ */ new Set();
1290
+ const valueOptions = /* @__PURE__ */ new Set([
1291
+ "--ledgers",
1292
+ "--source-account",
1293
+ "--keys-file",
1294
+ "--secret-env",
1295
+ "--max-fee-stroops"
1296
+ ]);
1297
+ for (let i = 2; i < args.length; i++) {
1298
+ const arg = args[i];
1299
+ if (values.has(arg) || flags.has(arg)) return fail("Repeated extend option.");
1300
+ if (valueOptions.has(arg)) {
1301
+ const value = args[++i];
1302
+ if (!value || value.startsWith("-")) return fail("Missing extend option value.");
1303
+ values.set(arg, value);
1304
+ } else if (["--submit", "--dry-run", "--json", "--include-code"].includes(arg)) flags.add(arg);
1305
+ else return fail("Unknown or conflicting extend option.\n" + EXTEND_HELP);
1306
+ }
1307
+ const rawLedgers = values.get("--ledgers") ?? "";
1308
+ const additionalLedgers = Number(rawLedgers);
1309
+ const sourceAccount = values.get("--source-account") ?? deps.sourceAccount;
1310
+ if (flags.has("--submit") && flags.has("--dry-run"))
1311
+ return fail(
1312
+ "--submit and --dry-run are mutually exclusive.\n Dry-run is already the default; drop --dry-run to submit, or drop --submit to simulate."
1313
+ );
1314
+ const submit = flags.has("--submit");
1315
+ const secretEnv = values.get("--secret-env");
1316
+ const maxFeeStroops = values.get("--max-fee-stroops");
1317
+ if (!/^[1-9]\d*$/.test(rawLedgers) || !Number.isSafeInteger(additionalLedgers))
1318
+ return fail("--ledgers needs a positive safe integer.");
1319
+ if (!sourceAccount || !isValidPayerAccount(sourceAccount))
1320
+ return fail("A valid public payer is required (--source-account or EVERGREEN_SOURCE_ACCOUNT).");
1321
+ if (maxFeeStroops !== void 0 && !/^[1-9]\d*$/.test(maxFeeStroops) || submit && (!secretEnv || !maxFeeStroops) || !submit && secretEnv !== void 0 || secretEnv !== void 0 && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(secretEnv))
1322
+ return fail(
1323
+ "--submit requires --secret-env NAME and --max-fee-stroops N; secret selection is live-only."
1324
+ );
1325
+ let dataKeys = [];
1326
+ const keysPath = values.get("--keys-file");
1327
+ if (keysPath !== void 0) {
1328
+ try {
1329
+ const parsed = JSON.parse(await deps.readKeysFile(keysPath));
1330
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !("dataKeys" in parsed) || !Array.isArray(parsed.dataKeys) || parsed.dataKeys.some((k) => typeof k !== "string"))
1331
+ return fail("Invalid keys file: expected dataKeys string array.");
1332
+ dataKeys = parsed.dataKeys;
1333
+ } catch {
1334
+ return fail("Unable to read a valid keys file.");
1335
+ }
1336
+ }
1337
+ try {
1338
+ const report = await deps.run(
1339
+ {
1340
+ contractId,
1341
+ additionalLedgers,
1342
+ sourceAccount,
1343
+ dataKeys,
1344
+ includeCode: flags.has("--include-code"),
1345
+ submit,
1346
+ ...secretEnv === void 0 ? {} : { secretEnv },
1347
+ ...maxFeeStroops === void 0 ? {} : { maxFeeStroops }
1348
+ },
1349
+ (text) => deps.preview?.(text)
1350
+ );
1351
+ const stdout = flags.has("--json") ? JSON.stringify(report, null, 2) : [
1352
+ `Mode: ${report.result.mode}. ${report.result.ok ? "Complete" : "Incomplete"}.`,
1353
+ ...report.plan.warnings,
1354
+ ...report.previews.map(extensionPreview),
1355
+ ...report.result.records.map(
1356
+ (r) => `${r.entryKey}: ${r.outcome}${"transactionHash" in r && r.transactionHash ? ` (${r.transactionHash})` : ""}`
1357
+ ),
1358
+ ...report.result.skipped.map((k) => `${k}: no-op (target already satisfied)`),
1359
+ ...report.result.unattempted.map((k) => `${k}: not attempted`),
1360
+ "A submitted/unconfirmed hash must be reconciled before retrying; no automatic replacement was sent."
1361
+ ].join("\n");
1362
+ return {
1363
+ stdout,
1364
+ stderr: report.result.ok ? "" : "Extension incomplete. Inspect per-entry outcomes and reconcile any submitted hash before retrying.",
1365
+ exitCode: report.result.ok ? 0 : 2
1366
+ };
1367
+ } catch (error) {
1368
+ if (error instanceof ProtectedEntryError) return fail(error.message);
1369
+ return fail(
1370
+ "Extension preparation failed. Check Testnet RPC, selected live keys, public payer and network configuration. No raw provider details are printed."
1371
+ );
1372
+ }
1373
+ }
1374
+
1375
+ // src/optimizer.ts
1376
+ function formatStorageAdvice(report) {
1377
+ const lines = ["Storage advice \u2014 observed keys only"];
1378
+ const settings = report.context.settings;
1379
+ if (settings) {
1380
+ lines.push(
1381
+ `Network minimum lifetimes at ledger ${settings.observedAtLedger}: temporary ${settings.minTemporaryTtl}, persistent ${settings.minPersistentTtl} ledgers.`
1382
+ );
1383
+ } else {
1384
+ const old = report.evidence.settings;
1385
+ lines.push(
1386
+ `Historical minimum lifetimes (${old.recordedOn}, ledger ${old.observedAtLedger}; not current configuration): temporary ${old.minTemporaryTtl}, persistent ${old.minPersistentTtl} ledgers.`,
1387
+ `Source: ${old.source}`
1388
+ );
1389
+ }
1390
+ lines.push(
1391
+ "Minimum lifetime includes the current ledger; it is not the expiry of an already-existing entry."
1392
+ );
1393
+ for (const finding of report.findings) {
1394
+ lines.push(
1395
+ "",
1396
+ `${finding.code}: ${finding.entryKey}`,
1397
+ `Known consumers: ${finding.knownConsumers.join(", ")}`,
1398
+ `Action: ${finding.action}`,
1399
+ `Why: ${finding.rationale}`
1400
+ );
1401
+ if (finding.ttl.status === "known")
1402
+ lines.push(
1403
+ `Observed at ledger ${finding.observedAtLedger}: ${finding.ttl.remainingLedgers} remaining; last live ledger ${finding.ttl.endsAtLedger}.`
1404
+ );
1405
+ if (finding.currentRent.status === "quoted") {
1406
+ const quote = report.context.quote;
1407
+ lines.push(
1408
+ `Current rent quote: ${finding.currentRent.stroops} stroops; pricing context ledger ${quote.pricedAtLedger}, requested increment ${quote.additionalLedgers}. Estimate, not a price guarantee; excludes other transaction fees.`
1409
+ );
1410
+ } else lines.push("Current rent: unavailable (not zero).");
1411
+ }
1412
+ if (report.findings.some((f) => f.benchmarkId !== void 0)) {
1413
+ const benchmark = report.evidence.rent;
1414
+ lines.push(
1415
+ "",
1416
+ `Historical A benchmark (${benchmark.recordedAt}): persistent rent ${benchmark.persistent.rentStroops} versus temporary rent ${benchmark.temporary.rentStroops} stroops; about 1.95x.`,
1417
+ benchmark.qualification,
1418
+ `Source: ${benchmark.source}`
1419
+ );
1420
+ }
1421
+ if (report.findings.some((f) => f.code === "temporary-retention")) {
1422
+ const deletion = report.evidence.deletion;
1423
+ lines.push(
1424
+ `Observed deletion reference: ${deletion.subject} (${deletion.recordedOn}); present at ${deletion.lastLiveLedger}, absent at ${deletion.firstAbsentLedger}.`,
1425
+ `Source: ${deletion.source}`
1426
+ );
1427
+ }
1428
+ lines.push("", ...report.limitations.map((note) => `Limit: ${note}`));
1429
+ return lines;
1430
+ }
1431
+
1432
+ // src/cost.ts
1433
+ function approximateXlm(stroops) {
1434
+ const exact = Number(stroopsToXlm(stroops));
1435
+ if (exact === 0) return "0 XLM";
1436
+ const magnitude = Math.floor(Math.log10(Math.abs(exact)));
1437
+ const factor = 10 ** (magnitude - 1);
1438
+ const rounded = Math.round(exact / factor) * factor;
1439
+ const decimals = Math.max(0, 1 - magnitude);
1440
+ return `about ${rounded.toFixed(decimals)} XLM`;
1441
+ }
1442
+ function formatCost(cost) {
1443
+ const lines = [];
1444
+ const entries = `${cost.entryCount} entr${cost.entryCount === 1 ? "y" : "ies"}`;
1445
+ lines.push(`Cost to extend ${entries} by ${formatCount(cost.additionalLedgers)} more ledgers`);
1446
+ lines.push(
1447
+ ` total ${approximateXlm(cost.totalStroops)} (${formatCount(Number(cost.totalStroops))} stroops) \u2014 what leaves the account`
1448
+ );
1449
+ lines.push(
1450
+ ` rent ${approximateXlm(cost.rentStroops)} (${formatCount(Number(cost.rentStroops))} stroops)`
1451
+ );
1452
+ lines.push(
1453
+ ` fees ${approximateXlm(cost.otherStroops)} (${formatCount(Number(cost.otherStroops))} stroops) \u2014 non-refundable resource + base fee`
1454
+ );
1455
+ const rentEntries = Object.entries(cost.rentByEntry);
1456
+ if (rentEntries.length > 1) {
1457
+ const [topKey, topRent] = rentEntries.reduce((a, b) => BigInt(b[1]) > BigInt(a[1]) ? b : a);
1458
+ const share = Number(BigInt(topRent) * 100n / (BigInt(cost.rentStroops) || 1n));
1459
+ if (share >= 60) {
1460
+ lines.push("");
1461
+ lines.push(
1462
+ ` ${share}% of that rent is one entry (${topKey.slice(0, 10)}\u2026). Code entries hold the`
1463
+ );
1464
+ lines.push(
1465
+ " Wasm and are usually the expensive one \u2014 and the one shared between contracts."
1466
+ );
1467
+ }
1468
+ }
1469
+ lines.push("");
1470
+ lines.push(
1471
+ ` Priced by simulating against the network at ledger ${formatCount(cost.pricedAtLedger)}.`
1472
+ );
1473
+ lines.push(" Rent pricing varies with network state \u2014 a quote taken on another day has");
1474
+ lines.push(" differed by ~18%. This is an estimate to budget against, not a quoted price.");
1475
+ if (cost.cappedEntryCount > 0) {
1476
+ lines.push("");
1477
+ lines.push(
1478
+ ` \u26A0 ${cost.cappedEntryCount} entr${cost.cappedEntryCount === 1 ? "y was" : "ies were"} CAPPED at the operation maximum of ${formatCount(cost.maxEntryTtl - 1)} ledgers`
1479
+ );
1480
+ lines.push(" (~180 days). Those entries get less than requested; the price above reflects");
1481
+ lines.push(" the capped extension, not the request.");
1482
+ }
1483
+ lines.push("");
1484
+ lines.push(" Targets are computed from TTL read now, and the ledger advances before");
1485
+ lines.push(" submission. Two consequences, in opposite directions:");
1486
+ lines.push(" \xB7 REMAINING TTL, measured after inclusion, lands a few ledgers UNDER target");
1487
+ lines.push(" \xB7 ABSOLUTE EXPIRY moves a few ledgers PAST the request (+N plus the gap)");
1488
+ return lines;
1489
+ }
1490
+
1491
+ // src/scan.ts
1492
+ var ANSI = {
1493
+ healthy: "\x1B[32m",
1494
+ warning: "\x1B[33m",
1495
+ critical: "\x1B[31m",
1496
+ unknown: "\x1B[35m"
1497
+ };
1498
+ var RESET = "\x1B[0m";
1499
+ function paint(health, text, color) {
1500
+ return color ? `${ANSI[health]}${text}${RESET}` : text;
1501
+ }
1502
+ var LABEL = {
1503
+ healthy: "HEALTHY",
1504
+ warning: "WARNING",
1505
+ critical: "CRITICAL",
1506
+ unknown: "UNKNOWN"
1507
+ };
1508
+ var EXIT_OK = 0;
1509
+ var EXIT_BELOW_THRESHOLD = 1;
1510
+ var EXIT_ERROR = 2;
1511
+ var EXIT_INCOMPLETE = 3;
1512
+ var DEFAULT_THRESHOLD_LEDGERS = 17280;
1513
+ function tiers(thresholdLedgers) {
1514
+ return resolveHealthThresholds({ bumpWhenRemainingLedgersBelow: thresholdLedgers });
1515
+ }
1516
+ function healthReport(result, thresholdLedgers) {
1517
+ const thresholds = tiers(thresholdLedgers);
1518
+ const byEntry = {};
1519
+ for (const [key, entry] of Object.entries(result.entries)) {
1520
+ byEntry[key] = assessEntryWithThresholds(entry, thresholds);
1521
+ }
1522
+ const assessments = Object.values(byEntry);
1523
+ const worst = worstHealth(assessments);
1524
+ return {
1525
+ thresholdLedgers,
1526
+ warnBelowLedgers: thresholds.warnBelowLedgers,
1527
+ ...worst === void 0 ? {} : { worst },
1528
+ sharedEntryCount: assessments.filter((a) => a.sharingStatus === "shared").length,
1529
+ undeterminedSharingCount: assessments.filter((a) => a.sharingStatus === "undetermined").length,
1530
+ byEntry
1531
+ };
1532
+ }
1533
+ function formatHuman(result, now, options = {}) {
1534
+ const color = options.color === true;
1535
+ const thresholdLedgers = options.thresholdLedgers ?? DEFAULT_THRESHOLD_LEDGERS;
1536
+ const lines = [];
1537
+ const entries = Object.entries(result.entries);
1538
+ const assessments = [];
1539
+ const caveats = coverageIssues(result);
1540
+ if (result.contracts.length > 0) {
1541
+ lines.push(
1542
+ `Scanned ${result.contracts.length} contract(s): ${result.contracts.map((c) => c.id).join(", ")}`
1543
+ );
1544
+ }
1545
+ if (result.coverage) {
1546
+ lines.push("Coverage: known keys only \u2014 contract storage has NOT been fully enumerated.");
1547
+ for (const [contract, count] of Object.entries(result.coverage.dataKeysSuppliedByContract)) {
1548
+ lines.push(` ${contract}: ${count} explicit data key(s)`);
1549
+ if (result.coverage.noDataKeysDeclaredByContract?.[contract] === true) {
1550
+ lines.push(" No additional data keys declared by caller; not independently verified.");
1551
+ }
1552
+ }
1553
+ for (const caveat of caveats) {
1554
+ if (caveat.kind !== "coverage-limited") continue;
1555
+ lines.push(` ${caveat.message}`);
1556
+ }
1557
+ lines.push("");
1558
+ } else {
1559
+ lines.push("Coverage: unspecified by producer; health assessment is incomplete.", "");
1560
+ }
1561
+ if (entries.length === 0 && result.issues.length === 0) {
1562
+ return [...lines, "No ledger entries found."].join("\n");
1563
+ }
1564
+ for (const [key, entry] of entries) {
1565
+ const live = isLive(entry.ttl);
1566
+ const assessment = assessEntryWithThresholds(entry, tiers(thresholdLedgers));
1567
+ assessments.push(assessment);
1568
+ const shortKey = `${key.slice(0, 10)}\u2026`;
1569
+ lines.push(
1570
+ `${paint(assessment.health, LABEL[assessment.health], color)} ${entry.kind} ${shortKey}`
1571
+ );
1572
+ lines.push(` contracts: ${entry.contracts.join(", ")}`);
1573
+ if (assessment.sharingStatus === "shared") {
1574
+ const others = assessment.observedContractCount - 1;
1575
+ lines.push(
1576
+ ` \u26A0 shared: this ${entry.kind} entry is shared with ${others} other contract${others === 1 ? "" : "s"} \u2014 they fail together`
1577
+ );
1578
+ } else if (assessment.sharingStatus === "undetermined") {
1579
+ lines.push(
1580
+ " \u26A0 sharing: code entries are shared by every contract built from the same Wasm."
1581
+ );
1582
+ lines.push(
1583
+ ` This scan saw ${assessment.observedContractCount}. Whether others depend on this entry cannot be`
1584
+ );
1585
+ lines.push(" determined from a single-contract scan \u2014 pass them together.");
1586
+ }
1587
+ if (entry.ttl.status === "unavailable") {
1588
+ lines.push(" ttl: no TTL metadata returned; health is unknown");
1589
+ } else {
1590
+ const state = live ? "live" : `EXPIRED (${entry.endBehavior})`;
1591
+ lines.push(` remaining: ${formatCount(entry.ttl.remainingLedgers)} ledgers \u2014 ${state}`);
1592
+ lines.push(` ends at: ledger ${formatCount(entry.ttl.endsAtLedger)}`);
1593
+ const at = estimateEndsAt(entry.ttl, now);
1594
+ if (at) lines.push(` expires ~: ${at.toISOString()} (estimate \u2014 ledgers are the truth)`);
1595
+ }
1596
+ lines.push(` observed: ledger ${formatCount(entry.observedAtLedger)}`);
1597
+ lines.push(` health: ${LABEL[assessment.health]} \u2014 ${assessment.reason}`);
1598
+ lines.push("");
1599
+ }
1600
+ for (const issue of result.issues) {
1601
+ lines.push(`! ${issue.kind}: ${issue.message}`);
1602
+ lines.push(` contracts: ${issue.contracts.join(", ")}`);
1603
+ if (issue.kind === "entry-not-found") {
1604
+ lines.push(" This means one of two things, and a scan cannot distinguish them:");
1605
+ lines.push(" \xB7 the entry was ARCHIVED \u2014 restore it with RestoreFootprintOp, or");
1606
+ lines.push(" \xB7 it never existed \u2014 check the contract ID and that it is deployed here.");
1607
+ }
1608
+ lines.push("");
1609
+ }
1610
+ const worst = worstHealth(assessments);
1611
+ if (worst !== void 0) {
1612
+ const shared = assessments.filter((a) => a.sharingStatus === "shared").length;
1613
+ lines.push(
1614
+ `Worst entry health: ${paint(worst, LABEL[worst], color)} (warn below ${formatCount(tiers(thresholdLedgers).warnBelowLedgers)} \xB7 act below ${formatCount(thresholdLedgers)} ledgers)` + (shared > 0 ? ` \xB7 ${shared} shared entr${shared === 1 ? "y" : "ies"}` : "")
1615
+ );
1616
+ }
1617
+ if (result.issues.length > 0) {
1618
+ lines.push(`Scan is PARTIAL \u2014 ${result.issues.length} issue(s). Absence is not health.`);
1619
+ }
1620
+ return lines.join("\n").trimEnd();
1621
+ }
1622
+ function scanIsDegraded(result) {
1623
+ return Object.keys(result.entries).length === 0 || result.issues.some(
1624
+ (i) => i.kind === "entry-not-found" || i.kind === "unsupported-executable"
1625
+ ) || Object.values(result.entries).some((e) => e.ttl.status === "unavailable");
1626
+ }
1627
+ function scopeIsUndeclared(result) {
1628
+ if (!result.coverage || result.contracts.length === 0) return true;
1629
+ return result.contracts.some((c) => {
1630
+ const count = result.coverage?.dataKeysSuppliedByContract[c.id];
1631
+ const empty = result.coverage?.noDataKeysDeclaredByContract?.[c.id] === true;
1632
+ if (count === void 0 || !Number.isInteger(count) || count < 0) return true;
1633
+ return count === 0 ? !empty : empty;
1634
+ });
1635
+ }
1636
+ function exitCodeFor(result, thresholdLedgers, options = {}) {
1637
+ if (result.issues.some((i) => i.kind === "rpc-error" || i.kind === "invalid-response"))
1638
+ return EXIT_ERROR;
1639
+ if (scanIsDegraded(result)) return EXIT_INCOMPLETE;
1640
+ if (options.requireDeclaredScope === true && scopeIsUndeclared(result)) return EXIT_INCOMPLETE;
1641
+ for (const entry of Object.values(result.entries)) {
1642
+ if (entry.ttl.status === "unavailable") continue;
1643
+ if (needsAction(entry.ttl.remainingLedgers, thresholdLedgers)) return EXIT_BELOW_THRESHOLD;
1644
+ }
1645
+ return EXIT_OK;
1646
+ }
1647
+
1648
+ // src/command.ts
1649
+ var DEFAULT_EXTEND_LEDGERS = 518400;
1650
+ var USAGE = "usage: evergreen scan <contract-id> [<contract-id> ...] [--keys-file <path> | --no-data-keys] [--require-declared-scope] [--threshold N] [--json] [--cost [--ledgers N]] [--optimize]";
1651
+ var HELP = `${USAGE}
1652
+
1653
+ Reads instance/Wasm and supplied persistent/temporary keys on Stellar Testnet.
1654
+ Keys file: { "dataKeys": ["base64 XDR LedgerKey", ...] }
1655
+
1656
+ PASS SEVERAL CONTRACTS TOGETHER to see real shared-code blast radius. Contracts
1657
+ built from the same Wasm share ONE ContractCode ledger entry, and a scan of one
1658
+ contract cannot tell whether others depend on it \u2014 the chain does not index
1659
+ reverse dependencies from a single query, so that entry reports "sharing
1660
+ undetermined". Naming them together resolves it:
1661
+
1662
+ evergreen scan <A> code entry: 1 consumer, sharing UNDETERMINED
1663
+ evergreen scan <A> <B> <C> code entry: 3 consumers, SHARED, they fail together
1664
+
1665
+ Every scan prints which contracts it actually scanned, so a mistyped or dropped
1666
+ argument is visible rather than inferred. --keys-file takes exactly one contract,
1667
+ because data keys belong to a specific contract and the file does not say which.
1668
+
1669
+ Exit: 0 everything scanned is healthy; 1 observed low TTL; 2 error;
1670
+ 3 the scan came back incomplete (entry missing, TTL unavailable,
1671
+ executable not followable, or nothing observed).
1672
+ Precedence: 2 > 3 > 1 > 0. Exit status never authorizes a transaction.
1673
+
1674
+ Scanning reads the keys it is given; it cannot enumerate a contract's storage,
1675
+ so a clean exit means "everything I was asked to check is healthy" and never
1676
+ "this contract is fully healthy". Coverage is printed with every scan.
1677
+
1678
+ --threshold N act-now threshold in LEDGERS, default 17,280 (~1 day).
1679
+ What evergreen-check sets in CI: a repository that wants
1680
+ a week of warning fails its build at 120,960, not at ours.
1681
+ Both health tiers move with it \u2014 WARNING widens as the
1682
+ action threshold rises, so raising it never silently
1683
+ narrows the earlier warning. Exit 1 means an entry is at
1684
+ or below this value; the boundary is inclusive, because
1685
+ remaining exactly N is already the margin you set out to
1686
+ keep. Changes what is REPORTED and never what is written.
1687
+
1688
+ --no-data-keys assert this contract has no data keys beyond its instance.
1689
+ Only its author can know that; it is a caller declaration
1690
+ and is never independently verified.
1691
+ --require-declared-scope
1692
+ also exit 3 when scope was not declared. Intended for CI on
1693
+ a contract you own; evergreen-check sets it by default.
1694
+ --json machine-readable output. The human view is a summary; JSON
1695
+ is the complete record, including every issue.
1696
+ --optimize append conditional storage advice with evidence and scope
1697
+ limits. Reads network minimum lifetimes; no payer needed.
1698
+ Add --cost for current rent quotes. No storage is changed.
1699
+ --cost [--ledgers N] estimate what extending every entry by N more ledgers
1700
+ would cost, priced by simulating against the network.
1701
+ Default N is 518,400 (~30 days). Nothing is submitted.
1702
+
1703
+ "--ledgers N" means "give me N MORE ledgers". The protocol
1704
+ wants an absolute target, so the CLI computes it for you
1705
+ and caps it at max_entry_ttl - 1, saying so when it does.
1706
+ Costs are estimates: rent pricing varies with network
1707
+ state and has differed ~18% between days.
1708
+
1709
+ Health states, printed per entry and as a worst-of summary:
1710
+ HEALTHY above threshold.
1711
+ WARNING low, recoverable, and affects only this contract.
1712
+ CRITICAL expired, OR temporary (deleted at expiry, unrecoverable), OR low and
1713
+ SHARED \u2014 a code entry shared by N contracts at 3 days is N contracts
1714
+ at 3 days, not one.
1715
+ UNKNOWN TTL could not be read. Not healthy; unread.
1716
+
1717
+ Colour is added only for an interactive terminal and honours NO_COLOR. The state
1718
+ word always prints, so piped output and screenshots lose nothing.`;
1719
+ async function runCli(args, dependencies) {
1720
+ if (args[0] === "extend") {
1721
+ if (args[1] === "--help" && args.length === 2)
1722
+ return { stdout: EXTEND_HELP, stderr: "", exitCode: 0 };
1723
+ if (!dependencies.extend)
1724
+ return {
1725
+ stdout: "",
1726
+ stderr: "Extension dependencies are unavailable.",
1727
+ exitCode: EXIT_ERROR
1728
+ };
1729
+ return runExtendCli(args, dependencies.extend);
1730
+ }
1731
+ const fail = (message) => ({
1732
+ stdout: "",
1733
+ stderr: message,
1734
+ exitCode: EXIT_ERROR
1735
+ });
1736
+ if (args.length === 1 && args[0] === "--help" || args.length === 2 && args[0] === "scan" && args[1] === "--help") {
1737
+ return {
1738
+ stdout: HELP + "\n\nManual extension: evergreen extend --help",
1739
+ stderr: "",
1740
+ exitCode: 0
1741
+ };
1742
+ }
1743
+ if (args[0] !== "scan") return fail(USAGE);
1744
+ const contractIds = [];
1745
+ let argIndex = 1;
1746
+ for (; argIndex < args.length && !args[argIndex].startsWith("-"); argIndex++) {
1747
+ contractIds.push(args[argIndex]);
1748
+ }
1749
+ if (contractIds.length === 0) return fail(USAGE);
1750
+ for (const id of contractIds) {
1751
+ if (!isValidContractId(id)) {
1752
+ return fail(
1753
+ `Not a Stellar contract ID: ${id}
1754
+ Contract IDs start with C and are 56 characters (StrKey-encoded).
1755
+ Check for a truncated paste or an account address (G\u2026) used by mistake.`
1756
+ );
1757
+ }
1758
+ }
1759
+ const duplicate = contractIds.find((id, i) => contractIds.indexOf(id) !== i);
1760
+ if (duplicate !== void 0) return fail(`Repeated contract ID: ${duplicate}`);
1761
+ let asJson = false;
1762
+ let withCost = false;
1763
+ let withOptimize = false;
1764
+ let additionalLedgers = DEFAULT_EXTEND_LEDGERS;
1765
+ let noDataKeys = false;
1766
+ let requireDeclaredScope = false;
1767
+ let keysPath;
1768
+ let thresholdLedgers = DEFAULT_THRESHOLD_LEDGERS;
1769
+ for (let i = argIndex; i < args.length; i++) {
1770
+ if (args[i] === "--json" && !asJson) asJson = true;
1771
+ else if (args[i] === "--cost" && !withCost) withCost = true;
1772
+ else if (args[i] === "--optimize" && !withOptimize) withOptimize = true;
1773
+ else if (args[i] === "--ledgers") {
1774
+ const raw = args[++i];
1775
+ const parsed = Number(raw);
1776
+ if (!raw || !/^\d+$/.test(raw) || !Number.isInteger(parsed) || parsed <= 0) {
1777
+ return fail(`--ledgers needs a positive whole number of ledgers.
1778
+ ${USAGE}`);
1779
+ }
1780
+ additionalLedgers = parsed;
1781
+ } else if (args[i] === "--threshold") {
1782
+ const raw = args[++i];
1783
+ const parsed = Number(raw);
1784
+ if (!raw || !/^\d+$/.test(raw) || !Number.isInteger(parsed) || parsed <= 0) {
1785
+ return fail(`--threshold needs a positive whole number of ledgers.
1786
+ ${USAGE}`);
1787
+ }
1788
+ thresholdLedgers = parsed;
1789
+ } else if (args[i] === "--no-data-keys" && !noDataKeys) noDataKeys = true;
1790
+ else if (args[i] === "--require-declared-scope" && !requireDeclaredScope)
1791
+ requireDeclaredScope = true;
1792
+ else if (args[i] === "--keys-file" && keysPath === void 0) {
1793
+ keysPath = args[++i];
1794
+ if (!keysPath || keysPath.startsWith("-")) return fail(`--keys-file needs a path.
1795
+ ${USAGE}`);
1796
+ } else return fail(`Unknown or repeated argument.
1797
+ ${USAGE}`);
1798
+ }
1799
+ if (noDataKeys && keysPath !== void 0)
1800
+ return fail("--keys-file and --no-data-keys are mutually exclusive.");
1801
+ if (keysPath !== void 0 && contractIds.length > 1)
1802
+ return fail(
1803
+ "--keys-file applies to exactly one contract.\n Data keys are owned by a specific contract and the file does not say which,\n so spreading one list across several would misattribute entries.\n Scan them together without --keys-file to see shared-code blast radius,\n or scan one at a time when you need explicit data keys."
1804
+ );
1805
+ let dataKeys = [];
1806
+ if (keysPath !== void 0) {
1807
+ try {
1808
+ const parsed = JSON.parse(await dependencies.readKeysFile(keysPath));
1809
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !("dataKeys" in parsed) || !Array.isArray(parsed.dataKeys) || !parsed.dataKeys.every((key) => typeof key === "string") || Object.keys(parsed).some((key) => key !== "dataKeys")) {
1810
+ return fail("Keys file must be a JSON object containing only a dataKeys array of strings.");
1811
+ }
1812
+ dataKeys = parsed.dataKeys;
1813
+ } catch {
1814
+ return fail("Could not read keys file as JSON. Check the file path and JSON syntax.");
1815
+ }
1816
+ }
1817
+ let reader;
1818
+ try {
1819
+ reader = await dependencies.connect();
1820
+ } catch (error) {
1821
+ if (error instanceof NotTestnetError) {
1822
+ return fail(
1823
+ `${error.message}
1824
+ Evergreen only runs against Stellar Testnet. Point SOROBAN_RPC_URL at a
1825
+ testnet endpoint \u2014 the default is https://soroban-testnet.stellar.org.`
1826
+ );
1827
+ }
1828
+ return fail(
1829
+ "Could not reach the Stellar RPC endpoint. Check SOROBAN_RPC_URL, the URL\nsyntax, and your network connection. Nothing was read and nothing was changed."
1830
+ );
1831
+ }
1832
+ const scanned = await scanContracts(
1833
+ reader,
1834
+ contractIds.map((id) => ({ contract: { id }, dataKeys, noDataKeys }))
1835
+ );
1836
+ const result = { ...scanned, issues: [...scanned.issues, ...coverageIssues(scanned)] };
1837
+ let settings;
1838
+ if (withOptimize) {
1839
+ try {
1840
+ settings = await dependencies.readStorageSettings?.();
1841
+ } catch {
1842
+ }
1843
+ }
1844
+ const advice = (priced) => withOptimize ? analyzeStorage(result, {
1845
+ ...settings === void 0 ? {} : { settings },
1846
+ ...priced === void 0 ? {} : {
1847
+ quote: {
1848
+ rentByEntry: priced.rentByEntry,
1849
+ pricedAtLedger: priced.pricedAtLedger,
1850
+ additionalLedgers: priced.additionalLedgers
1851
+ }
1852
+ }
1853
+ }) : void 0;
1854
+ let cost;
1855
+ if (withCost) {
1856
+ if (dependencies.priceExtend === void 0 && !withOptimize) {
1857
+ return fail("--cost is unavailable: no pricing backend was configured.");
1858
+ }
1859
+ try {
1860
+ if (dependencies.priceExtend === void 0) throw new Error("No pricing backend");
1861
+ cost = await dependencies.priceExtend({ scan: result, additionalLedgers });
1862
+ } catch {
1863
+ const optimization2 = advice();
1864
+ return {
1865
+ stdout: asJson ? JSON.stringify(
1866
+ {
1867
+ ...result,
1868
+ health: healthReport(result, thresholdLedgers),
1869
+ ...optimization2 === void 0 ? {} : { optimization: optimization2 }
1870
+ },
1871
+ null,
1872
+ 2
1873
+ ) : `${formatHuman(result, dependencies.now(), {
1874
+ color: dependencies.color === true,
1875
+ thresholdLedgers
1876
+ })}
1877
+
1878
+ ! Could not price an extend: the network declined to simulate it.
1879
+ The TTL results above are unaffected.${optimization2 === void 0 ? "" : `
1880
+
1881
+ ${formatStorageAdvice(optimization2).join("\n")}`}`,
1882
+ stderr: "",
1883
+ exitCode: exitCodeFor(result, thresholdLedgers, { requireDeclaredScope })
1884
+ };
1885
+ }
1886
+ }
1887
+ const optimization = advice(cost);
1888
+ return {
1889
+ stdout: asJson ? (
1890
+ // Additive envelope: every existing key of ScanResult is untouched, so
1891
+ // a consumer reading `entries` or `issues` is unaffected by `health`.
1892
+ JSON.stringify(
1893
+ {
1894
+ ...result,
1895
+ health: healthReport(result, thresholdLedgers),
1896
+ ...cost === void 0 ? {} : { cost },
1897
+ ...optimization === void 0 ? {} : { optimization }
1898
+ },
1899
+ null,
1900
+ 2
1901
+ )
1902
+ ) : formatHuman(result, dependencies.now(), {
1903
+ color: dependencies.color === true,
1904
+ thresholdLedgers
1905
+ }) + (cost === void 0 ? "" : `
1906
+
1907
+ ${formatCost(cost).join("\n")}`) + (optimization === void 0 ? "" : `
1908
+
1909
+ ${formatStorageAdvice(optimization).join("\n")}`),
1910
+ stderr: "",
1911
+ // Same constant the display grades against, so the printed health and the
1912
+ // exit code can never describe different thresholds.
1913
+ exitCode: exitCodeFor(result, thresholdLedgers, { requireDeclaredScope })
1914
+ };
1915
+ }
1916
+
1917
+ // src/bin.ts
1918
+ var DEFAULT_RPC = "https://soroban-testnet.stellar.org";
1919
+ var BASE_FEE_STROOPS = 100n;
1920
+ async function priceExtend(rpcUrl, sourceAccountId, args) {
1921
+ const server = new rpc5.Server(rpcUrl);
1922
+ const settings = await readStateArchivalSettings(server);
1923
+ const quoter = createSimulatingQuoter(server, {
1924
+ ...sourceAccountId === void 0 ? {} : { sourceAccountId },
1925
+ networkPassphrase: Networks4.TESTNET
1926
+ });
1927
+ let cappedEntryCount = 0;
1928
+ const targets = {};
1929
+ for (const [entryKey, entry] of Object.entries(args.scan.entries)) {
1930
+ if (entry.ttl.status !== "known") continue;
1931
+ const resolved = resolveExtendTarget({
1932
+ currentRemainingLedgers: entry.ttl.remainingLedgers,
1933
+ additionalLedgers: args.additionalLedgers,
1934
+ maxEntryTtl: settings.maxEntryTtl
1935
+ });
1936
+ if (resolved.wasCapped) cappedEntryCount += 1;
1937
+ targets[entryKey] = resolved.extendToLedgers;
1938
+ }
1939
+ const { estimate } = await estimateRent(args.scan, { extendToLedgers: targets }, quoter);
1940
+ const priced = Object.keys(estimate.estimatedRentStroopsByEntry);
1941
+ let resourceTotal = 0n;
1942
+ for (const entryKey of priced) {
1943
+ const [q] = await quoter.quoteDetailed({
1944
+ entryKeys: [entryKey],
1945
+ extendToLedgers: targets[entryKey]
1946
+ });
1947
+ resourceTotal += BigInt(q.minResourceFeeStroops);
1948
+ }
1949
+ const rent = BigInt(estimate.totalEstimatedRentStroops);
1950
+ const total = resourceTotal + BASE_FEE_STROOPS * BigInt(priced.length);
1951
+ return {
1952
+ totalStroops: total.toString(),
1953
+ rentStroops: rent.toString(),
1954
+ otherStroops: (total - rent).toString(),
1955
+ entryCount: priced.length,
1956
+ additionalLedgers: args.additionalLedgers,
1957
+ cappedEntryCount,
1958
+ maxEntryTtl: settings.maxEntryTtl,
1959
+ pricedAtLedger: settings.observedAtLedger,
1960
+ rentByEntry: estimate.estimatedRentStroopsByEntry
1961
+ };
1962
+ }
1963
+ async function main() {
1964
+ const rpcUrl = process.env.SOROBAN_RPC_URL ?? DEFAULT_RPC;
1965
+ const color = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
1966
+ const output = await runCli(process.argv.slice(2), {
1967
+ extend: {
1968
+ ...process.env.EVERGREEN_SOURCE_ACCOUNT ? { sourceAccount: process.env.EVERGREEN_SOURCE_ACCOUNT } : {},
1969
+ readKeysFile: (path) => readFile(path, "utf8"),
1970
+ preview: (text) => console.error(text),
1971
+ run: (request, preview) => runExtension(rpcUrl, request, preview)
1972
+ },
1973
+ connect: () => connectTestnet(rpcUrl),
1974
+ readStorageSettings: () => readStateArchivalSettings(new rpc5.Server(rpcUrl, { timeout: 1e4 })),
1975
+ readKeysFile: (path) => readFile(path, "utf8"),
1976
+ now: () => /* @__PURE__ */ new Date(),
1977
+ color,
1978
+ // Public key only; simulation never signs. Falls back to the well-known
1979
+ // testnet identity so `--cost` works without configuration.
1980
+ // No account is passed: simulation neither signs nor needs one to exist.
1981
+ // EVERGREEN_SOURCE_ACCOUNT stays available for anyone who wants a specific
1982
+ // identity in their own RPC logs.
1983
+ priceExtend: (args) => priceExtend(rpcUrl, process.env.EVERGREEN_SOURCE_ACCOUNT, args)
1984
+ });
1985
+ if (output.stdout) console.log(output.stdout);
1986
+ if (output.stderr) console.error(output.stderr);
1987
+ return output.exitCode;
1988
+ }
1989
+ main().then((code) => process.exit(code)).catch(() => {
1990
+ console.error("\n\u2716 Unexpected command failure.");
1991
+ process.exit(EXIT_ERROR);
1992
+ });
1993
+ async function runExtension(rpcUrl, request, preview) {
1994
+ const server = new rpc5.Server(rpcUrl, { timeout: 1e4 });
1995
+ if ((await server.getNetwork()).passphrase !== Networks4.TESTNET)
1996
+ throw new Error("RPC is not Testnet");
1997
+ const reader = createRpcReader(server);
1998
+ const scan = await scanContract(reader, { id: request.contractId }, request.dataKeys);
1999
+ const settings = await readStateArchivalSettings(server);
2000
+ const plan = planExtension(scan, {
2001
+ contractId: request.contractId,
2002
+ additionalLedgers: request.additionalLedgers,
2003
+ maxEntryTtl: settings.maxEntryTtl,
2004
+ dataKeys: request.dataKeys,
2005
+ includeCode: request.includeCode
2006
+ });
2007
+ const previews = [];
2008
+ const result = await executeExtensions(
2009
+ plan,
2010
+ {
2011
+ payer: request.sourceAccount,
2012
+ submit: request.submit,
2013
+ ...request.maxFeeStroops === void 0 ? {} : { maxFeeStroops: request.maxFeeStroops }
2014
+ },
2015
+ {
2016
+ prepare: (entry) => prepareExtension(server, entry, request.sourceAccount),
2017
+ signer: (prepared, remainingFeeStroops) => createEd25519Signer({
2018
+ payer: request.sourceAccount,
2019
+ sourceAccount: request.sourceAccount,
2020
+ entryKey: prepared.entry.entryKey,
2021
+ extendToLedgers: prepared.entry.extendToLedgers,
2022
+ expectedHash: prepared.transactionHash,
2023
+ maxFeeStroops: remainingFeeStroops,
2024
+ readSecret: () => {
2025
+ const secret = request.secretEnv ? process.env[request.secretEnv] : void 0;
2026
+ if (!secret) throw new Error("Signing key is unavailable");
2027
+ return secret;
2028
+ }
2029
+ }),
2030
+ submit: (prepared, signed) => submitExtension(server, prepared, signed),
2031
+ confirm: (hash) => confirmExtension(server, hash),
2032
+ readAfter: async (entryKey) => {
2033
+ const after = await scanContract(reader, { id: request.contractId }, request.dataKeys);
2034
+ const entry = after.entries[entryKey];
2035
+ if (!entry || entry.ttl.status !== "known" || after.issues.some((i) => i.entryKey === entryKey))
2036
+ throw new Error("Post-read incomplete");
2037
+ return { observedAtLedger: entry.observedAtLedger, endsAtLedger: entry.ttl.endsAtLedger };
2038
+ },
2039
+ preview: async (prepared) => {
2040
+ previews.push(prepared);
2041
+ preview(
2042
+ `Mode: ${request.submit ? "live (explicit submit)" : "dry-run"}; requested increment: ${request.additionalLedgers}; aggregate budget: ${request.maxFeeStroops ?? "not supplied (simulation only)"}
2043
+ ${extensionPreview(prepared)}`
2044
+ );
2045
+ },
2046
+ now: () => /* @__PURE__ */ new Date()
2047
+ }
2048
+ );
2049
+ return { plan, result, previews };
2050
+ }
2051
+ //# sourceMappingURL=evergreen.mjs.map