@healchain/sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HealChain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # @healchain/sdk
2
+
3
+ JavaScript SDK for [HealChain](https://healchain.org) — self-healing decentralized storage using Reed-Solomon erasure coding on Ethereum and Arbitrum.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @healchain/sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import HealChain from '@healchain/sdk';
15
+
16
+ const hc = new HealChain({ apiUrl: 'https://api.healchain.org' });
17
+
18
+ // Store data — oracle fulfills automatically
19
+ const { recordId, chainId } = await hc.store('Hello World', {
20
+ label: 'my first record',
21
+ });
22
+ console.log(`Stored as record #${recordId} on chain ${chainId}`);
23
+
24
+ // Retrieve — auto-discovers chain from GlobalRegistry
25
+ const { text, chainId: retrievedFrom } = await hc.retrieve(recordId);
26
+ console.log(`Retrieved from chain ${retrievedFrom}: ${text}`);
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### `new HealChain(options)`
32
+
33
+ | Option | Type | Default | Description |
34
+ |---|---|---|---|
35
+ | `apiUrl` | string | `https://api.healchain.org` | API base URL |
36
+ | `apiKey` | string | — | Optional API key |
37
+ | `network` | string | `'sepolia'` | Default network: `'sepolia'` or `'arbitrum'` |
38
+ | `pollInterval` | number | `4000` | Oracle poll interval in ms |
39
+ | `pollTimeout` | number | `120000` | Max wait for oracle fulfillment in ms |
40
+ | `dataShards` | number | `10` | Default RS data shards |
41
+ | `parityShards` | number | `4` | Default RS parity shards |
42
+
43
+ ---
44
+
45
+ ### `hc.store(data, options?)`
46
+
47
+ Store data on-chain. Polls until the oracle fulfills the request.
48
+
49
+ **Parameters:**
50
+ - `data` — `string` or `Uint8Array`
51
+ - `options.label` — record label (default: `'sdk upload'`)
52
+ - `options.network` — override network for this call
53
+ - `options.dataShards` / `options.parityShards` — override shard config
54
+ - `options.onPending(info)` — callback when tx is submitted
55
+ - `options.onFulfilled(result)` — callback when oracle fulfills
56
+
57
+ **Returns:** `Promise<StoreResult>`
58
+
59
+ ```typescript
60
+ {
61
+ recordId: string // on-chain record ID
62
+ requestId: string // oracle request ID
63
+ tx: string // submission transaction hash
64
+ chainId: string // chain where data is stored
65
+ originalSize: string // original data size in bytes
66
+ encodedSize: string // encoded shard size in bytes
67
+ }
68
+ ```
69
+
70
+ **Example with progress callbacks:**
71
+
72
+ ```javascript
73
+ const result = await hc.store('my data', {
74
+ label: 'important file',
75
+ network: 'arbitrum', // ~75x cheaper than Sepolia
76
+ onPending: ({ requestId }) => {
77
+ console.log(`Submitted, oracle processing requestId=${requestId}...`);
78
+ },
79
+ onFulfilled: ({ recordId }) => {
80
+ console.log(`Fulfilled! Record ID: ${recordId}`);
81
+ },
82
+ });
83
+ ```
84
+
85
+ ---
86
+
87
+ ### `hc.retrieve(id)`
88
+
89
+ Retrieve a record by ID. Automatically queries the GlobalRegistry to find which chain holds the data and fetches from there.
90
+
91
+ **Parameters:**
92
+ - `id` — `string` or `number`
93
+
94
+ **Returns:** `Promise<RetrieveResult>`
95
+
96
+ ```typescript
97
+ {
98
+ recordId: string // record ID
99
+ data: string // hex-encoded original data (0x...)
100
+ text: string // UTF-8 decoded text
101
+ bytes: number // original data size in bytes
102
+ chainId: string // chain where data was stored ('11155111' or '421614')
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ### `hc.getMetadata(id)`
109
+
110
+ Fetch record metadata without retrieving the full data payload.
111
+
112
+ **Returns:** `Promise<object>` — label, owner, sizes, shards, timestamp, dataHash
113
+
114
+ ---
115
+
116
+ ### `hc.list(page?, limit?)`
117
+
118
+ List records with pagination.
119
+
120
+ **Parameters:**
121
+ - `page` — page number, 0-indexed (default: `0`)
122
+ - `limit` — records per page, max 50 (default: `10`)
123
+
124
+ **Returns:**
125
+ ```typescript
126
+ {
127
+ records: object[] // record summaries
128
+ total: number // total record count
129
+ pages: number // total page count
130
+ page: number // current page
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ### `hc.health()`
137
+
138
+ Check service health.
139
+
140
+ **Returns:** `Promise<{ status, version, geth, lastBlock }>`
141
+
142
+ ---
143
+
144
+ ## Error Handling
145
+
146
+ All methods throw `HealChainError` on failure:
147
+
148
+ ```javascript
149
+ import { HealChain, HealChainError } from '@healchain/sdk';
150
+
151
+ try {
152
+ const result = await hc.retrieve(999);
153
+ } catch (err) {
154
+ if (err instanceof HealChainError) {
155
+ console.error(err.message); // human-readable message
156
+ console.error(err.status); // HTTP status code (if applicable)
157
+ console.error(err.code); // 'NETWORK_ERROR' | 'FULFILLMENT_TIMEOUT' | null
158
+ }
159
+ }
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Browser (ESM)
165
+
166
+ ```html
167
+ <script type="module">
168
+ import HealChain from 'https://esm.sh/@healchain/sdk';
169
+
170
+ const hc = new HealChain({ apiUrl: 'https://api.healchain.org' });
171
+ const { text } = await hc.retrieve(0);
172
+ console.log(text);
173
+ </script>
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Networks
179
+
180
+ | Network | Chain ID | Notes |
181
+ |---|---|---|
182
+ | `sepolia` | 11155111 | Ethereum Sepolia testnet |
183
+ | `arbitrum` | 421614 | Arbitrum Sepolia testnet — ~75x cheaper gas |
184
+
185
+ Store on Arbitrum for lower cost. Retrieve works automatically regardless of which chain the data is on — the SDK doesn't need to know.
186
+
187
+ ---
188
+
189
+ ## Roadmap
190
+
191
+ - **v0.1** — REST API client (store, retrieve, list, health) ✅
192
+ - **v0.2** — Direct wallet signing via ethers.js (no service required)
193
+ - **v0.3** — Streaming large file support
194
+ - **v1.0** — Mainnet support
195
+
196
+ ---
197
+
198
+ ## License
199
+
200
+ MIT — see [LICENSE](LICENSE)
201
+
202
+ ---
203
+
204
+ *Built on [HealChain](https://healchain.org) · [GitHub](https://github.com/karmaxul/ci-quantum-storage)*
Binary file
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@healchain/sdk",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript SDK for HealChain — self-healing decentralized storage via Reed-Solomon erasure coding",
5
+ "main": "src/index.js",
6
+ "module": "src/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.js",
10
+ "require": "./src/index.js",
11
+ "default": "./src/index.js"
12
+ }
13
+ },
14
+ "type": "module",
15
+ "files": [
16
+ "src/",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --experimental-vm-modules test/index.test.js"
22
+ },
23
+ "keywords": [
24
+ "healchain",
25
+ "blockchain",
26
+ "storage",
27
+ "reed-solomon",
28
+ "erasure-coding",
29
+ "ethereum",
30
+ "arbitrum",
31
+ "decentralized",
32
+ "self-healing"
33
+ ],
34
+ "author": "HealChain Team",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/karmaxul/ci-quantum-storage"
39
+ },
40
+ "homepage": "https://healchain.org",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ }
44
+ }
package/src/index.js ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * HealChain SDK v0.1.0
3
+ * REST API client for HealChain — self-healing decentralized storage.
4
+ *
5
+ * Browser (ESM) and Node.js compatible.
6
+ *
7
+ * @example
8
+ * const hc = new HealChain({ apiUrl: 'https://api.healchain.org' });
9
+ * const { recordId } = await hc.store('Hello World', { label: 'my record' });
10
+ * const { data } = await hc.retrieve(recordId);
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ // ── Defaults ──────────────────────────────────────────────────────────────────
16
+
17
+ const DEFAULT_API_URL = 'https://api.healchain.org';
18
+ const DEFAULT_POLL_INTERVAL = 4000; // ms between fulfillment polls
19
+ const DEFAULT_POLL_TIMEOUT = 120000; // ms before giving up (2 minutes)
20
+ const DEFAULT_DATA_SHARDS = 10;
21
+ const DEFAULT_PARITY_SHARDS = 4;
22
+
23
+ // ── Helpers ───────────────────────────────────────────────────────────────────
24
+
25
+ function toHex(input) {
26
+ if (typeof input === 'string') {
27
+ // Already hex
28
+ if (input.startsWith('0x')) return input;
29
+ // Plain text → hex
30
+ const bytes = new TextEncoder().encode(input);
31
+ return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
32
+ }
33
+ if (input instanceof Uint8Array || ArrayBuffer.isView(input)) {
34
+ return '0x' + Array.from(new Uint8Array(input.buffer ?? input))
35
+ .map(b => b.toString(16).padStart(2, '0')).join('');
36
+ }
37
+ throw new HealChainError('store() data must be a string or Uint8Array');
38
+ }
39
+
40
+ function sleep(ms) {
41
+ return new Promise(resolve => setTimeout(resolve, ms));
42
+ }
43
+
44
+ // ── Error class ───────────────────────────────────────────────────────────────
45
+
46
+ class HealChainError extends Error {
47
+ constructor(message, { status, code, response } = {}) {
48
+ super(message);
49
+ this.name = 'HealChainError';
50
+ this.status = status ?? null;
51
+ this.code = code ?? null;
52
+ this.response = response ?? null;
53
+ }
54
+ }
55
+
56
+ // ── Main class ────────────────────────────────────────────────────────────────
57
+
58
+ class HealChain {
59
+ /**
60
+ * @param {object} options
61
+ * @param {string} [options.apiUrl='https://api.healchain.org'] - API base URL
62
+ * @param {string} [options.apiKey] - Optional API key
63
+ * @param {number} [options.pollInterval] - Fulfillment poll interval in ms
64
+ * @param {number} [options.pollTimeout] - Fulfillment poll timeout in ms
65
+ * @param {number} [options.dataShards] - Default RS data shards
66
+ * @param {number} [options.parityShards] - Default RS parity shards
67
+ * @param {string} [options.network] - 'sepolia' | 'arbitrum' (for store routing)
68
+ */
69
+ constructor(options = {}) {
70
+ this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '');
71
+ this.apiKey = options.apiKey ?? null;
72
+ this.pollInterval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;
73
+ this.pollTimeout = options.pollTimeout ?? DEFAULT_POLL_TIMEOUT;
74
+ this.dataShards = options.dataShards ?? DEFAULT_DATA_SHARDS;
75
+ this.parityShards = options.parityShards ?? DEFAULT_PARITY_SHARDS;
76
+ this.network = options.network ?? 'sepolia';
77
+ }
78
+
79
+ // ── Internal fetch wrapper ──────────────────────────────────────────────────
80
+
81
+ async _fetch(path, init = {}) {
82
+ const url = `${this.apiUrl}${path}`;
83
+ const headers = { 'Content-Type': 'application/json', ...init.headers };
84
+ if (this.apiKey) headers['X-API-Key'] = this.apiKey;
85
+
86
+ let res;
87
+ try {
88
+ res = await fetch(url, { ...init, headers });
89
+ } catch (err) {
90
+ throw new HealChainError(`Network error: ${err.message}`, { code: 'NETWORK_ERROR' });
91
+ }
92
+
93
+ let body;
94
+ const ct = res.headers.get('content-type') ?? '';
95
+ if (ct.includes('application/json')) {
96
+ body = await res.json();
97
+ } else {
98
+ body = await res.text();
99
+ }
100
+
101
+ if (!res.ok) {
102
+ const msg = (typeof body === 'object' ? body.error : body) ?? res.statusText;
103
+ throw new HealChainError(msg, { status: res.status, response: body });
104
+ }
105
+
106
+ return body;
107
+ }
108
+
109
+ // ── health() ───────────────────────────────────────────────────────────────
110
+
111
+ /**
112
+ * Check service health.
113
+ * @returns {Promise<{status: string, version: string, geth: string, lastBlock: string}>}
114
+ */
115
+ async health() {
116
+ return this._fetch('/health');
117
+ }
118
+
119
+ // ── store() ────────────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Store data on-chain. Polls until the oracle fulfills the request.
123
+ *
124
+ * @param {string|Uint8Array} data - Data to store
125
+ * @param {object} [options]
126
+ * @param {string} [options.label='sdk upload'] - Record label
127
+ * @param {number} [options.dataShards] - Override RS data shards
128
+ * @param {number} [options.parityShards] - Override RS parity shards
129
+ * @param {string} [options.network] - Override network for this call
130
+ * @param {function} [options.onPending] - Called with requestId when submitted
131
+ * @param {function} [options.onFulfilled] - Called when oracle fulfills
132
+ *
133
+ * @returns {Promise<StoreResult>}
134
+ *
135
+ * @typedef {object} StoreResult
136
+ * @property {string} recordId - On-chain record ID
137
+ * @property {string} requestId - Oracle request ID
138
+ * @property {string} tx - Submission transaction hash
139
+ * @property {string} chainId - Chain where data is stored
140
+ * @property {string} originalSize - Original data size in bytes
141
+ * @property {string} encodedSize - Encoded shard size in bytes
142
+ */
143
+ async store(data, options = {}) {
144
+ const hex = toHex(data);
145
+ const label = options.label ?? 'sdk upload';
146
+ const dataShards = options.dataShards ?? this.dataShards;
147
+ const parityShards = options.parityShards ?? this.parityShards;
148
+ const network = options.network ?? this.network;
149
+
150
+ // Submit store request
151
+ const submitted = await this._fetch('/storeOnChain', {
152
+ method: 'POST',
153
+ body: JSON.stringify({ data: hex, label, network, dataShards, parityShards }),
154
+ });
155
+
156
+ // Devnet returns synchronously
157
+ if (submitted.status === 'success' && submitted.recordId !== undefined) {
158
+ return {
159
+ recordId: String(submitted.recordId),
160
+ requestId: submitted.requestId ?? null,
161
+ tx: submitted.tx,
162
+ chainId: submitted.chainId ?? null,
163
+ originalSize: submitted.originalSize,
164
+ encodedSize: submitted.encodedSize,
165
+ };
166
+ }
167
+
168
+ // Sepolia/Arbitrum: poll for oracle fulfillment
169
+ const { requestId, tx } = submitted;
170
+ if (!requestId) {
171
+ throw new HealChainError('No requestId returned from store', { response: submitted });
172
+ }
173
+
174
+ if (typeof options.onPending === 'function') {
175
+ options.onPending({ requestId, tx });
176
+ }
177
+
178
+ return this._pollFulfillment(requestId, tx, options.onFulfilled);
179
+ }
180
+
181
+ // ── _pollFulfillment() ─────────────────────────────────────────────────────
182
+
183
+ async _pollFulfillment(requestId, tx, onFulfilled) {
184
+ const deadline = Date.now() + this.pollTimeout;
185
+
186
+ while (Date.now() < deadline) {
187
+ await sleep(this.pollInterval);
188
+
189
+ let status;
190
+ try {
191
+ status = await this._fetch(`/sepoliaStatus?requestId=${encodeURIComponent(requestId)}`);
192
+ } catch {
193
+ // Transient error — keep polling
194
+ continue;
195
+ }
196
+
197
+ if (status.status === 'fulfilled') {
198
+ const result = {
199
+ recordId: String(status.recordId ?? requestId),
200
+ requestId: String(requestId),
201
+ tx,
202
+ chainId: status.chainId ?? null,
203
+ originalSize: status.originalSize ?? null,
204
+ encodedSize: status.encodedSize ?? null,
205
+ totalRecords: status.totalRecords ?? null,
206
+ };
207
+ if (typeof onFulfilled === 'function') onFulfilled(result);
208
+ return result;
209
+ }
210
+ }
211
+
212
+ throw new HealChainError(
213
+ `Oracle fulfillment timed out after ${this.pollTimeout / 1000}s (requestId=${requestId})`,
214
+ { code: 'FULFILLMENT_TIMEOUT' }
215
+ );
216
+ }
217
+
218
+ // ── retrieve() ─────────────────────────────────────────────────────────────
219
+
220
+ /**
221
+ * Retrieve a record by ID. Automatically routes to the correct chain
222
+ * via the GlobalRegistry.
223
+ *
224
+ * @param {string|number} id - Record ID
225
+ * @returns {Promise<RetrieveResult>}
226
+ *
227
+ * @typedef {object} RetrieveResult
228
+ * @property {string} recordId - Record ID
229
+ * @property {string} data - Hex-encoded original data
230
+ * @property {string} text - UTF-8 decoded text (if valid)
231
+ * @property {number} bytes - Original data size in bytes
232
+ * @property {string} chainId - Chain where data was stored
233
+ */
234
+ async retrieve(id) {
235
+ const result = await this._fetch(`/retrieve?id=${encodeURIComponent(String(id))}`);
236
+ return {
237
+ recordId: String(result.recordId),
238
+ data: result.data,
239
+ text: result.text,
240
+ bytes: result.bytes,
241
+ chainId: result.chainId ?? null,
242
+ };
243
+ }
244
+
245
+ // ── getMetadata() ──────────────────────────────────────────────────────────
246
+
247
+ /**
248
+ * Get record metadata without fetching the full data.
249
+ *
250
+ * @param {string|number} id - Record ID
251
+ * @returns {Promise<object>} Record metadata
252
+ */
253
+ async getMetadata(id) {
254
+ return this._fetch(`/getMetadata?id=${encodeURIComponent(String(id))}`);
255
+ }
256
+
257
+ // ── list() ─────────────────────────────────────────────────────────────────
258
+
259
+ /**
260
+ * List records with pagination.
261
+ *
262
+ * @param {number} [page=0] - Page number (0-indexed)
263
+ * @param {number} [limit=10] - Records per page (max 50)
264
+ * @returns {Promise<ListResult>}
265
+ *
266
+ * @typedef {object} ListResult
267
+ * @property {object[]} records - Array of record summaries
268
+ * @property {number} total - Total record count
269
+ * @property {number} pages - Total page count
270
+ * @property {number} page - Current page
271
+ */
272
+ async list(page = 0, limit = 10) {
273
+ return this._fetch(`/listRecords?page=${page}&limit=${limit}`);
274
+ }
275
+ }
276
+
277
+ // ── Exports ───────────────────────────────────────────────────────────────────
278
+
279
+ export { HealChain, HealChainError };
280
+ export default HealChain;
281
+
282
+ // CommonJS compatibility
283
+ if (typeof module !== 'undefined') {
284
+ module.exports = HealChain;
285
+ module.exports.HealChain = HealChain;
286
+ module.exports.HealChainError = HealChainError;
287
+ module.exports.default = HealChain;
288
+ }
Binary file