@lumenwipe/sdk 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -60,6 +60,12 @@ var LumenWipeTimeoutError = class extends Error {
60
60
  this.name = "LumenWipeTimeoutError";
61
61
  }
62
62
  };
63
+ var LumenWipeInvalidNetworkError = class extends Error {
64
+ constructor() {
65
+ super('LumenWipe network must be "mainnet" or "testnet"');
66
+ this.name = "LumenWipeInvalidNetworkError";
67
+ }
68
+ };
63
69
  var LumenWipeAbortError = class extends Error {
64
70
  constructor(reason) {
65
71
  super("LumenWipe API request was aborted");
@@ -69,7 +75,7 @@ var LumenWipeAbortError = class extends Error {
69
75
  };
70
76
 
71
77
  // src/version.ts
72
- var SDK_VERSION = "0.8.0";
78
+ var SDK_VERSION = "0.9.0";
73
79
 
74
80
  // src/http.ts
75
81
  var MAX_DELAY_MS = 3e4;
@@ -84,11 +90,11 @@ var HttpTransport = class {
84
90
  this.retry = retry;
85
91
  }
86
92
  async request(method, path, body, opts) {
87
- var _a, _b;
93
+ var _a, _b, _c;
88
94
  const attempts = opts.retry === "never" ? 1 : Math.max(1, Math.floor((_b = (_a = this.retry) == null ? void 0 : _a.attempts) != null ? _b : 1));
89
95
  for (let attempt = 1; ; attempt++) {
90
96
  try {
91
- return await this.once(method, path, body, opts.signal);
97
+ return await this.once(method, path, body, (_c = opts.bearer) != null ? _c : this.apiKey, opts.signal);
92
98
  } catch (e) {
93
99
  if (attempt >= attempts || !this.shouldRetry(opts.retry, e)) throw e;
94
100
  await this.sleep(this.delayMs(e, attempt), opts.signal);
@@ -124,12 +130,11 @@ var HttpTransport = class {
124
130
  signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
125
131
  });
126
132
  }
127
- async once(method, path, body, callerSignal) {
133
+ async once(method, path, body, bearer, callerSignal) {
128
134
  if (callerSignal == null ? void 0 : callerSignal.aborted) throw new LumenWipeAbortError(callerSignal.reason);
129
- const headers = {
130
- Authorization: `Bearer ${this.apiKey}`,
131
- "X-LumenWipe-SDK": `sdk/${SDK_VERSION}`
132
- };
135
+ const headers = {};
136
+ if (bearer !== void 0) headers.Authorization = `Bearer ${bearer}`;
137
+ headers["X-LumenWipe-SDK"] = `sdk/${SDK_VERSION}`;
133
138
  if (body !== void 0) headers["Content-Type"] = "application/json";
134
139
  const controller = new AbortController();
135
140
  let timedOut = false;
@@ -173,7 +178,16 @@ var HttpTransport = class {
173
178
  }
174
179
  };
175
180
 
181
+ // src/url.ts
182
+ var SLASH = 47;
183
+ function withoutTrailingSlashes(url) {
184
+ let end = url.length;
185
+ while (end > 0 && url.charCodeAt(end - 1) === SLASH) end--;
186
+ return url.slice(0, end);
187
+ }
188
+
176
189
  // src/client.ts
190
+ var NETWORKS = ["mainnet", "testnet"];
177
191
  function isPlaintextRemote(baseUrl) {
178
192
  try {
179
193
  const url = new URL(baseUrl);
@@ -193,7 +207,7 @@ var LumenWipeClient = class {
193
207
  throw new Error("No fetch implementation available; pass one via options.fetch.");
194
208
  }
195
209
  this.http = new HttpTransport(
196
- options.baseUrl.replace(/\/+$/, ""),
210
+ withoutTrailingSlashes(options.baseUrl),
197
211
  options.apiKey,
198
212
  (_b = options.timeout) != null ? _b : 3e4,
199
213
  resolved,
@@ -208,44 +222,78 @@ var LumenWipeClient = class {
208
222
  }
209
223
  }
210
224
  resolveNetwork(network) {
211
- if (network) return network;
212
- if (this.defaultNetwork) return this.defaultNetwork;
213
- if (!this.warnedDefaultNetwork) {
214
- this.warnedDefaultNetwork = true;
215
- this.log(
216
- 'LumenWipe: no network given, defaulting to "testnet". Pass `network` to the client or the call to silence this.'
217
- );
225
+ const resolved = network != null ? network : this.defaultNetwork;
226
+ if (resolved === void 0) {
227
+ if (!this.warnedDefaultNetwork) {
228
+ this.warnedDefaultNetwork = true;
229
+ this.log(
230
+ 'LumenWipe: no network given, defaulting to "testnet". Pass `network` to the client or the call to silence this.'
231
+ );
232
+ }
233
+ return "testnet";
218
234
  }
219
- return "testnet";
235
+ if (!NETWORKS.includes(resolved)) throw new LumenWipeInvalidNetworkError();
236
+ return resolved;
237
+ }
238
+ /** The `/v1/{network}` path for `rest`. The network is checked before any request is built. */
239
+ v1(network, rest) {
240
+ return `/v1/${encodeURIComponent(this.resolveNetwork(network))}${rest}`;
220
241
  }
221
242
  health(options) {
222
243
  return this.http.request("GET", "/health", void 0, __spreadProps(__spreadValues({}, options), {
223
244
  retry: "transient"
224
245
  }));
225
246
  }
226
- getAccount(address, network, options) {
247
+ /** Whether Stellar RPC is reachable on every network, and the days left on the exchange registry.
248
+ * A degraded service answers 503, thrown as a `LumenWipeApiError` whose `body` is a
249
+ * `HealthDeepResponse` with `status: "error"`. Never retried: the 503 is the answer. */
250
+ healthDeep(options) {
251
+ return this.http.request("GET", "/health/deep", void 0, __spreadProps(__spreadValues({}, options), {
252
+ retry: "never"
253
+ }));
254
+ }
255
+ /** The exchange registry the API closes against. Do not rely on it past `validUntil`. */
256
+ getExchangeRegistry(options) {
257
+ return this.http.request(
258
+ "GET",
259
+ "/v1/config/exchange-registry",
260
+ void 0,
261
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
262
+ );
263
+ }
264
+ async getAccount(address, network, options) {
227
265
  return this.http.request(
228
266
  "GET",
229
- `/${this.resolveNetwork(network)}/account/${encodeURIComponent(address)}`,
267
+ this.v1(network, `/account/${encodeURIComponent(address)}`),
230
268
  void 0,
231
269
  __spreadProps(__spreadValues({}, options), { retry: "transient" })
232
270
  );
233
271
  }
234
- getPaths(params, network, options) {
272
+ async getPaths(params, network, options) {
235
273
  const query = new URLSearchParams({ fromAsset: params.fromAsset, amount: params.amount });
236
274
  return this.http.request(
237
275
  "GET",
238
- `/${this.resolveNetwork(network)}/paths?${query.toString()}`,
276
+ this.v1(network, `/paths?${query.toString()}`),
239
277
  void 0,
240
278
  __spreadProps(__spreadValues({}, options), { retry: "transient" })
241
279
  );
242
280
  }
243
- closePlan(body, network, options) {
281
+ async closePlan(body, network, options) {
282
+ return this.http.request("POST", this.v1(network, "/close/plan"), body, __spreadProps(__spreadValues({}, options), {
283
+ retry: "throttle"
284
+ }));
285
+ }
286
+ /** Plans up to 20 accounts into one shared destination in one call. Read-only: no decisions are
287
+ * applied, so plan each account with `closePlan` before closing it. An address that cannot be
288
+ * read or planned comes back as a `blocked` plan, never as a missing entry. */
289
+ async batchPlan(body, network, options) {
244
290
  return this.http.request(
245
291
  "POST",
246
- `/v1/${this.resolveNetwork(network)}/close/plan`,
292
+ this.v1(network, "/close/batch-plan"),
247
293
  body,
248
- __spreadProps(__spreadValues({}, options), { retry: "throttle" })
294
+ __spreadProps(__spreadValues({}, options), {
295
+ retry: "throttle"
296
+ })
249
297
  );
250
298
  }
251
299
  /**
@@ -255,34 +303,68 @@ var LumenWipeClient = class {
255
303
  * submit the returned transactions in `order`, wait for confirmation, then call this
256
304
  * again until `requiresAnotherCall` is false.
257
305
  */
258
- closeTransactions(body, network, options) {
306
+ async closeTransactions(body, network, options) {
259
307
  return this.http.request(
260
308
  "POST",
261
- `/v1/${this.resolveNetwork(network)}/close/transactions`,
309
+ this.v1(network, "/close/transactions"),
262
310
  body,
263
311
  __spreadProps(__spreadValues({}, options), { retry: "throttle" })
264
312
  );
265
313
  }
266
- submit(signedXdr, network, options) {
314
+ async submit(signedXdr, network, options) {
267
315
  return this.http.request(
268
316
  "POST",
269
- `/v1/${this.resolveNetwork(network)}/submit`,
317
+ this.v1(network, "/submit"),
270
318
  { signedXdr },
271
319
  __spreadProps(__spreadValues({}, options), { retry: "never" })
272
320
  );
273
321
  }
274
- mediatorCheck(address, network, options) {
322
+ /** Looks a transaction up by hash and sends nothing. `not_found` is not "did not land": the
323
+ * transaction may be queued, or older than the window the response names. */
324
+ async getTransaction(hash, network, options) {
325
+ return this.http.request(
326
+ "GET",
327
+ this.v1(network, `/transactions/${encodeURIComponent(hash)}`),
328
+ void 0,
329
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
330
+ );
331
+ }
332
+ /** A close's phase and remaining work, from live state. Informational: a close is finished only
333
+ * when the hash of the merge you signed is confirmed by `getTransaction`. */
334
+ async closeStatus(source, network, options) {
335
+ const query = new URLSearchParams({ source });
275
336
  return this.http.request(
276
337
  "GET",
277
- `/${this.resolveNetwork(network)}/mediator/check/${encodeURIComponent(address)}`,
338
+ this.v1(network, `/close/status?${query.toString()}`),
278
339
  void 0,
279
340
  __spreadProps(__spreadValues({}, options), { retry: "transient" })
280
341
  );
281
342
  }
282
- mediatorSign(transaction, network, options) {
343
+ /** The XLM confirmed close transactions delivered to `destination`, read from each one's ledger
344
+ * result: a merge into it, or the relay account's forward of a merged balance. A hash that
345
+ * cannot be read as delivered is an `unavailable` entry, not an error, and `totalStroops` is
346
+ * set only when every entry is `delivered`. Pass 1 to 20 distinct hashes. */
347
+ async closeDelivered(destination, hashes, network, options) {
348
+ const query = new URLSearchParams({ destination, hashes: hashes.join(",") });
349
+ return this.http.request(
350
+ "GET",
351
+ this.v1(network, `/close/delivered?${query.toString()}`),
352
+ void 0,
353
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
354
+ );
355
+ }
356
+ async mediatorCheck(address, network, options) {
357
+ return this.http.request(
358
+ "GET",
359
+ this.v1(network, `/mediator/check/${encodeURIComponent(address)}`),
360
+ void 0,
361
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
362
+ );
363
+ }
364
+ async mediatorSign(transaction, network, options) {
283
365
  return this.http.request(
284
366
  "POST",
285
- `/${this.resolveNetwork(network)}/mediator/sign`,
367
+ this.v1(network, "/mediator/sign"),
286
368
  {
287
369
  transaction
288
370
  },
@@ -292,10 +374,10 @@ var LumenWipeClient = class {
292
374
  /** Wraps a wind-down transaction (its own fee already zero) in a signed CAP-15 fee-bump
293
375
  * envelope for an account that cannot pay its own fee (architecture.md §8.1). The caller
294
376
  * submits the returned XDR through `submit()`, exactly like any other close transaction. */
295
- feeBumpSponsor(transaction, network, options) {
377
+ async feeBumpSponsor(transaction, network, options) {
296
378
  return this.http.request(
297
379
  "POST",
298
- `/${this.resolveNetwork(network)}/fee-bump/sponsor`,
380
+ this.v1(network, "/fee-bump/sponsor"),
299
381
  {
300
382
  transaction
301
383
  },
@@ -304,24 +386,125 @@ var LumenWipeClient = class {
304
386
  }
305
387
  /** Every live SEP-41 allowance the account has granted (architecture.md §12). Independent of
306
388
  * closing an account - a standalone security utility. */
307
- getAllowances(address, network, options) {
389
+ async getAllowances(address, network, options) {
308
390
  return this.http.request(
309
391
  "GET",
310
- `/${this.resolveNetwork(network)}/allowances/${encodeURIComponent(address)}`,
392
+ this.v1(network, `/allowances/${encodeURIComponent(address)}`),
311
393
  void 0,
312
394
  __spreadProps(__spreadValues({}, options), { retry: "transient" })
313
395
  );
314
396
  }
315
397
  /** Builds the unsigned `approve(owner, spender, 0, 0)` transaction that revokes one allowance.
316
398
  * The caller signs and submits it through `submit()`, like any other transaction here. */
317
- revokeAllowance(params, network, options) {
399
+ async revokeAllowance(params, network, options) {
318
400
  return this.http.request(
319
401
  "POST",
320
- `/${this.resolveNetwork(network)}/allowances/revoke`,
402
+ this.v1(network, "/allowances/revoke"),
321
403
  params,
322
404
  __spreadProps(__spreadValues({}, options), { retry: "throttle" })
323
405
  );
324
406
  }
407
+ /** Accounts closed and XLM recovered through LumenWipe on the network. */
408
+ async getStats(network, options) {
409
+ return this.http.request("GET", this.v1(network, "/stats"), void 0, __spreadProps(__spreadValues({}, options), {
410
+ retry: "transient"
411
+ }));
412
+ }
413
+ /** The totals, the most recent closes and the daily activity on the network. */
414
+ async getStatsFeed(network, options) {
415
+ return this.http.request("GET", this.v1(network, "/stats/feed"), void 0, __spreadProps(__spreadValues({}, options), {
416
+ retry: "transient"
417
+ }));
418
+ }
419
+ /** Counts a confirmed close that was submitted outside this API. The API re-reads the
420
+ * transaction and counts it only if it built it, it confirmed and it merged an account;
421
+ * `counted` is false for a hash already counted. Closes sent through `submit()` are counted
422
+ * without this. */
423
+ async recordMerge(txHash, network, options) {
424
+ return this.http.request(
425
+ "POST",
426
+ this.v1(network, "/stats/merges"),
427
+ { txHash },
428
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
429
+ );
430
+ }
431
+ };
432
+
433
+ // src/integrator-client.ts
434
+ var LumenWipeIntegratorClient = class {
435
+ constructor(options) {
436
+ var _a, _b, _c;
437
+ const resolved = (_a = options.fetch) != null ? _a : globalThis.fetch;
438
+ if (!resolved) {
439
+ throw new Error("No fetch implementation available; pass one via options.fetch.");
440
+ }
441
+ this.http = new HttpTransport(
442
+ withoutTrailingSlashes(options.baseUrl),
443
+ void 0,
444
+ (_b = options.timeout) != null ? _b : 3e4,
445
+ resolved,
446
+ options.retry
447
+ );
448
+ if (isPlaintextRemote(options.baseUrl)) {
449
+ ((_c = options.logger) != null ? _c : ((message) => console.warn(message)))(
450
+ "LumenWipe: baseUrl uses http:// for a non-local host. Session tokens and new keys travel in clear text; use https://."
451
+ );
452
+ }
453
+ }
454
+ /** A SEP-53 message for `address` (G...) to sign, valid until `expiresAt`. */
455
+ challenge(address, options) {
456
+ return this.http.request(
457
+ "POST",
458
+ "/v1/integrator/auth/challenge",
459
+ { address },
460
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
461
+ );
462
+ }
463
+ /** Exchanges the signed challenge for a session token. */
464
+ session(request, options) {
465
+ return this.http.request(
466
+ "POST",
467
+ "/v1/integrator/auth/session",
468
+ { address: request.address, message: request.message, signature: request.signature },
469
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
470
+ );
471
+ }
472
+ /** The wallet's keys, each with its own request counts, and the wallet's total counts. */
473
+ listKeys(token, options) {
474
+ return this.http.request("GET", "/v1/integrator/keys", void 0, __spreadProps(__spreadValues({}, options), {
475
+ retry: "transient",
476
+ bearer: token
477
+ }));
478
+ }
479
+ /** Creates a key, optionally named and limited to some networks. The raw `key` is in this
480
+ * response only and can never be read again. */
481
+ createKey(token, request = {}, options) {
482
+ return this.http.request(
483
+ "POST",
484
+ "/v1/integrator/keys",
485
+ { label: request.label, networks: request.networks },
486
+ __spreadProps(__spreadValues({}, options), { retry: "never", bearer: token })
487
+ );
488
+ }
489
+ /** Revokes one of the wallet's keys by its `id`. */
490
+ revokeKey(token, id, options) {
491
+ return this.http.request(
492
+ "POST",
493
+ `/v1/integrator/keys/${encodeURIComponent(id)}/revoke`,
494
+ void 0,
495
+ __spreadProps(__spreadValues({}, options), { retry: "never", bearer: token })
496
+ );
497
+ }
498
+ /** Revokes a key and issues its replacement, keeping its label and networks. The new raw `key`
499
+ * is in this response only. */
500
+ rotateKey(token, id, options) {
501
+ return this.http.request(
502
+ "POST",
503
+ `/v1/integrator/keys/${encodeURIComponent(id)}/rotate`,
504
+ void 0,
505
+ __spreadProps(__spreadValues({}, options), { retry: "never", bearer: token })
506
+ );
507
+ }
325
508
  };
326
509
 
327
510
  // src/close-engine.ts
@@ -334,33 +517,200 @@ var InsufficientSignatureWeightError = class extends Error {
334
517
  this.name = "InsufficientSignatureWeightError";
335
518
  }
336
519
  };
520
+ var STATE_CHANGED_REFETCHES = 2;
521
+ var STATE_CHANGED_REFETCH_DELAY_MS = 3e3;
522
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
523
+ var CloseStateChangedError = class extends Error {
524
+ constructor(apiError) {
525
+ super(
526
+ "Your account changed while the close was being prepared, so nothing was submitted. Plan the close again and review it before continuing."
527
+ );
528
+ this.apiError = apiError;
529
+ this.name = "CloseStateChangedError";
530
+ }
531
+ };
532
+ function hasCode(error, code) {
533
+ return typeof error === "object" && error !== null && error.code === code;
534
+ }
535
+ var SUBMIT_RESEND_ATTEMPTS = 2;
536
+ var SUBMIT_RESEND_DELAY_MS = 5e3;
537
+ var SUBMISSION_GIVE_UP_MARGIN_SECONDS = 120;
538
+ var CloseOutcomeUnknownError = class extends Error {
539
+ constructor(submission) {
540
+ super(
541
+ "This transaction is not confirmed yet. Your account may already be closed, so check again before doing anything else."
542
+ );
543
+ this.submission = submission;
544
+ this.name = "CloseOutcomeUnknownError";
545
+ }
546
+ };
547
+ var CloseTransactionFailedError = class extends Error {
548
+ constructor(submission, message) {
549
+ super(message);
550
+ this.submission = submission;
551
+ this.name = "CloseTransactionFailedError";
552
+ }
553
+ };
554
+ function isSubmitRefused(error) {
555
+ const status = error == null ? void 0 : error.status;
556
+ return typeof status === "number" && status >= 400 && status < 500;
557
+ }
558
+ async function resolveSubmission(submission, lookup, now = Date.now) {
559
+ let found;
560
+ try {
561
+ found = await lookup(submission.hash);
562
+ } catch (e) {
563
+ return { outcome: "unknown" };
564
+ }
565
+ if (found.hash !== submission.hash) return { outcome: "unknown" };
566
+ if (found.status === "confirmed") return { outcome: "confirmed" };
567
+ if (found.status === "failed") return { outcome: "failed", message: found.message };
568
+ if (submission.validUntil === null) return { outcome: "unknown" };
569
+ const elapsed = Math.min(now() / 1e3, found.searched.latestLedgerCloseTime);
570
+ return elapsed > submission.validUntil + SUBMISSION_GIVE_UP_MARGIN_SECONDS ? { outcome: "expired" } : { outcome: "unknown" };
571
+ }
572
+ var submissionClosesAccount = (submission) => submission.covers.some((step) => step === "MERGE" || step === "CLOSE_ACCOUNT");
573
+ var ROUND_MARGIN = 5;
574
+ var HARD_MAX_ROUNDS = 200;
575
+ var CloseDidNotConvergeError = class extends Error {
576
+ constructor(rounds, bound, reason) {
577
+ super(
578
+ reason === "empty_batch" ? "The close stopped because the service reported more steps but had nothing to sign. Check your account, then try again - the close picks up where it stopped." : `The close needed more steps than expected (${rounds} of at most ${bound}). Check your account, then try again - the close picks up where it stopped.`
579
+ );
580
+ this.rounds = rounds;
581
+ this.bound = bound;
582
+ this.reason = reason;
583
+ this.name = "CloseDidNotConvergeError";
584
+ }
585
+ };
337
586
  async function runClose(deps, resume) {
338
- var _a, _b;
339
- const maxRounds = (_a = deps.maxRounds) != null ? _a : 25;
587
+ var _a, _b, _c, _d, _e, _f;
588
+ let bound = 0;
589
+ let planHash = resume ? void 0 : deps.planHash;
590
+ const run = __spreadProps(__spreadValues({}, deps), {
591
+ onConfirmed: (tx, hash) => {
592
+ var _a2;
593
+ planHash = void 0;
594
+ (_a2 = deps.onConfirmed) == null ? void 0 : _a2.call(deps, tx, hash);
595
+ }
596
+ });
597
+ if ((_a = deps.reconcile) == null ? void 0 : _a.unresolved) {
598
+ const settled = await settleLeftOver(deps.reconcile, deps.reconcile.unresolved);
599
+ if (settled === "closed") return { mergeConfirmed: true };
600
+ if (settled === "confirmed") planHash = void 0;
601
+ }
602
+ let firstRound = 0;
340
603
  if (resume) {
341
- await signOrThrow(deps, resume.tx, resume.xdr, resume.queue, resume.requiresAnotherCall);
342
- await processTxs(deps, resume.queue, resume.requiresAnotherCall);
343
- if (!resume.requiresAnotherCall) return;
604
+ const at = resumedPosition(resume);
605
+ firstRound = at.round + 1;
606
+ await signOrThrow(run, resume.tx, resume.xdr, resume.queue, at);
607
+ await processTxs(run, resume.queue, __spreadProps(__spreadValues({}, at), { txIndex: at.txIndex + 1 }));
608
+ if (!resume.requiresAnotherCall) return resultOf(run);
344
609
  }
345
- for (let round = 0; round < maxRounds; round++) {
346
- (_b = deps.onProgress) == null ? void 0 : _b.call(deps, "Preparing transactions\u2026");
347
- const batch = await deps.getTransactions();
610
+ for (let round = 0; round < ((_b = run.maxRounds) != null ? _b : Math.max(bound, 1)); round++) {
611
+ (_c = run.onProgress) == null ? void 0 : _c.call(run, "Preparing transactions\u2026");
612
+ report(run, () => ({ phase: "preparing", round: firstRound + round }));
613
+ const batch = await fetchRound(run, planHash).catch(async (error) => {
614
+ if (await mergeExplainsMissingAccount(run, error)) return null;
615
+ throw error;
616
+ });
617
+ if (!batch) return resultOf(run);
348
618
  const txs = [...batch.transactions].sort((a, b) => a.order - b.order);
349
- await processTxs(deps, txs, batch.remaining.requiresAnotherCall);
350
- if (!batch.remaining.requiresAnotherCall) return;
619
+ const { requiresAnotherCall, steps } = batch.remaining;
620
+ if (requiresAnotherCall && txs.length === 0) {
621
+ throw new CloseDidNotConvergeError(round + 1, (_d = run.maxRounds) != null ? _d : bound, "empty_batch");
622
+ }
623
+ await processTxs(run, txs, {
624
+ round: firstRound + round,
625
+ txIndex: 0,
626
+ txCount: txs.length,
627
+ moreRoundsExpected: requiresAnotherCall
628
+ });
629
+ if (!requiresAnotherCall) return resultOf(run);
630
+ bound = Math.max(bound, Math.min(HARD_MAX_ROUNDS, round + 1 + steps + ROUND_MARGIN));
351
631
  }
352
- throw new Error("The close did not converge after the maximum number of rounds.");
632
+ throw new CloseDidNotConvergeError((_e = run.maxRounds) != null ? _e : bound, (_f = run.maxRounds) != null ? _f : bound, "round_limit");
633
+ }
634
+ function resultOf(run) {
635
+ return { mergeConfirmed: run.confirmedMerge !== void 0 };
353
636
  }
354
- async function processTxs(deps, txs, requiresAnotherCall) {
637
+ async function settleLeftOver(reconcile, submission) {
638
+ var _a, _b;
639
+ const settled = await resolveSubmission(submission, reconcile.lookup, reconcile.now);
640
+ if (settled.outcome === "unknown") throw new CloseOutcomeUnknownError(submission);
641
+ await ((_a = reconcile.onSubmission) == null ? void 0 : _a.call(reconcile, null));
642
+ if (settled.outcome === "failed") {
643
+ throw new CloseTransactionFailedError(submission, settled.message);
644
+ }
645
+ if (settled.outcome === "expired") return "expired";
646
+ (_b = reconcile.onResolved) == null ? void 0 : _b.call(reconcile, submission);
647
+ return submissionClosesAccount(submission) ? "closed" : "confirmed";
648
+ }
649
+ async function mergeExplainsMissingAccount(run, error) {
650
+ if (!hasCode(error, "account_not_found") || !run.reconcile || !run.confirmedMerge) return false;
651
+ const settled = await resolveSubmission(
652
+ run.confirmedMerge,
653
+ run.reconcile.lookup,
654
+ run.reconcile.now
655
+ );
656
+ return settled.outcome === "confirmed";
657
+ }
658
+ async function fetchRound(deps, planHash) {
659
+ var _a;
660
+ for (let attempt = 0; ; attempt++) {
661
+ if (attempt > 0) await ((_a = deps.sleep) != null ? _a : defaultSleep)(STATE_CHANGED_REFETCH_DELAY_MS);
662
+ try {
663
+ return await deps.getTransactions(planHash === void 0 ? {} : { planHash });
664
+ } catch (error) {
665
+ if (!hasCode(error, "state_changed")) throw error;
666
+ if (planHash !== void 0 || attempt === STATE_CHANGED_REFETCHES) {
667
+ throw new CloseStateChangedError(error);
668
+ }
669
+ }
670
+ }
671
+ }
672
+ function resumedPosition(resume) {
673
+ var _a, _b, _c;
674
+ const txIndex = (_a = resume.txIndex) != null ? _a : 0;
675
+ return {
676
+ round: (_b = resume.round) != null ? _b : 0,
677
+ txIndex,
678
+ txCount: (_c = resume.txCount) != null ? _c : txIndex + 1 + resume.queue.length,
679
+ moreRoundsExpected: resume.requiresAnotherCall
680
+ };
681
+ }
682
+ function report(deps, build) {
683
+ if (!deps.onProgressEvent) return;
684
+ try {
685
+ const returned = deps.onProgressEvent(build());
686
+ if (returned instanceof Promise) returned.catch(() => {
687
+ });
688
+ } catch (e) {
689
+ }
690
+ }
691
+ function reportTx(deps, phase, at, covers) {
692
+ report(deps, () => ({
693
+ phase,
694
+ round: at.round,
695
+ txIndex: at.txIndex,
696
+ txCount: at.txCount,
697
+ covers: [...covers],
698
+ moreRoundsExpected: at.moreRoundsExpected
699
+ }));
700
+ }
701
+ async function processTxs(deps, txs, start) {
355
702
  for (let i = 0; i < txs.length; i++) {
356
703
  const tx = txs[i];
704
+ const at = __spreadProps(__spreadValues({}, start), { txIndex: start.txIndex + i });
705
+ reportTx(deps, "verifying", at, tx.covers);
357
706
  await deps.verify(tx);
358
- await signOrThrow(deps, tx, tx.xdr, txs.slice(i + 1), requiresAnotherCall);
707
+ await signOrThrow(deps, tx, tx.xdr, txs.slice(i + 1), at);
359
708
  }
360
709
  }
361
- async function signOrThrow(deps, tx, xdr, queue, requiresAnotherCall) {
362
- var _a;
710
+ async function signOrThrow(deps, tx, xdr, queue, at) {
711
+ var _a, _b, _c, _d, _e, _f;
363
712
  const required = deps.requiredWeight(tx);
713
+ reportTx(deps, "awaiting-signature", at, tx.covers);
364
714
  const { xdr: signedXdr, weight } = await deps.sign(tx, xdr);
365
715
  if (weight < required) {
366
716
  throw new InsufficientSignatureWeightError({
@@ -369,18 +719,81 @@ async function signOrThrow(deps, tx, xdr, queue, requiresAnotherCall) {
369
719
  requiredWeight: required,
370
720
  accumulatedWeight: weight,
371
721
  queue,
372
- requiresAnotherCall
722
+ requiresAnotherCall: at.moreRoundsExpected,
723
+ round: at.round,
724
+ txIndex: at.txIndex,
725
+ txCount: at.txCount
373
726
  });
374
727
  }
375
- const hash = await deps.submit(tx, signedXdr);
376
- (_a = deps.onConfirmed) == null ? void 0 : _a.call(deps, tx, hash);
728
+ reportTx(deps, "submitting", at, tx.covers);
729
+ const finalXdr = deps.prepare ? await deps.prepare(tx, signedXdr) : signedXdr;
730
+ if (!deps.reconcile) {
731
+ (_a = deps.onConfirmed) == null ? void 0 : _a.call(deps, tx, await deps.submit(tx, finalXdr));
732
+ reportTx(deps, "confirmed", at, tx.covers);
733
+ return;
734
+ }
735
+ const submission = openSubmission(deps.reconcile, tx, finalXdr);
736
+ await ((_c = (_b = deps.reconcile).onSubmission) == null ? void 0 : _c.call(_b, submission));
737
+ await submitUntilSettled(deps, deps.reconcile, tx, finalXdr, submission);
738
+ await ((_e = (_d = deps.reconcile).onSubmission) == null ? void 0 : _e.call(_d, null));
739
+ if (submissionClosesAccount(submission)) deps.confirmedMerge = submission;
740
+ (_f = deps.onConfirmed) == null ? void 0 : _f.call(deps, __spreadProps(__spreadValues({}, tx), { covers: submission.covers }), submission.hash);
741
+ reportTx(deps, "confirmed", at, submission.covers);
742
+ }
743
+ function openSubmission(reconcile, tx, finalXdr) {
744
+ const claimed = tx.covers.filter((s) => s !== "MERGE" && s !== "CLOSE_ACCOUNT");
745
+ return __spreadValues(__spreadValues({
746
+ hash: reconcile.hashOf(finalXdr),
747
+ covers: reconcile.closesAccount(finalXdr) ? [...claimed, "MERGE", "CLOSE_ACCOUNT"] : claimed,
748
+ validUntil: reconcile.validUntil(finalXdr)
749
+ }, tx.coversTargets && { coversTargets: tx.coversTargets }), tx.needsSponsoredFee && { needsSponsoredFee: true });
750
+ }
751
+ async function submitUntilSettled(deps, reconcile, tx, finalXdr, submission) {
752
+ var _a, _b, _c, _d;
753
+ for (let attempt = 0; attempt <= SUBMIT_RESEND_ATTEMPTS; attempt++) {
754
+ if (attempt > 0) {
755
+ (_a = deps.onProgress) == null ? void 0 : _a.call(deps, "Still waiting for the network to confirm\u2026");
756
+ await ((_b = deps.sleep) != null ? _b : defaultSleep)(SUBMIT_RESEND_DELAY_MS);
757
+ }
758
+ try {
759
+ if (await deps.submit(tx, finalXdr) === submission.hash) return;
760
+ } catch (error) {
761
+ if (!isSubmitRefused(error)) continue;
762
+ if (attempt === 0) {
763
+ await ((_c = reconcile.onSubmission) == null ? void 0 : _c.call(reconcile, null));
764
+ throw error;
765
+ }
766
+ break;
767
+ }
768
+ }
769
+ const settled = await resolveSubmission(submission, reconcile.lookup, reconcile.now);
770
+ if (settled.outcome === "confirmed") return;
771
+ if (settled.outcome === "failed") {
772
+ await ((_d = reconcile.onSubmission) == null ? void 0 : _d.call(reconcile, null));
773
+ throw new CloseTransactionFailedError(submission, settled.message);
774
+ }
775
+ throw new CloseOutcomeUnknownError(submission);
377
776
  }
378
777
  export {
778
+ CloseDidNotConvergeError,
779
+ CloseOutcomeUnknownError,
780
+ CloseStateChangedError,
781
+ CloseTransactionFailedError,
379
782
  InsufficientSignatureWeightError,
380
783
  LumenWipeAbortError,
381
784
  LumenWipeApiError,
382
785
  LumenWipeClient,
786
+ LumenWipeIntegratorClient,
787
+ LumenWipeInvalidNetworkError,
383
788
  LumenWipeTimeoutError,
789
+ STATE_CHANGED_REFETCHES,
790
+ STATE_CHANGED_REFETCH_DELAY_MS,
791
+ SUBMISSION_GIVE_UP_MARGIN_SECONDS,
792
+ SUBMIT_RESEND_ATTEMPTS,
793
+ SUBMIT_RESEND_DELAY_MS,
384
794
  isApiErrorCode,
385
- runClose
795
+ isSubmitRefused,
796
+ resolveSubmission,
797
+ runClose,
798
+ submissionClosesAccount
386
799
  };