@hypawave/sdk 0.2.2 → 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@hypawave/sdk` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0]
9
+
10
+ ### Added
11
+ - Automatic request retries with full-jitter exponential backoff, honoring the
12
+ `Retry-After` header on 429 / overload responses.
13
+ - `maxRetries` config option (default `3`; set `0` to disable retries).
14
+ - Adaptive backoff in `waitForSettlement` (ramps from `pollInterval` up to
15
+ `maxPollInterval`, default 2s → 20s) instead of a fixed-interval poll.
16
+
17
+ ### Changed
18
+ - Retry policy is gated by safety: every `GET` and the server-idempotent POSTs
19
+ (`confirmPayment`, `topup`, `getUnlockStatus`, `getOfferDownloadUrl`) retry on
20
+ 5xx and network errors; non-idempotent POSTs (e.g. `createInvoice`) retry only
21
+ on 429. This prevents duplicate side effects on retry.
22
+ - `timeout` now applies per attempt rather than to the whole call.
23
+ - Query parameters accept `string | number | boolean | undefined`; values are
24
+ serialized centrally and `undefined` is omitted.
25
+
26
+ ### Fixed
27
+ - Non-JSON responses (e.g. an HTML 502 from an overloaded proxy) now surface as
28
+ a clean `HypawaveAPIError` instead of throwing an opaque JSON parse error.
29
+ - A successful (2xx) response with a non-JSON body now throws instead of
30
+ returning a malformed success object.
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  **Bitcoin Lightning SDK for AI Agent Payments — non-custodial settlement with preimage-proof unlocks**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@hypawave/sdk.svg)](https://www.npmjs.com/package/@hypawave/sdk)
6
+ [![CI](https://github.com/hypawave/sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/hypawave/sdk/actions/workflows/ci.yml)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/hypawave/sdk/blob/main/LICENSE)
7
8
  [![Node >= 18](https://img.shields.io/badge/Node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
8
9
 
@@ -156,7 +157,8 @@ Both files point to two authoritative web sources so instructions stay fresh:
156
157
  |-----------|------|---------|-------------|
157
158
  | `apiKey` | `string` | — | API key (`sk_test_*` or `sk_live_*`) |
158
159
  | `baseUrl` | `string` | `https://hypawave.com` | API base URL |
159
- | `timeout` | `number` | `30000` | Request timeout in ms |
160
+ | `timeout` | `number` | `30000` | Request timeout in ms (per attempt) |
161
+ | `maxRetries` | `number` | `3` | Max automatic retries on 429 / retryable 5xx / network errors (`0` disables). Honors `Retry-After`. |
160
162
 
161
163
  ### `createInvoice(params)`
162
164
 
@@ -330,12 +332,15 @@ const { downloadUrl } = await pp.getOfferDownloadUrl(paymentIntentId, {
330
332
 
331
333
  ### `waitForSettlement(invoiceId, options?)`
332
334
 
333
- Poll until an invoice settles, fails, or expires.
335
+ Poll until an invoice settles, fails, or expires. Polling backs off
336
+ adaptively — the interval ramps from `pollInterval` up to `maxPollInterval`
337
+ (with jitter) so a busy backend isn't hit on a fixed cadence.
334
338
 
335
339
  ```typescript
336
340
  const result = await pp.waitForSettlement(invoiceId, {
337
- pollInterval: 2000, // ms between polls (default: 2000)
338
- timeout: 300000, // max wait time in ms (default: 300000)
341
+ pollInterval: 2000, // starting interval in ms (default: 2000)
342
+ maxPollInterval: 20000, // ceiling the interval ramps toward (default: 20000)
343
+ timeout: 300000, // max wait time in ms (default: 300000)
339
344
  });
340
345
 
341
346
  if (result.unlocked) {
package/dist/index.d.mts CHANGED
@@ -2,6 +2,8 @@ interface HypawaveConfig {
2
2
  apiKey: string;
3
3
  baseUrl?: string;
4
4
  timeout?: number;
5
+ /** Max automatic retries on 429 / retryable 5xx / network errors. Default 3. Set 0 to disable. */
6
+ maxRetries?: number;
5
7
  }
6
8
  interface CreateInvoiceParams {
7
9
  client_email: string;
@@ -259,6 +261,7 @@ declare class Hypawave {
259
261
  private readonly apiKey;
260
262
  private readonly baseUrl;
261
263
  private readonly timeout;
264
+ private readonly maxRetries;
262
265
  constructor(config: HypawaveConfig);
263
266
  private request;
264
267
  createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceResponse>;
@@ -292,6 +295,7 @@ declare class Hypawave {
292
295
  getSettings(): Promise<PublicSettingsResponse>;
293
296
  waitForSettlement(invoiceId: number, options?: {
294
297
  pollInterval?: number;
298
+ maxPollInterval?: number;
295
299
  timeout?: number;
296
300
  }): Promise<UnlockStatusResponse["statuses"][string]>;
297
301
  payAndConfirm(params: PayAndConfirmParams): Promise<PayAndConfirmResult>;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ interface HypawaveConfig {
2
2
  apiKey: string;
3
3
  baseUrl?: string;
4
4
  timeout?: number;
5
+ /** Max automatic retries on 429 / retryable 5xx / network errors. Default 3. Set 0 to disable. */
6
+ maxRetries?: number;
5
7
  }
6
8
  interface CreateInvoiceParams {
7
9
  client_email: string;
@@ -259,6 +261,7 @@ declare class Hypawave {
259
261
  private readonly apiKey;
260
262
  private readonly baseUrl;
261
263
  private readonly timeout;
264
+ private readonly maxRetries;
262
265
  constructor(config: HypawaveConfig);
263
266
  private request;
264
267
  createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceResponse>;
@@ -292,6 +295,7 @@ declare class Hypawave {
292
295
  getSettings(): Promise<PublicSettingsResponse>;
293
296
  waitForSettlement(invoiceId: number, options?: {
294
297
  pollInterval?: number;
298
+ maxPollInterval?: number;
295
299
  timeout?: number;
296
300
  }): Promise<UnlockStatusResponse["statuses"][string]>;
297
301
  payAndConfirm(params: PayAndConfirmParams): Promise<PayAndConfirmResult>;
package/dist/index.js CHANGED
@@ -51,6 +51,20 @@ var HypawaveAPIError = class extends Error {
51
51
  // src/client.ts
52
52
  var DEFAULT_BASE_URL = "https://hypawave.com";
53
53
  var DEFAULT_TIMEOUT = 3e4;
54
+ var DEFAULT_MAX_RETRIES = 3;
55
+ var RETRY_BASE_MS = 500;
56
+ var RETRY_CAP_MS = 8e3;
57
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
58
+ function backoffDelay(attempt, retryAfter) {
59
+ if (retryAfter) {
60
+ const secs = Number(retryAfter);
61
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
62
+ const date = Date.parse(retryAfter);
63
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
64
+ }
65
+ const exp = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
66
+ return Math.random() * exp;
67
+ }
54
68
  async function sha256hex(hex) {
55
69
  const bytes = new Uint8Array(hex.match(/.{2}/g).map((b) => parseInt(b, 16)));
56
70
  const hash = await crypto.subtle.digest("SHA-256", bytes);
@@ -67,42 +81,71 @@ var Hypawave = class {
67
81
  this.apiKey = config.apiKey;
68
82
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
69
83
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
84
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
70
85
  }
71
- async request(method, path, body, query) {
86
+ async request(method, path, body, query, opts) {
72
87
  let url = `${this.baseUrl}${path}`;
73
88
  if (query) {
74
- const params = new URLSearchParams(query);
75
- url += `?${params.toString()}`;
76
- }
77
- const controller = new AbortController();
78
- const timer = setTimeout(() => controller.abort(), this.timeout);
79
- try {
80
- const res = await fetch(url, {
81
- method,
82
- headers: {
83
- Authorization: `Bearer ${this.apiKey}`,
84
- "Content-Type": "application/json",
85
- Accept: "application/json"
86
- },
87
- body: body ? JSON.stringify(body) : void 0,
88
- signal: controller.signal
89
- });
90
- const data = await res.json();
91
- if (!res.ok) {
92
- throw new HypawaveAPIError(res.status, data);
89
+ const params = new URLSearchParams();
90
+ for (const [k, v] of Object.entries(query)) {
91
+ if (v !== void 0) params.set(k, String(v));
93
92
  }
94
- return data;
95
- } catch (err) {
96
- if (err instanceof HypawaveAPIError) throw err;
97
- if (err instanceof DOMException && err.name === "AbortError") {
98
- throw new HypawaveAPIError(408, {
99
- error: "timeout",
100
- message: `Request timed out after ${this.timeout}ms`
93
+ const qs = params.toString();
94
+ if (qs) url += `?${qs}`;
95
+ }
96
+ const retrySafe = opts?.retrySafe ?? method === "GET";
97
+ for (let attempt = 0; ; attempt++) {
98
+ const controller = new AbortController();
99
+ const timer = setTimeout(() => controller.abort(), this.timeout);
100
+ try {
101
+ const res = await fetch(url, {
102
+ method,
103
+ headers: {
104
+ Authorization: `Bearer ${this.apiKey}`,
105
+ "Content-Type": "application/json",
106
+ Accept: "application/json"
107
+ },
108
+ body: body ? JSON.stringify(body) : void 0,
109
+ signal: controller.signal
101
110
  });
111
+ let data;
112
+ let parseFailed = false;
113
+ try {
114
+ data = await res.json();
115
+ } catch {
116
+ parseFailed = true;
117
+ data = res.ok ? { error: "invalid_response", message: "Server returned a successful status with a non-JSON body." } : { error: `http_${res.status}`, message: `Server returned status ${res.status} with a non-JSON body.` };
118
+ }
119
+ if (!res.ok) {
120
+ const retryable = res.status === 429 || res.status >= 500 && retrySafe;
121
+ if (retryable && attempt < this.maxRetries) {
122
+ clearTimeout(timer);
123
+ await sleep(backoffDelay(attempt, res.headers.get("retry-after")));
124
+ continue;
125
+ }
126
+ throw new HypawaveAPIError(res.status, data);
127
+ }
128
+ if (parseFailed) {
129
+ throw new HypawaveAPIError(res.status, data);
130
+ }
131
+ return data;
132
+ } catch (err) {
133
+ if (err instanceof HypawaveAPIError) throw err;
134
+ if (err instanceof DOMException && err.name === "AbortError") {
135
+ throw new HypawaveAPIError(408, {
136
+ error: "timeout",
137
+ message: `Request timed out after ${this.timeout}ms`
138
+ });
139
+ }
140
+ if (retrySafe && attempt < this.maxRetries) {
141
+ clearTimeout(timer);
142
+ await sleep(backoffDelay(attempt, null));
143
+ continue;
144
+ }
145
+ throw err;
146
+ } finally {
147
+ clearTimeout(timer);
102
148
  }
103
- throw err;
104
- } finally {
105
- clearTimeout(timer);
106
149
  }
107
150
  }
108
151
  async createInvoice(params) {
@@ -112,7 +155,9 @@ var Hypawave = class {
112
155
  return this.request("GET", "/api/paystream-cb", void 0, { token: accessToken });
113
156
  }
114
157
  async confirmPayment(invoiceId, params) {
115
- return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params);
158
+ return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params, void 0, {
159
+ retrySafe: true
160
+ });
116
161
  }
117
162
  getPaymentPayload(params, response) {
118
163
  return {
@@ -145,12 +190,16 @@ var Hypawave = class {
145
190
  * - duplicate_topup — a pending top-up invoice already exists
146
191
  */
147
192
  async topup(_params) {
148
- return this.request("POST", "/api/agent/topup", {});
193
+ return this.request("POST", "/api/agent/topup", {}, void 0, { retrySafe: true });
149
194
  }
150
195
  async getUnlockStatus(invoiceIds) {
151
- return this.request("POST", "/api/get-unlock-status", {
152
- invoice_ids: invoiceIds
153
- });
196
+ return this.request(
197
+ "POST",
198
+ "/api/get-unlock-status",
199
+ { invoice_ids: invoiceIds },
200
+ void 0,
201
+ { retrySafe: true }
202
+ );
154
203
  }
155
204
  async getKey(invoiceFileId, token) {
156
205
  const query = { invoice_file_id: invoiceFileId };
@@ -167,16 +216,11 @@ var Hypawave = class {
167
216
  return this.request("POST", "/api/agent/store-file-key", params);
168
217
  }
169
218
  async listInvoices(params) {
170
- const query = {};
171
- if (params?.limit !== void 0) query.limit = String(params.limit);
172
- if (params?.offset !== void 0) query.offset = String(params.offset);
173
- if (params?.status) query.status = params.status;
174
- return this.request(
175
- "GET",
176
- "/api/agent/list-invoices",
177
- void 0,
178
- Object.keys(query).length ? query : void 0
179
- );
219
+ return this.request("GET", "/api/agent/list-invoices", void 0, {
220
+ limit: params?.limit,
221
+ offset: params?.offset,
222
+ status: params?.status
223
+ });
180
224
  }
181
225
  async getInvoiceFiles(invoiceIds, token) {
182
226
  const body = { invoice_ids: invoiceIds };
@@ -192,12 +236,14 @@ var Hypawave = class {
192
236
  return this.request(
193
237
  "POST",
194
238
  `/api/offers/payment-intent/${paymentIntentId}/download-url`,
195
- params
239
+ params,
240
+ void 0,
241
+ { retrySafe: true }
196
242
  );
197
243
  }
198
244
  async getReceipt(invoiceId) {
199
245
  return this.request("GET", "/api/agent/receipt", void 0, {
200
- invoice_id: String(invoiceId)
246
+ invoice_id: invoiceId
201
247
  });
202
248
  }
203
249
  async getPayerReceipt(invoiceId, preimage) {
@@ -209,10 +255,11 @@ var Hypawave = class {
209
255
  return this.request("GET", "/api/public-settings");
210
256
  }
211
257
  async waitForSettlement(invoiceId, options) {
212
- const interval = options?.pollInterval ?? 2e3;
258
+ const base = options?.pollInterval ?? 2e3;
259
+ const maxInterval = options?.maxPollInterval ?? 2e4;
213
260
  const timeout = options?.timeout ?? 3e5;
214
261
  const start = Date.now();
215
- while (Date.now() - start < timeout) {
262
+ for (let attempt = 0; Date.now() - start < timeout; attempt++) {
216
263
  const status = await this.getUnlockStatus([invoiceId]);
217
264
  const entry = status.statuses?.[String(invoiceId)];
218
265
  if (entry?.unlocked) {
@@ -221,7 +268,9 @@ var Hypawave = class {
221
268
  if (entry?.status === "failed" || entry?.status === "expired") {
222
269
  return entry;
223
270
  }
224
- await new Promise((r) => setTimeout(r, interval));
271
+ const ceiling = Math.min(maxInterval, base * 2 ** attempt);
272
+ const wait = ceiling / 2 + Math.random() * (ceiling / 2);
273
+ await sleep(Math.min(wait, Math.max(0, start + timeout - Date.now())));
225
274
  }
226
275
  throw new HypawaveAPIError(408, {
227
276
  error: "settlement_timeout",
package/dist/index.mjs CHANGED
@@ -24,6 +24,20 @@ var HypawaveAPIError = class extends Error {
24
24
  // src/client.ts
25
25
  var DEFAULT_BASE_URL = "https://hypawave.com";
26
26
  var DEFAULT_TIMEOUT = 3e4;
27
+ var DEFAULT_MAX_RETRIES = 3;
28
+ var RETRY_BASE_MS = 500;
29
+ var RETRY_CAP_MS = 8e3;
30
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
+ function backoffDelay(attempt, retryAfter) {
32
+ if (retryAfter) {
33
+ const secs = Number(retryAfter);
34
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
35
+ const date = Date.parse(retryAfter);
36
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
37
+ }
38
+ const exp = Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
39
+ return Math.random() * exp;
40
+ }
27
41
  async function sha256hex(hex) {
28
42
  const bytes = new Uint8Array(hex.match(/.{2}/g).map((b) => parseInt(b, 16)));
29
43
  const hash = await crypto.subtle.digest("SHA-256", bytes);
@@ -40,42 +54,71 @@ var Hypawave = class {
40
54
  this.apiKey = config.apiKey;
41
55
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
42
56
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
57
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_MAX_RETRIES);
43
58
  }
44
- async request(method, path, body, query) {
59
+ async request(method, path, body, query, opts) {
45
60
  let url = `${this.baseUrl}${path}`;
46
61
  if (query) {
47
- const params = new URLSearchParams(query);
48
- url += `?${params.toString()}`;
49
- }
50
- const controller = new AbortController();
51
- const timer = setTimeout(() => controller.abort(), this.timeout);
52
- try {
53
- const res = await fetch(url, {
54
- method,
55
- headers: {
56
- Authorization: `Bearer ${this.apiKey}`,
57
- "Content-Type": "application/json",
58
- Accept: "application/json"
59
- },
60
- body: body ? JSON.stringify(body) : void 0,
61
- signal: controller.signal
62
- });
63
- const data = await res.json();
64
- if (!res.ok) {
65
- throw new HypawaveAPIError(res.status, data);
62
+ const params = new URLSearchParams();
63
+ for (const [k, v] of Object.entries(query)) {
64
+ if (v !== void 0) params.set(k, String(v));
66
65
  }
67
- return data;
68
- } catch (err) {
69
- if (err instanceof HypawaveAPIError) throw err;
70
- if (err instanceof DOMException && err.name === "AbortError") {
71
- throw new HypawaveAPIError(408, {
72
- error: "timeout",
73
- message: `Request timed out after ${this.timeout}ms`
66
+ const qs = params.toString();
67
+ if (qs) url += `?${qs}`;
68
+ }
69
+ const retrySafe = opts?.retrySafe ?? method === "GET";
70
+ for (let attempt = 0; ; attempt++) {
71
+ const controller = new AbortController();
72
+ const timer = setTimeout(() => controller.abort(), this.timeout);
73
+ try {
74
+ const res = await fetch(url, {
75
+ method,
76
+ headers: {
77
+ Authorization: `Bearer ${this.apiKey}`,
78
+ "Content-Type": "application/json",
79
+ Accept: "application/json"
80
+ },
81
+ body: body ? JSON.stringify(body) : void 0,
82
+ signal: controller.signal
74
83
  });
84
+ let data;
85
+ let parseFailed = false;
86
+ try {
87
+ data = await res.json();
88
+ } catch {
89
+ parseFailed = true;
90
+ data = res.ok ? { error: "invalid_response", message: "Server returned a successful status with a non-JSON body." } : { error: `http_${res.status}`, message: `Server returned status ${res.status} with a non-JSON body.` };
91
+ }
92
+ if (!res.ok) {
93
+ const retryable = res.status === 429 || res.status >= 500 && retrySafe;
94
+ if (retryable && attempt < this.maxRetries) {
95
+ clearTimeout(timer);
96
+ await sleep(backoffDelay(attempt, res.headers.get("retry-after")));
97
+ continue;
98
+ }
99
+ throw new HypawaveAPIError(res.status, data);
100
+ }
101
+ if (parseFailed) {
102
+ throw new HypawaveAPIError(res.status, data);
103
+ }
104
+ return data;
105
+ } catch (err) {
106
+ if (err instanceof HypawaveAPIError) throw err;
107
+ if (err instanceof DOMException && err.name === "AbortError") {
108
+ throw new HypawaveAPIError(408, {
109
+ error: "timeout",
110
+ message: `Request timed out after ${this.timeout}ms`
111
+ });
112
+ }
113
+ if (retrySafe && attempt < this.maxRetries) {
114
+ clearTimeout(timer);
115
+ await sleep(backoffDelay(attempt, null));
116
+ continue;
117
+ }
118
+ throw err;
119
+ } finally {
120
+ clearTimeout(timer);
75
121
  }
76
- throw err;
77
- } finally {
78
- clearTimeout(timer);
79
122
  }
80
123
  }
81
124
  async createInvoice(params) {
@@ -85,7 +128,9 @@ var Hypawave = class {
85
128
  return this.request("GET", "/api/paystream-cb", void 0, { token: accessToken });
86
129
  }
87
130
  async confirmPayment(invoiceId, params) {
88
- return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params);
131
+ return this.request("POST", `/api/invoice/${invoiceId}/confirm`, params, void 0, {
132
+ retrySafe: true
133
+ });
89
134
  }
90
135
  getPaymentPayload(params, response) {
91
136
  return {
@@ -118,12 +163,16 @@ var Hypawave = class {
118
163
  * - duplicate_topup — a pending top-up invoice already exists
119
164
  */
120
165
  async topup(_params) {
121
- return this.request("POST", "/api/agent/topup", {});
166
+ return this.request("POST", "/api/agent/topup", {}, void 0, { retrySafe: true });
122
167
  }
123
168
  async getUnlockStatus(invoiceIds) {
124
- return this.request("POST", "/api/get-unlock-status", {
125
- invoice_ids: invoiceIds
126
- });
169
+ return this.request(
170
+ "POST",
171
+ "/api/get-unlock-status",
172
+ { invoice_ids: invoiceIds },
173
+ void 0,
174
+ { retrySafe: true }
175
+ );
127
176
  }
128
177
  async getKey(invoiceFileId, token) {
129
178
  const query = { invoice_file_id: invoiceFileId };
@@ -140,16 +189,11 @@ var Hypawave = class {
140
189
  return this.request("POST", "/api/agent/store-file-key", params);
141
190
  }
142
191
  async listInvoices(params) {
143
- const query = {};
144
- if (params?.limit !== void 0) query.limit = String(params.limit);
145
- if (params?.offset !== void 0) query.offset = String(params.offset);
146
- if (params?.status) query.status = params.status;
147
- return this.request(
148
- "GET",
149
- "/api/agent/list-invoices",
150
- void 0,
151
- Object.keys(query).length ? query : void 0
152
- );
192
+ return this.request("GET", "/api/agent/list-invoices", void 0, {
193
+ limit: params?.limit,
194
+ offset: params?.offset,
195
+ status: params?.status
196
+ });
153
197
  }
154
198
  async getInvoiceFiles(invoiceIds, token) {
155
199
  const body = { invoice_ids: invoiceIds };
@@ -165,12 +209,14 @@ var Hypawave = class {
165
209
  return this.request(
166
210
  "POST",
167
211
  `/api/offers/payment-intent/${paymentIntentId}/download-url`,
168
- params
212
+ params,
213
+ void 0,
214
+ { retrySafe: true }
169
215
  );
170
216
  }
171
217
  async getReceipt(invoiceId) {
172
218
  return this.request("GET", "/api/agent/receipt", void 0, {
173
- invoice_id: String(invoiceId)
219
+ invoice_id: invoiceId
174
220
  });
175
221
  }
176
222
  async getPayerReceipt(invoiceId, preimage) {
@@ -182,10 +228,11 @@ var Hypawave = class {
182
228
  return this.request("GET", "/api/public-settings");
183
229
  }
184
230
  async waitForSettlement(invoiceId, options) {
185
- const interval = options?.pollInterval ?? 2e3;
231
+ const base = options?.pollInterval ?? 2e3;
232
+ const maxInterval = options?.maxPollInterval ?? 2e4;
186
233
  const timeout = options?.timeout ?? 3e5;
187
234
  const start = Date.now();
188
- while (Date.now() - start < timeout) {
235
+ for (let attempt = 0; Date.now() - start < timeout; attempt++) {
189
236
  const status = await this.getUnlockStatus([invoiceId]);
190
237
  const entry = status.statuses?.[String(invoiceId)];
191
238
  if (entry?.unlocked) {
@@ -194,7 +241,9 @@ var Hypawave = class {
194
241
  if (entry?.status === "failed" || entry?.status === "expired") {
195
242
  return entry;
196
243
  }
197
- await new Promise((r) => setTimeout(r, interval));
244
+ const ceiling = Math.min(maxInterval, base * 2 ** attempt);
245
+ const wait = ceiling / 2 + Math.random() * (ceiling / 2);
246
+ await sleep(Math.min(wait, Math.max(0, start + timeout - Date.now())));
198
247
  }
199
248
  throw new HypawaveAPIError(408, {
200
249
  error: "settlement_timeout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypawave/sdk",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript SDK for Lightning settlement, preimage proof, and execution unlocks for AI agents.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,10 +17,14 @@
17
17
  "skills",
18
18
  "AGENTS.md",
19
19
  "README.md",
20
+ "CHANGELOG.md",
20
21
  "LICENSE"
21
22
  ],
22
23
  "scripts": {
23
24
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
24
28
  "prepublishOnly": "npm run build"
25
29
  },
26
30
  "keywords": [
@@ -65,7 +69,8 @@
65
69
  "homepage": "https://hypawave.com/docs",
66
70
  "devDependencies": {
67
71
  "tsup": "^8.0.0",
68
- "typescript": "^5.0.0"
72
+ "typescript": "^5.0.0",
73
+ "vitest": "^3.0.0"
69
74
  },
70
75
  "engines": {
71
76
  "node": ">=18"