@sage-protocol/sdk 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,9 +68,297 @@ Module Overview
68
68
  | `treasury` | Reserve/POL snapshot, pending withdrawals, liquidity plans | Treasury analytics, Safe operators |
69
69
  | `boost` | Merkle/Direct boost readers + builders | Incentive distribution tooling |
70
70
  | `subgraph` | GraphQL helpers for proposals/libraries | Historical analytics |
71
+ | `services` | High-level SubgraphService + IPFSService with retry/caching | Web app, CLI integration |
71
72
  | `adapters` | Normalized governance adapters (OZ) | UI layers needing a unified model |
72
73
  | `errors` | `SageSDKError` codes | Consistent downstream error handling |
73
74
 
75
+ ### Service Layer
76
+
77
+ The SDK provides high-level service classes with built-in retry logic, caching, and error handling for production applications.
78
+
79
+ **SubgraphService** - GraphQL queries with automatic retry and caching
80
+
81
+ ```js
82
+ import { services, serviceErrors } from '@sage-protocol/sdk';
83
+
84
+ // Initialize service
85
+ const subgraphService = new services.SubgraphService({
86
+ url: 'https://api.studio.thegraph.com/query/your-subgraph',
87
+ timeout: 10000, // 10s timeout (default)
88
+ retries: 3, // 3 retry attempts with exponential backoff (default)
89
+ cache: {
90
+ enabled: true, // Enable caching (default: true)
91
+ ttl: 30000, // 30s cache TTL (default)
92
+ maxSize: 100, // Max 100 cache entries (default)
93
+ },
94
+ });
95
+
96
+ // Fetch SubDAOs with caching
97
+ const subdaos = await subgraphService.getSubDAOs({ limit: 50, skip: 0 });
98
+
99
+ // Fetch proposals with filters
100
+ const proposals = await subgraphService.getProposals({
101
+ governor: '0xGovernor',
102
+ states: ['ACTIVE', 'PENDING'],
103
+ fromTimestamp: 1640000000,
104
+ limit: 20,
105
+ cache: true, // Use cache (default: true)
106
+ });
107
+
108
+ // Get single proposal by ID
109
+ const proposal = await subgraphService.getProposalById('0x123...', { cache: true });
110
+
111
+ // Fetch libraries
112
+ const libraries = await subgraphService.getLibraries({
113
+ subdao: '0xSubDAO',
114
+ limit: 50,
115
+ });
116
+
117
+ // Fetch prompts by tag
118
+ const prompts = await subgraphService.getPromptsByTag({
119
+ tagsHash: '0xabc123...',
120
+ registry: '0xRegistry',
121
+ limit: 50,
122
+ });
123
+
124
+ // Cache management
125
+ subgraphService.clearCache();
126
+ const stats = subgraphService.getCacheStats();
127
+ // → { enabled: true, size: 12, maxSize: 100 }
128
+ ```
129
+
130
+ **IPFSService** - Parallel gateway fetching with caching
131
+
132
+ ```js
133
+ import { services, serviceErrors } from '@sage-protocol/sdk';
134
+ import { ethers } from 'ethers';
135
+
136
+ // Initialize service
137
+ const ipfsService = new services.IPFSService({
138
+ workerBaseUrl: 'https://api.sageprotocol.io',
139
+ gateway: 'https://ipfs.io',
140
+ signer: ethers.Wallet.fromPhrase('...'), // Optional: for worker auth
141
+ timeout: 15000, // 15s timeout (default)
142
+ retries: 2, // 2 retry attempts (default)
143
+ cache: {
144
+ enabled: true, // Enable caching (default: true)
145
+ ttl: 300000, // 5min cache TTL for immutable CIDs (default)
146
+ maxSize: 50, // Max 50 cache entries (default)
147
+ },
148
+ });
149
+
150
+ // Fetch content by CID (parallel gateway race)
151
+ // Tries multiple gateways in parallel, returns first success
152
+ const content = await ipfsService.fetchByCID('QmTest123...', {
153
+ cache: true,
154
+ timeout: 5000, // 5s timeout per gateway
155
+ extraGateways: ['https://cloudflare-ipfs.com', 'https://gateway.pinata.cloud'],
156
+ });
157
+
158
+ // Upload content to IPFS worker
159
+ const cid = await ipfsService.upload(
160
+ { title: 'My Prompt', content: '...' },
161
+ { name: 'prompt.json', warm: true }
162
+ );
163
+ // → 'QmNewContent123...'
164
+
165
+ // Pin CIDs to worker
166
+ await ipfsService.pin(['QmTest1...', 'QmTest2...'], { warm: false });
167
+
168
+ // Warm gateways (prefetch)
169
+ await ipfsService.warm('QmTest123...', {
170
+ gateways: ['https://ipfs.io', 'https://cloudflare-ipfs.com'],
171
+ });
172
+
173
+ // Cache management
174
+ ipfsService.clearCache();
175
+ const stats = ipfsService.getCacheStats();
176
+ ```
177
+
178
+ **Error Handling**
179
+
180
+ ```js
181
+ import { services, serviceErrors } from '@sage-protocol/sdk';
182
+
183
+ try {
184
+ const subdaos = await subgraphService.getSubDAOs({ limit: 50 });
185
+ } catch (error) {
186
+ if (error instanceof serviceErrors.SubgraphError) {
187
+ console.error(`Subgraph error [${error.code}]:`, error.message);
188
+ console.log('Retryable:', error.retryable);
189
+ // Codes: TIMEOUT, NETWORK, INVALID_RESPONSE, NOT_FOUND, QUERY_FAILED
190
+ }
191
+ }
192
+
193
+ try {
194
+ const content = await ipfsService.fetchByCID('QmTest...');
195
+ } catch (error) {
196
+ if (error instanceof serviceErrors.IPFSError) {
197
+ console.error(`IPFS error [${error.code}]:`, error.message);
198
+ // Codes: TIMEOUT, PIN_FAILED, INVALID_CID, NOT_FOUND, GATEWAY_FAILED, UPLOAD_FAILED
199
+ }
200
+ }
201
+
202
+ // Format user-friendly error messages
203
+ const friendlyMsg = serviceErrors.formatErrorMessage(error);
204
+ ```
205
+
206
+ **Utility Exports**
207
+
208
+ ```js
209
+ import { serviceUtils } from '@sage-protocol/sdk';
210
+
211
+ // Simple in-memory cache with TTL
212
+ const cache = new serviceUtils.SimpleCache({
213
+ enabled: true,
214
+ ttl: 60000, // 1 minute
215
+ maxSize: 200,
216
+ });
217
+ cache.set('key', { data: 'value' });
218
+ const val = cache.get('key');
219
+
220
+ // Exponential backoff retry
221
+ const result = await serviceUtils.retryWithBackoff(
222
+ async () => {
223
+ // Your async operation
224
+ return await fetchData();
225
+ },
226
+ {
227
+ attempts: 3,
228
+ baseDelay: 1000, // Start at 1s
229
+ maxDelay: 10000, // Cap at 10s
230
+ onRetry: ({ attempt, totalAttempts, delay, error }) => {
231
+ console.log(`Retry ${attempt}/${totalAttempts} after ${delay}ms:`, error.message);
232
+ },
233
+ }
234
+ );
235
+ ```
236
+
237
+ **Performance Notes**
238
+
239
+ - **Parallel Gateway Fetching**: IPFSService fetches from multiple IPFS gateways simultaneously (Promise.race pattern), reducing typical fetch times from 7-28s to 1-2s (10-14x improvement)
240
+ - **In-Memory Caching**: Both services cache results in memory with configurable TTL to reduce redundant network calls
241
+ - **Exponential Backoff**: Automatic retry with exponential backoff (1s → 2s → 4s delays) for transient failures
242
+ - **Edge Runtime Compatible**: Uses `fetch()` instead of axios, compatible with Cloudflare Pages and Vercel Edge
243
+
244
+ **React Integration**
245
+
246
+ The SDK provides React hooks built on SWR for seamless integration in React applications:
247
+
248
+ **Prerequisites**:
249
+ ```bash
250
+ # Install peer dependencies
251
+ npm install react swr
252
+ # or
253
+ yarn add react swr
254
+ ```
255
+
256
+ **Usage**:
257
+
258
+ ```js
259
+ import { services, hooks } from '@sage-protocol/sdk';
260
+
261
+ // Initialize services (once, typically in a context provider)
262
+ const subgraphService = new services.SubgraphService({
263
+ url: 'https://api.studio.thegraph.com/query/your-subgraph',
264
+ });
265
+
266
+ const ipfsService = new services.IPFSService({
267
+ workerBaseUrl: 'https://api.sageprotocol.io',
268
+ gateway: 'https://ipfs.io',
269
+ signer: yourSigner, // Optional for uploads
270
+ });
271
+
272
+ // Use hooks in components
273
+ function SubDAOList() {
274
+ const { data: subdaos, error, isLoading } = hooks.useSubDAOs(subgraphService, {
275
+ limit: 50,
276
+ });
277
+
278
+ if (isLoading) return <div>Loading...</div>;
279
+ if (error) return <div>Error: {error.message}</div>;
280
+
281
+ return (
282
+ <ul>
283
+ {subdaos.map(subdao => (
284
+ <li key={subdao.id}>{subdao.name}</li>
285
+ ))}
286
+ </ul>
287
+ );
288
+ }
289
+
290
+ function ProposalList({ governorAddress }) {
291
+ const { data: proposals } = hooks.useProposals(subgraphService, {
292
+ governor: governorAddress,
293
+ states: ['ACTIVE', 'PENDING'],
294
+ limit: 20,
295
+ refreshInterval: 30000, // Auto-refresh every 30s
296
+ });
297
+
298
+ return (
299
+ <ul>
300
+ {proposals?.map(proposal => (
301
+ <li key={proposal.id}>{proposal.description}</li>
302
+ ))}
303
+ </ul>
304
+ );
305
+ }
306
+
307
+ function PromptViewer({ cid }) {
308
+ const { data: content, isLoading } = hooks.useFetchCID(ipfsService, cid, {
309
+ extraGateways: ['https://cloudflare-ipfs.com'],
310
+ });
311
+
312
+ if (isLoading) return <div>Loading prompt...</div>;
313
+
314
+ return <pre>{JSON.stringify(content, null, 2)}</pre>;
315
+ }
316
+
317
+ function UploadForm() {
318
+ const { upload, isUploading, error, data: cid } = hooks.useUpload(ipfsService);
319
+
320
+ const handleSubmit = async (e) => {
321
+ e.preventDefault();
322
+ const content = { title: 'My Prompt', content: '...' };
323
+ try {
324
+ const newCid = await upload(content, { name: 'prompt.json' });
325
+ console.log('Uploaded:', newCid);
326
+ } catch (err) {
327
+ console.error('Upload failed:', err);
328
+ }
329
+ };
330
+
331
+ return (
332
+ <form onSubmit={handleSubmit}>
333
+ <button type="submit" disabled={isUploading}>
334
+ {isUploading ? 'Uploading...' : 'Upload'}
335
+ </button>
336
+ {error && <div>Error: {error.message}</div>}
337
+ {cid && <div>Uploaded to: {cid}</div>}
338
+ </form>
339
+ );
340
+ }
341
+ ```
342
+
343
+ **Available Hooks**:
344
+
345
+ | Hook | Description | Returns |
346
+ |------|-------------|---------|
347
+ | `useSubDAOs(service, options)` | Fetch SubDAOs from subgraph | `{ data, error, isLoading, mutate }` |
348
+ | `useProposals(service, options)` | Fetch proposals from subgraph | `{ data, error, isLoading, mutate }` |
349
+ | `useFetchCID(service, cid, options)` | Fetch IPFS content by CID | `{ data, error, isLoading, mutate }` |
350
+ | `useUpload(service)` | Upload content to IPFS | `{ upload, isUploading, error, data, reset }` |
351
+
352
+ **Hook Options**:
353
+
354
+ All data-fetching hooks support SWR options:
355
+ - `refreshInterval` - Auto-refresh interval in ms
356
+ - `revalidateOnFocus` - Revalidate when window focused
357
+ - `revalidateOnReconnect` - Revalidate on network reconnect
358
+ - `cache` - Use service-level caching
359
+
360
+ **Note**: Hooks are optional and only available when `react` and `swr` peer dependencies are installed.
361
+
74
362
  ### Examples
75
363
 
76
364
  - [Legacy base mini app hook](examples/base-mini-app/README.md) – React-friendly context mirroring the former in-repo app (now maintained externally).