@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 +288 -0
- package/dist/browser/index.mjs +462 -5134
- package/dist/index.cjs +983 -3
- package/dist/index.mjs +983 -3
- package/dist/node/index.cjs +983 -3
- package/dist/node/index.mjs +983 -3
- package/package.json +15 -3
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).
|