@hypawave/sdk 0.2.1 → 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 +30 -0
- package/README.md +22 -10
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +99 -50
- package/dist/index.mjs +99 -50
- package/package.json +7 -2
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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# @hypawave/sdk
|
|
2
2
|
|
|
3
|
-
**Lightning SDK for AI Agent Payments — non-custodial
|
|
3
|
+
**Bitcoin Lightning SDK for AI Agent Payments — non-custodial settlement with preimage-proof unlocks**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@hypawave/sdk)
|
|
6
|
+
[](https://github.com/hypawave/sdk/actions/workflows/ci.yml)
|
|
6
7
|
[](https://github.com/hypawave/sdk/blob/main/LICENSE)
|
|
7
8
|
[](https://nodejs.org)
|
|
8
9
|
|
|
@@ -10,13 +11,19 @@
|
|
|
10
11
|
|
|
11
12
|
> ⚠️ **Hypawave has no token and will never issue one.** Any "HYPA," "WAVE," airdrop, or token offer claiming to be from Hypawave is a scam. Hypawave is non-custodial Bitcoin Lightning settlement — buyers pay creators directly in sats. The protocol fee is the only economic primitive.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Hypawave is programmable settlement infrastructure for AI agents: a non-custodial Bitcoin Lightning protocol where verified payment unlocks API access, files, agent actions, and digital work.
|
|
15
|
+
|
|
16
|
+
This TypeScript SDK lets developers create creator-direct Lightning invoices, deliver payment payloads to payer agents, verify preimage proof, and trigger deterministic unlocks.
|
|
14
17
|
|
|
15
18
|
**Payment is the authorization — confirmed settlement unconditionally unlocks access.**
|
|
16
19
|
|
|
17
20
|
## Why Hypawave
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
Hypawave lets developers charge for digital work with one primitive: verified Bitcoin Lightning settlement unlocks access.
|
|
23
|
+
|
|
24
|
+
Use it when an API, file, webhook, inference job, automation, or agent action should only run after payment. The SDK creates creator-direct Lightning invoices, packages payment instructions for payer agents, verifies preimage proof after payment, and returns the preimage your app gates execution on.
|
|
25
|
+
|
|
26
|
+
The important difference from a normal invoice flow is that payment proof and authorization are the same event. Once the payer submits a valid preimage, Hypawave can release encrypted files, return gated data, or trigger execution without manual reconciliation, payer accounts, or custody.
|
|
20
27
|
|
|
21
28
|
This SDK covers **Path 2**: account-based agent flows. Accountless Paths 3a and 3b use raw HTTP via [llms.txt](https://hypawave.com/llms.txt) and the [OpenAPI spec](https://hypawave.com/.well-known/openapi.json). Requires programmable Lightning infrastructure (LND, CLN, Alby API, LNbits, NWC, etc.) that returns the preimage after payment.
|
|
22
29
|
|
|
@@ -47,6 +54,8 @@ npm install @hypawave/sdk
|
|
|
47
54
|
|
|
48
55
|
## Quick Start
|
|
49
56
|
|
|
57
|
+
This example shows the full settlement loop in one script for clarity. In production, the creator/developer usually creates the invoice, then sends either `payment_url` to a browser payer or `getPaymentPayload()` to a payer agent.
|
|
58
|
+
|
|
50
59
|
```typescript
|
|
51
60
|
import { Hypawave } from "@hypawave/sdk";
|
|
52
61
|
|
|
@@ -59,7 +68,7 @@ const invoice = await pp.createInvoice({
|
|
|
59
68
|
client_last_name: "Smith",
|
|
60
69
|
amount: 5.00,
|
|
61
70
|
due_date: "2026-12-31",
|
|
62
|
-
payment_destination: "creator@getalby.com",
|
|
71
|
+
payment_destination: "creator@getalby.com", // optional; defaults to the API key owner's Lightning Address
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
console.log(invoice.invoice_id); // Use for confirmation + key retrieval
|
|
@@ -148,7 +157,8 @@ Both files point to two authoritative web sources so instructions stay fresh:
|
|
|
148
157
|
|-----------|------|---------|-------------|
|
|
149
158
|
| `apiKey` | `string` | — | API key (`sk_test_*` or `sk_live_*`) |
|
|
150
159
|
| `baseUrl` | `string` | `https://hypawave.com` | API base URL |
|
|
151
|
-
| `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`. |
|
|
152
162
|
|
|
153
163
|
### `createInvoice(params)`
|
|
154
164
|
|
|
@@ -322,12 +332,15 @@ const { downloadUrl } = await pp.getOfferDownloadUrl(paymentIntentId, {
|
|
|
322
332
|
|
|
323
333
|
### `waitForSettlement(invoiceId, options?)`
|
|
324
334
|
|
|
325
|
-
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.
|
|
326
338
|
|
|
327
339
|
```typescript
|
|
328
340
|
const result = await pp.waitForSettlement(invoiceId, {
|
|
329
|
-
pollInterval: 2000,
|
|
330
|
-
|
|
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)
|
|
331
344
|
});
|
|
332
345
|
|
|
333
346
|
if (result.unlocked) {
|
|
@@ -369,11 +382,10 @@ const settings = await pp.getSettings();
|
|
|
369
382
|
Additional methods available — see types for full signatures, or [openapi.json](https://hypawave.com/.well-known/openapi.json) for the complete API reference.
|
|
370
383
|
|
|
371
384
|
- `listInvoices(params?)` — list invoices with filters and pagination
|
|
372
|
-
- `getPayerReceipt(invoiceId,
|
|
385
|
+
- `getPayerReceipt(invoiceId, preimage)` — payer receipt fetch using the Lightning preimage as proof of payment (no API key needed)
|
|
373
386
|
- `getUploadUrl(params)` — signed URL for encrypted file upload (creator side)
|
|
374
387
|
- `storeFile(params)` — register an uploaded file against an invoice
|
|
375
388
|
- `storeFileKey(params)` — register a file's encryption key against an invoice
|
|
376
|
-
- `request(path, options)` — low-level escape hatch for direct API calls
|
|
377
389
|
|
|
378
390
|
## Error Handling
|
|
379
391
|
|
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(
|
|
75
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
152
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
48
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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(
|
|
125
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|