@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 +21 -0
- package/README.md +204 -0
- package/README.md:Zone.Identifier +0 -0
- package/package.json +44 -0
- package/src/index.js +288 -0
- package/src/index.js:Zone.Identifier +0 -0
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
|