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