@lumenwipe/sdk 0.2.0 → 0.7.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/README.md CHANGED
@@ -25,16 +25,16 @@ const client = new LumenWipeClient({
25
25
  network: "mainnet", // or "testnet"
26
26
  });
27
27
 
28
- const account = await client.getAccount(address);
28
+ const account = await client.getAccount(source);
29
29
 
30
30
  const plan = await client.closePlan({
31
- address,
31
+ source,
32
32
  destination,
33
33
  // ...see @lumenwipe/types for the full request shape
34
34
  });
35
35
 
36
36
  const { transactions, remaining } = await client.closeTransactions({
37
- address,
37
+ source,
38
38
  destination,
39
39
  });
40
40
 
@@ -49,7 +49,7 @@ instead of calling `closeTransactions`/`submit` by hand:
49
49
  import { runClose } from "@lumenwipe/sdk";
50
50
 
51
51
  await runClose({
52
- getTransactions: () => client.closeTransactions({ address, destination }),
52
+ getTransactions: () => client.closeTransactions({ source, destination }),
53
53
  verify: (tx) => myVerify(tx), // see "Verification" below - this is your responsibility
54
54
  requiredWeight: (tx) => myRequiredWeight(tx),
55
55
  sign: (tx, xdr) => mySigner(tx, xdr),
@@ -57,6 +57,20 @@ await runClose({
57
57
  });
58
58
  ```
59
59
 
60
+ ## Headless example
61
+
62
+ [`examples/headless-close.ts`](https://github.com/LumenWipe/lumenwipe/blob/main/packages/sdk/examples/headless-close.ts)
63
+ closes a throwaway testnet account end to end from a plain Node/Bun process, with no browser or
64
+ web UI: it funds two accounts, then runs `getAccount`, `closePlan`, `closeTransactions` and
65
+ `submit` through `runClose`, signing with an in-process keypair. It decodes and verifies every
66
+ transaction itself before signing (`examples/verify.ts`), which is the pattern to copy.
67
+
68
+ ```bash
69
+ LUMENWIPE_API_KEY=... bun run --filter '@lumenwipe/sdk' example:headless
70
+ ```
71
+
72
+ `LUMENWIPE_API_URL` overrides the default `https://api.lumenwipe.com`. Testnet only.
73
+
60
74
  ## Verification is the caller's responsibility
61
75
 
62
76
  This SDK does not verify or sign anything. Every transaction it returns is unsigned XDR built by
@@ -86,15 +100,16 @@ no separate install is needed.
86
100
 
87
101
  ## Errors
88
102
 
89
- - `LumenWipeApiError` - thrown on any non-2xx API response; carries `status` and the parsed
90
- error `body`.
103
+ - `LumenWipeApiError` - thrown on any non-2xx API response; carries `status`, `code`, `details`,
104
+ `retryAfterMs`, `requestId` and the parsed error `body`. Narrow with `isApiErrorCode`.
105
+ - `LumenWipeAbortError` - thrown when the `signal` you passed aborts the request.
91
106
  - `LumenWipeTimeoutError` - thrown when a request exceeds the configured `timeout`
92
107
  (default 30s).
93
108
 
94
109
  ## Links
95
110
 
96
111
  - [Architecture](https://github.com/LumenWipe/lumenwipe/blob/main/docs/architecture.md)
97
- - [Full documentation](https://docs.lumenwipe.com)
112
+ - [Full documentation](https://docs.lumenwipe.com/sdk/overview)
98
113
  - [Issues](https://github.com/LumenWipe/lumenwipe/issues)
99
114
 
100
115
  ## License
package/dist/index.cjs CHANGED
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
10
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
+ var __spreadValues = (a, b) => {
12
+ for (var prop in b || (b = {}))
13
+ if (__hasOwnProp.call(b, prop))
14
+ __defNormalProp(a, prop, b[prop]);
15
+ if (__getOwnPropSymbols)
16
+ for (var prop of __getOwnPropSymbols(b)) {
17
+ if (__propIsEnum.call(b, prop))
18
+ __defNormalProp(a, prop, b[prop]);
19
+ }
20
+ return a;
21
+ };
22
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
6
23
  var __export = (target, all) => {
7
24
  for (var name in all)
8
25
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -21,22 +38,50 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
38
  var index_exports = {};
22
39
  __export(index_exports, {
23
40
  InsufficientSignatureWeightError: () => InsufficientSignatureWeightError,
41
+ LumenWipeAbortError: () => LumenWipeAbortError,
24
42
  LumenWipeApiError: () => LumenWipeApiError,
25
43
  LumenWipeClient: () => LumenWipeClient,
26
44
  LumenWipeTimeoutError: () => LumenWipeTimeoutError,
45
+ isApiErrorCode: () => isApiErrorCode,
27
46
  runClose: () => runClose
28
47
  });
29
48
  module.exports = __toCommonJS(index_exports);
30
49
 
31
50
  // src/errors.ts
51
+ function parseRetryAfterMs(value) {
52
+ if (!value) return void 0;
53
+ const trimmed = value.trim();
54
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
55
+ const at = Date.parse(trimmed);
56
+ return Number.isNaN(at) ? void 0 : Math.max(0, at - Date.now());
57
+ }
58
+ function readEnvelope(body) {
59
+ if (typeof body !== "object" || body === null || !("error" in body)) return void 0;
60
+ const error = body.error;
61
+ if (typeof error !== "object" || error === null) return void 0;
62
+ const { code, message, details } = error;
63
+ if (typeof code !== "string" || typeof message !== "string") return void 0;
64
+ return { code, message, details };
65
+ }
32
66
  var LumenWipeApiError = class extends Error {
33
- constructor(status, body) {
34
- super(`LumenWipe API error ${status}`);
67
+ constructor(status, body, headers) {
68
+ var _a, _b;
69
+ const envelope = readEnvelope(body);
70
+ super(
71
+ envelope ? `LumenWipe API error ${status} ${envelope.code}: ${envelope.message}` : `LumenWipe API error ${status}`
72
+ );
35
73
  this.status = status;
36
74
  this.body = body;
37
75
  this.name = "LumenWipeApiError";
76
+ this.code = (_a = envelope == null ? void 0 : envelope.code) != null ? _a : "upstream_error";
77
+ this.details = envelope == null ? void 0 : envelope.details;
78
+ this.retryAfterMs = parseRetryAfterMs(headers == null ? void 0 : headers.get("Retry-After"));
79
+ this.requestId = (_b = headers == null ? void 0 : headers.get("X-Request-Id")) != null ? _b : void 0;
38
80
  }
39
81
  };
82
+ function isApiErrorCode(error, code) {
83
+ return error instanceof LumenWipeApiError && error.code === code;
84
+ }
40
85
  var LumenWipeTimeoutError = class extends Error {
41
86
  constructor(timeoutMs) {
42
87
  super(`LumenWipe API request timed out after ${timeoutMs}ms`);
@@ -44,21 +89,90 @@ var LumenWipeTimeoutError = class extends Error {
44
89
  this.name = "LumenWipeTimeoutError";
45
90
  }
46
91
  };
92
+ var LumenWipeAbortError = class extends Error {
93
+ constructor(reason) {
94
+ super("LumenWipe API request was aborted");
95
+ this.reason = reason;
96
+ this.name = "LumenWipeAbortError";
97
+ }
98
+ };
99
+
100
+ // src/version.ts
101
+ var SDK_VERSION = "0.7.0";
47
102
 
48
103
  // src/http.ts
104
+ var MAX_DELAY_MS = 3e4;
105
+ var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
106
+ var THROTTLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
49
107
  var HttpTransport = class {
50
- constructor(baseUrl, apiKey, timeout, doFetch) {
108
+ constructor(baseUrl, apiKey, timeout, doFetch, retry) {
51
109
  this.baseUrl = baseUrl;
52
110
  this.apiKey = apiKey;
53
111
  this.timeout = timeout;
54
112
  this.doFetch = doFetch;
113
+ this.retry = retry;
55
114
  }
56
- async request(method, path, body) {
57
- const headers = { Authorization: `Bearer ${this.apiKey}` };
115
+ async request(method, path, body, opts) {
116
+ var _a, _b;
117
+ const attempts = opts.retry === "never" ? 1 : Math.max(1, Math.floor((_b = (_a = this.retry) == null ? void 0 : _a.attempts) != null ? _b : 1));
118
+ for (let attempt = 1; ; attempt++) {
119
+ try {
120
+ return await this.once(method, path, body, opts.signal);
121
+ } catch (e) {
122
+ if (attempt >= attempts || !this.shouldRetry(opts.retry, e)) throw e;
123
+ await this.sleep(this.delayMs(e, attempt), opts.signal);
124
+ }
125
+ }
126
+ }
127
+ shouldRetry(policy, e) {
128
+ if (e instanceof LumenWipeApiError) {
129
+ const statuses = policy === "transient" ? TRANSIENT_STATUSES : THROTTLE_STATUSES;
130
+ return statuses.has(e.status);
131
+ }
132
+ return policy === "transient" && !(e instanceof LumenWipeTimeoutError) && !(e instanceof LumenWipeAbortError);
133
+ }
134
+ delayMs(e, attempt) {
135
+ var _a, _b;
136
+ if (e instanceof LumenWipeApiError && e.retryAfterMs !== void 0) {
137
+ return Math.min(e.retryAfterMs, MAX_DELAY_MS);
138
+ }
139
+ const base = (_b = (_a = this.retry) == null ? void 0 : _a.baseDelayMs) != null ? _b : 0;
140
+ return Math.min(base * 2 ** (attempt - 1) * (0.5 + Math.random() / 2), MAX_DELAY_MS);
141
+ }
142
+ sleep(ms, signal) {
143
+ return new Promise((resolve, reject) => {
144
+ if (signal == null ? void 0 : signal.aborted) return reject(new LumenWipeAbortError(signal.reason));
145
+ const onAbort = () => {
146
+ clearTimeout(timer);
147
+ reject(new LumenWipeAbortError(signal == null ? void 0 : signal.reason));
148
+ };
149
+ const timer = setTimeout(() => {
150
+ signal == null ? void 0 : signal.removeEventListener("abort", onAbort);
151
+ resolve();
152
+ }, ms);
153
+ signal == null ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
154
+ });
155
+ }
156
+ async once(method, path, body, callerSignal) {
157
+ 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
+ };
58
162
  if (body !== void 0) headers["Content-Type"] = "application/json";
59
163
  const controller = new AbortController();
164
+ let timedOut = false;
165
+ let callerAborted = false;
166
+ const onCallerAbort = () => {
167
+ callerAborted = true;
168
+ controller.abort();
169
+ };
170
+ callerSignal == null ? void 0 : callerSignal.addEventListener("abort", onCallerAbort, { once: true });
60
171
  const bounded = Number.isFinite(this.timeout) && this.timeout > 0;
61
- const timer = bounded ? setTimeout(() => controller.abort(), this.timeout) : void 0;
172
+ const timer = bounded ? setTimeout(() => {
173
+ timedOut = true;
174
+ controller.abort();
175
+ }, this.timeout) : void 0;
62
176
  try {
63
177
  const res = await this.doFetch(`${this.baseUrl}${path}`, {
64
178
  method,
@@ -75,20 +189,33 @@ var HttpTransport = class {
75
189
  parsed = text;
76
190
  }
77
191
  }
78
- if (!res.ok) throw new LumenWipeApiError(res.status, parsed);
192
+ if (!res.ok) throw new LumenWipeApiError(res.status, parsed, res.headers);
79
193
  return parsed;
80
194
  } catch (e) {
81
- if (controller.signal.aborted) throw new LumenWipeTimeoutError(this.timeout);
195
+ if (callerAborted) throw new LumenWipeAbortError(callerSignal == null ? void 0 : callerSignal.reason);
196
+ if (timedOut) throw new LumenWipeTimeoutError(this.timeout);
82
197
  throw e;
83
198
  } finally {
84
199
  if (timer) clearTimeout(timer);
200
+ callerSignal == null ? void 0 : callerSignal.removeEventListener("abort", onCallerAbort);
85
201
  }
86
202
  }
87
203
  };
88
204
 
89
205
  // src/client.ts
206
+ function isPlaintextRemote(baseUrl) {
207
+ try {
208
+ const url = new URL(baseUrl);
209
+ if (url.protocol !== "http:") return false;
210
+ const host = url.hostname.replace(/^\[|\]$/g, "");
211
+ return !(host === "localhost" || host === "::1" || /^127\./.test(host));
212
+ } catch (e) {
213
+ return false;
214
+ }
215
+ }
90
216
  var LumenWipeClient = class {
91
217
  constructor(options) {
218
+ this.warnedDefaultNetwork = false;
92
219
  var _a, _b, _c;
93
220
  const resolved = (_a = options.fetch) != null ? _a : globalThis.fetch;
94
221
  if (!resolved) {
@@ -98,77 +225,130 @@ var LumenWipeClient = class {
98
225
  options.baseUrl.replace(/\/+$/, ""),
99
226
  options.apiKey,
100
227
  (_b = options.timeout) != null ? _b : 3e4,
101
- resolved
228
+ resolved,
229
+ options.retry
102
230
  );
103
- this.defaultNetwork = (_c = options.network) != null ? _c : "testnet";
231
+ this.defaultNetwork = options.network;
232
+ this.log = (_c = options.logger) != null ? _c : ((message) => console.warn(message));
233
+ if (isPlaintextRemote(options.baseUrl)) {
234
+ this.log(
235
+ "LumenWipe: baseUrl uses http:// for a non-local host. The API key is sent in clear text; use https://."
236
+ );
237
+ }
238
+ }
239
+ 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
+ );
247
+ }
248
+ return "testnet";
104
249
  }
105
- health() {
106
- return this.http.request("GET", "/health");
250
+ health(options) {
251
+ return this.http.request("GET", "/health", void 0, __spreadProps(__spreadValues({}, options), {
252
+ retry: "transient"
253
+ }));
107
254
  }
108
- getAccount(address, network = this.defaultNetwork) {
255
+ getAccount(address, network, options) {
109
256
  return this.http.request(
110
257
  "GET",
111
- `/${network}/account/${encodeURIComponent(address)}`
258
+ `/${this.resolveNetwork(network)}/account/${encodeURIComponent(address)}`,
259
+ void 0,
260
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
112
261
  );
113
262
  }
114
- getPaths(params, network = this.defaultNetwork) {
263
+ getPaths(params, network, options) {
115
264
  const query = new URLSearchParams({ fromAsset: params.fromAsset, amount: params.amount });
116
- return this.http.request("GET", `/${network}/paths?${query.toString()}`);
265
+ return this.http.request(
266
+ "GET",
267
+ `/${this.resolveNetwork(network)}/paths?${query.toString()}`,
268
+ void 0,
269
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
270
+ );
117
271
  }
118
- closePlan(body, network = this.defaultNetwork) {
119
- return this.http.request("POST", `/v1/${network}/close/plan`, body);
272
+ closePlan(body, network, options) {
273
+ return this.http.request(
274
+ "POST",
275
+ `/v1/${this.resolveNetwork(network)}/close/plan`,
276
+ body,
277
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
278
+ );
120
279
  }
121
280
  /**
122
281
  * Builds the next unsigned transaction(s) for a close. A close can span several
123
- * transactions (a fused close, or separate claim / cleanup / mediator-merge steps),
282
+ * transactions (a single transaction, or separate claim / cleanup / mediator-merge steps),
124
283
  * so the response's `remaining.requiresAnotherCall` says whether more follow: sign and
125
284
  * submit the returned transactions in `order`, wait for confirmation, then call this
126
285
  * again until `requiresAnotherCall` is false.
127
286
  */
128
- closeTransactions(body, network = this.defaultNetwork) {
287
+ closeTransactions(body, network, options) {
129
288
  return this.http.request(
130
289
  "POST",
131
- `/v1/${network}/close/transactions`,
132
- body
290
+ `/v1/${this.resolveNetwork(network)}/close/transactions`,
291
+ body,
292
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
133
293
  );
134
294
  }
135
- submit(signedXdr, network = this.defaultNetwork) {
136
- return this.http.request("POST", `/v1/${network}/submit`, { signedXdr });
295
+ submit(signedXdr, network, options) {
296
+ return this.http.request(
297
+ "POST",
298
+ `/v1/${this.resolveNetwork(network)}/submit`,
299
+ { signedXdr },
300
+ __spreadProps(__spreadValues({}, options), { retry: "never" })
301
+ );
137
302
  }
138
- mediatorCheck(address, network = this.defaultNetwork) {
303
+ mediatorCheck(address, network, options) {
139
304
  return this.http.request(
140
305
  "GET",
141
- `/${network}/mediator/check/${encodeURIComponent(address)}`
306
+ `/${this.resolveNetwork(network)}/mediator/check/${encodeURIComponent(address)}`,
307
+ void 0,
308
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
142
309
  );
143
310
  }
144
- mediatorSign(transaction, network = this.defaultNetwork) {
145
- return this.http.request("POST", `/${network}/mediator/sign`, {
146
- transaction
147
- });
311
+ mediatorSign(transaction, network, options) {
312
+ return this.http.request(
313
+ "POST",
314
+ `/${this.resolveNetwork(network)}/mediator/sign`,
315
+ {
316
+ transaction
317
+ },
318
+ __spreadProps(__spreadValues({}, options), { retry: "never" })
319
+ );
148
320
  }
149
321
  /** Wraps a wind-down transaction (its own fee already zero) in a signed CAP-15 fee-bump
150
322
  * envelope for an account that cannot pay its own fee (architecture.md §8.1). The caller
151
323
  * submits the returned XDR through `submit()`, exactly like any other close transaction. */
152
- feeBumpSponsor(transaction, network = this.defaultNetwork) {
153
- return this.http.request("POST", `/${network}/fee-bump/sponsor`, {
154
- transaction
155
- });
324
+ feeBumpSponsor(transaction, network, options) {
325
+ return this.http.request(
326
+ "POST",
327
+ `/${this.resolveNetwork(network)}/fee-bump/sponsor`,
328
+ {
329
+ transaction
330
+ },
331
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
332
+ );
156
333
  }
157
334
  /** Every live SEP-41 allowance the account has granted (architecture.md §12). Independent of
158
335
  * closing an account - a standalone security utility. */
159
- getAllowances(address, network = this.defaultNetwork) {
336
+ getAllowances(address, network, options) {
160
337
  return this.http.request(
161
338
  "GET",
162
- `/${network}/allowances/${encodeURIComponent(address)}`
339
+ `/${this.resolveNetwork(network)}/allowances/${encodeURIComponent(address)}`,
340
+ void 0,
341
+ __spreadProps(__spreadValues({}, options), { retry: "transient" })
163
342
  );
164
343
  }
165
344
  /** Builds the unsigned `approve(owner, spender, 0, 0)` transaction that revokes one allowance.
166
345
  * The caller signs and submits it through `submit()`, like any other transaction here. */
167
- revokeAllowance(params, network = this.defaultNetwork) {
346
+ revokeAllowance(params, network, options) {
168
347
  return this.http.request(
169
348
  "POST",
170
- `/${network}/allowances/revoke`,
171
- params
349
+ `/${this.resolveNetwork(network)}/allowances/revoke`,
350
+ params,
351
+ __spreadProps(__spreadValues({}, options), { retry: "throttle" })
172
352
  );
173
353
  }
174
354
  };
@@ -227,8 +407,10 @@ async function signOrThrow(deps, tx, xdr, queue, requiresAnotherCall) {
227
407
  // Annotate the CommonJS export names for ESM import in node:
228
408
  0 && (module.exports = {
229
409
  InsufficientSignatureWeightError,
410
+ LumenWipeAbortError,
230
411
  LumenWipeApiError,
231
412
  LumenWipeClient,
232
413
  LumenWipeTimeoutError,
414
+ isApiErrorCode,
233
415
  runClose
234
416
  });