@lumenwipe/sdk 0.2.0 → 0.5.1

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
@@ -1,12 +1,58 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+
1
21
  // src/errors.ts
22
+ function parseRetryAfterMs(value) {
23
+ if (!value) return void 0;
24
+ const trimmed = value.trim();
25
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
26
+ const at = Date.parse(trimmed);
27
+ return Number.isNaN(at) ? void 0 : Math.max(0, at - Date.now());
28
+ }
29
+ function readEnvelope(body) {
30
+ if (typeof body !== "object" || body === null || !("error" in body)) return void 0;
31
+ const error = body.error;
32
+ if (typeof error !== "object" || error === null) return void 0;
33
+ const { code, message, details } = error;
34
+ if (typeof code !== "string" || typeof message !== "string") return void 0;
35
+ return { code, message, details };
36
+ }
2
37
  var LumenWipeApiError = class extends Error {
3
- constructor(status, body) {
4
- super(`LumenWipe API error ${status}`);
38
+ constructor(status, body, headers) {
39
+ var _a, _b;
40
+ const envelope = readEnvelope(body);
41
+ super(
42
+ envelope ? `LumenWipe API error ${status} ${envelope.code}: ${envelope.message}` : `LumenWipe API error ${status}`
43
+ );
5
44
  this.status = status;
6
45
  this.body = body;
7
46
  this.name = "LumenWipeApiError";
47
+ this.code = (_a = envelope == null ? void 0 : envelope.code) != null ? _a : "upstream_error";
48
+ this.details = envelope == null ? void 0 : envelope.details;
49
+ this.retryAfterMs = parseRetryAfterMs(headers == null ? void 0 : headers.get("Retry-After"));
50
+ this.requestId = (_b = headers == null ? void 0 : headers.get("X-Request-Id")) != null ? _b : void 0;
8
51
  }
9
52
  };
53
+ function isApiErrorCode(error, code) {
54
+ return error instanceof LumenWipeApiError && error.code === code;
55
+ }
10
56
  var LumenWipeTimeoutError = class extends Error {
11
57
  constructor(timeoutMs) {
12
58
  super(`LumenWipe API request timed out after ${timeoutMs}ms`);
@@ -14,21 +60,90 @@ var LumenWipeTimeoutError = class extends Error {
14
60
  this.name = "LumenWipeTimeoutError";
15
61
  }
16
62
  };
63
+ var LumenWipeAbortError = class extends Error {
64
+ constructor(reason) {
65
+ super("LumenWipe API request was aborted");
66
+ this.reason = reason;
67
+ this.name = "LumenWipeAbortError";
68
+ }
69
+ };
70
+
71
+ // src/version.ts
72
+ var SDK_VERSION = "0.5.1";
17
73
 
18
74
  // src/http.ts
75
+ var MAX_DELAY_MS = 3e4;
76
+ var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
77
+ var THROTTLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
19
78
  var HttpTransport = class {
20
- constructor(baseUrl, apiKey, timeout, doFetch) {
79
+ constructor(baseUrl, apiKey, timeout, doFetch, retry) {
21
80
  this.baseUrl = baseUrl;
22
81
  this.apiKey = apiKey;
23
82
  this.timeout = timeout;
24
83
  this.doFetch = doFetch;
84
+ this.retry = retry;
25
85
  }
26
- async request(method, path, body) {
27
- const headers = { Authorization: `Bearer ${this.apiKey}` };
86
+ async request(method, path, body, opts) {
87
+ var _a, _b;
88
+ const attempts = opts.retry === "never" ? 1 : Math.max(1, Math.floor((_b = (_a = this.retry) == null ? void 0 : _a.attempts) != null ? _b : 1));
89
+ for (let attempt = 1; ; attempt++) {
90
+ try {
91
+ return await this.once(method, path, body, opts.signal);
92
+ } catch (e) {
93
+ if (attempt >= attempts || !this.shouldRetry(opts.retry, e)) throw e;
94
+ await this.sleep(this.delayMs(e, attempt), opts.signal);
95
+ }
96
+ }
97
+ }
98
+ shouldRetry(policy, e) {
99
+ if (e instanceof LumenWipeApiError) {
100
+ const statuses = policy === "transient" ? TRANSIENT_STATUSES : THROTTLE_STATUSES;
101
+ return statuses.has(e.status);
102
+ }
103
+ return policy === "transient" && !(e instanceof LumenWipeTimeoutError) && !(e instanceof LumenWipeAbortError);
104
+ }
105
+ delayMs(e, attempt) {
106
+ var _a, _b;
107
+ if (e instanceof LumenWipeApiError && e.retryAfterMs !== void 0) {
108
+ return Math.min(e.retryAfterMs, MAX_DELAY_MS);
109
+ }
110
+ const base = (_b = (_a = this.retry) == null ? void 0 : _a.baseDelayMs) != null ? _b : 0;
111
+ return Math.min(base * 2 ** (attempt - 1) * (0.5 + Math.random() / 2), MAX_DELAY_MS);
112
+ }
113
+ sleep(ms, signal) {
114
+ return new Promise((resolve, reject) => {
115
+ if (signal == null ? void 0 : signal.aborted) return reject(new LumenWipeAbortError(signal.reason));
116
+ const onAbort = () => {
117
+ clearTimeout(timer);
118
+ reject(new LumenWipeAbortError(signal == null ? void 0 : signal.reason));
119
+ };
120
+ const timer = setTimeout(() => {
121
+ signal == null ? void 0 : signal.removeEventListener("abort", onAbort);
122
+ resolve();
123
+ }, ms);
124
+ signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
125
+ });
126
+ }
127
+ async once(method, path, body, callerSignal) {
128
+ 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
+ };
28
133
  if (body !== void 0) headers["Content-Type"] = "application/json";
29
134
  const controller = new AbortController();
135
+ let timedOut = false;
136
+ let callerAborted = false;
137
+ const onCallerAbort = () => {
138
+ callerAborted = true;
139
+ controller.abort();
140
+ };
141
+ callerSignal == null ? void 0 : callerSignal.addEventListener("abort", onCallerAbort, { once: true });
30
142
  const bounded = Number.isFinite(this.timeout) && this.timeout > 0;
31
- const timer = bounded ? setTimeout(() => controller.abort(), this.timeout) : void 0;
143
+ const timer = bounded ? setTimeout(() => {
144
+ timedOut = true;
145
+ controller.abort();
146
+ }, this.timeout) : void 0;
32
147
  try {
33
148
  const res = await this.doFetch(`${this.baseUrl}${path}`, {
34
149
  method,
@@ -45,20 +160,33 @@ var HttpTransport = class {
45
160
  parsed = text;
46
161
  }
47
162
  }
48
- if (!res.ok) throw new LumenWipeApiError(res.status, parsed);
163
+ if (!res.ok) throw new LumenWipeApiError(res.status, parsed, res.headers);
49
164
  return parsed;
50
165
  } catch (e) {
51
- if (controller.signal.aborted) throw new LumenWipeTimeoutError(this.timeout);
166
+ if (callerAborted) throw new LumenWipeAbortError(callerSignal == null ? void 0 : callerSignal.reason);
167
+ if (timedOut) throw new LumenWipeTimeoutError(this.timeout);
52
168
  throw e;
53
169
  } finally {
54
170
  if (timer) clearTimeout(timer);
171
+ callerSignal == null ? void 0 : callerSignal.removeEventListener("abort", onCallerAbort);
55
172
  }
56
173
  }
57
174
  };
58
175
 
59
176
  // src/client.ts
177
+ function isPlaintextRemote(baseUrl) {
178
+ try {
179
+ const url = new URL(baseUrl);
180
+ if (url.protocol !== "http:") return false;
181
+ const host = url.hostname.replace(/^\[|\]$/g, "");
182
+ return !(host === "localhost" || host === "::1" || /^127\./.test(host));
183
+ } catch (e) {
184
+ return false;
185
+ }
186
+ }
60
187
  var LumenWipeClient = class {
61
188
  constructor(options) {
189
+ this.warnedDefaultNetwork = false;
62
190
  var _a, _b, _c;
63
191
  const resolved = (_a = options.fetch) != null ? _a : globalThis.fetch;
64
192
  if (!resolved) {
@@ -68,77 +196,130 @@ var LumenWipeClient = class {
68
196
  options.baseUrl.replace(/\/+$/, ""),
69
197
  options.apiKey,
70
198
  (_b = options.timeout) != null ? _b : 3e4,
71
- resolved
199
+ resolved,
200
+ options.retry
72
201
  );
73
- this.defaultNetwork = (_c = options.network) != null ? _c : "testnet";
202
+ this.defaultNetwork = options.network;
203
+ this.log = (_c = options.logger) != null ? _c : ((message) => console.warn(message));
204
+ if (isPlaintextRemote(options.baseUrl)) {
205
+ this.log(
206
+ "LumenWipe: baseUrl uses http:// for a non-local host. The API key is sent in clear text; use https://."
207
+ );
208
+ }
209
+ }
210
+ 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
+ );
218
+ }
219
+ return "testnet";
74
220
  }
75
- health() {
76
- return this.http.request("GET", "/health");
221
+ health(options) {
222
+ return this.http.request("GET", "/health", void 0, __spreadProps(__spreadValues({}, options), {
223
+ retry: "transient"
224
+ }));
77
225
  }
78
- getAccount(address, network = this.defaultNetwork) {
226
+ getAccount(address, network, options) {
79
227
  return this.http.request(
80
228
  "GET",
81
- `/${network}/account/${encodeURIComponent(address)}`
229
+ `/${this.resolveNetwork(network)}/account/${encodeURIComponent(address)}`,
230
+ void 0,
231
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
82
232
  );
83
233
  }
84
- getPaths(params, network = this.defaultNetwork) {
234
+ getPaths(params, network, options) {
85
235
  const query = new URLSearchParams({ fromAsset: params.fromAsset, amount: params.amount });
86
- return this.http.request("GET", `/${network}/paths?${query.toString()}`);
236
+ return this.http.request(
237
+ "GET",
238
+ `/${this.resolveNetwork(network)}/paths?${query.toString()}`,
239
+ void 0,
240
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
241
+ );
87
242
  }
88
- closePlan(body, network = this.defaultNetwork) {
89
- return this.http.request("POST", `/v1/${network}/close/plan`, body);
243
+ closePlan(body, network, options) {
244
+ return this.http.request(
245
+ "POST",
246
+ `/v1/${this.resolveNetwork(network)}/close/plan`,
247
+ body,
248
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
249
+ );
90
250
  }
91
251
  /**
92
252
  * Builds the next unsigned transaction(s) for a close. A close can span several
93
- * transactions (a fused close, or separate claim / cleanup / mediator-merge steps),
253
+ * transactions (a single transaction, or separate claim / cleanup / mediator-merge steps),
94
254
  * so the response's `remaining.requiresAnotherCall` says whether more follow: sign and
95
255
  * submit the returned transactions in `order`, wait for confirmation, then call this
96
256
  * again until `requiresAnotherCall` is false.
97
257
  */
98
- closeTransactions(body, network = this.defaultNetwork) {
258
+ closeTransactions(body, network, options) {
99
259
  return this.http.request(
100
260
  "POST",
101
- `/v1/${network}/close/transactions`,
102
- body
261
+ `/v1/${this.resolveNetwork(network)}/close/transactions`,
262
+ body,
263
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
103
264
  );
104
265
  }
105
- submit(signedXdr, network = this.defaultNetwork) {
106
- return this.http.request("POST", `/v1/${network}/submit`, { signedXdr });
266
+ submit(signedXdr, network, options) {
267
+ return this.http.request(
268
+ "POST",
269
+ `/v1/${this.resolveNetwork(network)}/submit`,
270
+ { signedXdr },
271
+ __spreadProps(__spreadValues({}, options), { retry: "never" })
272
+ );
107
273
  }
108
- mediatorCheck(address, network = this.defaultNetwork) {
274
+ mediatorCheck(address, network, options) {
109
275
  return this.http.request(
110
276
  "GET",
111
- `/${network}/mediator/check/${encodeURIComponent(address)}`
277
+ `/${this.resolveNetwork(network)}/mediator/check/${encodeURIComponent(address)}`,
278
+ void 0,
279
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
112
280
  );
113
281
  }
114
- mediatorSign(transaction, network = this.defaultNetwork) {
115
- return this.http.request("POST", `/${network}/mediator/sign`, {
116
- transaction
117
- });
282
+ mediatorSign(transaction, network, options) {
283
+ return this.http.request(
284
+ "POST",
285
+ `/${this.resolveNetwork(network)}/mediator/sign`,
286
+ {
287
+ transaction
288
+ },
289
+ __spreadProps(__spreadValues({}, options), { retry: "never" })
290
+ );
118
291
  }
119
292
  /** Wraps a wind-down transaction (its own fee already zero) in a signed CAP-15 fee-bump
120
293
  * envelope for an account that cannot pay its own fee (architecture.md §8.1). The caller
121
294
  * submits the returned XDR through `submit()`, exactly like any other close transaction. */
122
- feeBumpSponsor(transaction, network = this.defaultNetwork) {
123
- return this.http.request("POST", `/${network}/fee-bump/sponsor`, {
124
- transaction
125
- });
295
+ feeBumpSponsor(transaction, network, options) {
296
+ return this.http.request(
297
+ "POST",
298
+ `/${this.resolveNetwork(network)}/fee-bump/sponsor`,
299
+ {
300
+ transaction
301
+ },
302
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
303
+ );
126
304
  }
127
305
  /** Every live SEP-41 allowance the account has granted (architecture.md §12). Independent of
128
306
  * closing an account - a standalone security utility. */
129
- getAllowances(address, network = this.defaultNetwork) {
307
+ getAllowances(address, network, options) {
130
308
  return this.http.request(
131
309
  "GET",
132
- `/${network}/allowances/${encodeURIComponent(address)}`
310
+ `/${this.resolveNetwork(network)}/allowances/${encodeURIComponent(address)}`,
311
+ void 0,
312
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
133
313
  );
134
314
  }
135
315
  /** Builds the unsigned `approve(owner, spender, 0, 0)` transaction that revokes one allowance.
136
316
  * The caller signs and submits it through `submit()`, like any other transaction here. */
137
- revokeAllowance(params, network = this.defaultNetwork) {
317
+ revokeAllowance(params, network, options) {
138
318
  return this.http.request(
139
319
  "POST",
140
- `/${network}/allowances/revoke`,
141
- params
320
+ `/${this.resolveNetwork(network)}/allowances/revoke`,
321
+ params,
322
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
142
323
  );
143
324
  }
144
325
  };
@@ -196,8 +377,10 @@ async function signOrThrow(deps, tx, xdr, queue, requiresAnotherCall) {
196
377
  }
197
378
  export {
198
379
  InsufficientSignatureWeightError,
380
+ LumenWipeAbortError,
199
381
  LumenWipeApiError,
200
382
  LumenWipeClient,
201
383
  LumenWipeTimeoutError,
384
+ isApiErrorCode,
202
385
  runClose
203
386
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumenwipe/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.5.1",
4
4
  "description": "Typed fetch client for the LumenWipe API - non-custodial Stellar account closing.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -44,12 +44,14 @@
44
44
  "build": "bun run build:types && api-extractor run --local && cp dist/index.d.ts dist/index.d.cts",
45
45
  "build:types": "bun run --filter '@lumenwipe/types' build && tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json",
46
46
  "check-api": "api-extractor run",
47
- "type-check": "tsc --noEmit && tsc -p tests/tsconfig.json --noEmit",
48
- "lint": "eslint src",
49
- "test": "bun test tests"
47
+ "type-check": "tsc --noEmit && tsc -p tests/tsconfig.json --noEmit && tsc -p examples/tsconfig.json --noEmit",
48
+ "lint": "eslint src examples",
49
+ "test": "bun test tests",
50
+ "example:headless": "bun run examples/headless-close.ts"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@microsoft/api-extractor": "^7.59.0",
54
+ "@stellar/stellar-sdk": "16.0.1",
53
55
  "@types/bun": "^1.4.0",
54
56
  "@typescript-eslint/eslint-plugin": "^8.69.0",
55
57
  "@typescript-eslint/parser": "^8.69.0",