@sage-protocol/sdk 0.1.6 → 0.1.7

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).
@@ -20,7 +20,7 @@ var require_package = __commonJS({
20
20
  "package.json"(exports, module) {
21
21
  module.exports = {
22
22
  name: "@sage-protocol/sdk",
23
- version: "0.1.6",
23
+ version: "0.1.4",
24
24
  description: "Backend-agnostic SDK for interacting with the Sage Protocol (governance, SubDAOs, tokens).",
25
25
  main: "dist/index.cjs",
26
26
  module: "dist/index.mjs",
@@ -56,10 +56,10 @@ var require_package = __commonJS({
56
56
  ],
57
57
  sideEffects: false,
58
58
  browser: {
59
+ child_process: false,
59
60
  fs: false,
60
- path: false,
61
61
  os: false,
62
- child_process: false
62
+ path: false
63
63
  },
64
64
  repository: {
65
65
  type: "git",
@@ -88,6 +88,18 @@ var require_package = __commonJS({
88
88
  sinon: "^17.0.1",
89
89
  tsup: "^8.1.0",
90
90
  typescript: "^5.4.0"
91
+ },
92
+ peerDependencies: {
93
+ react: "^18.0.0 || ^19.0.0",
94
+ swr: "^2.0.0"
95
+ },
96
+ peerDependenciesMeta: {
97
+ react: {
98
+ optional: true
99
+ },
100
+ swr: {
101
+ optional: true
102
+ }
91
103
  }
92
104
  };
93
105
  }
@@ -250,19 +262,20 @@ var require_abi = __commonJS({
250
262
  var PersonalLicenseReceipt = [
251
263
  "function balanceOf(address account, uint256 id) view returns (uint256)"
252
264
  ];
253
- var TreasuryWrapper = [
254
- "function execute(address,uint256,bytes,bytes32) returns (bool)",
255
- "function allowedTargets(address) view returns (bool)",
256
- "function allowedSelectors(bytes4) view returns (bool)",
257
- "function owners(address) view returns (bool)",
258
- "function ownerCount() view returns (uint256)",
259
- "function registry() view returns (address)",
260
- "event TreasuryAction(address indexed caller, address indexed target, uint256 value, bytes data, bytes32 refId)",
261
- "event AllowedTargetUpdated(address indexed target, bool allowed)",
262
- "event AllowedSelectorUpdated(bytes4 indexed selector, bool allowed)",
263
- "event OwnerAdded(address indexed owner)",
264
- "event OwnerRemoved(address indexed owner)",
265
- "event TokensSwept(address indexed token, address indexed to, uint256 amount)"
265
+ var SageTreasury = [
266
+ "function totalReserves() view returns (uint256)",
267
+ "function totalPOL() view returns (uint256)",
268
+ "function totalDebt() view returns (uint256)",
269
+ "function canonicalPool() view returns (address)",
270
+ "function routerOrVault() view returns (address)",
271
+ "function maxWithdrawalRate() view returns (uint256)",
272
+ "function emergencyWithdrawalLimit() view returns (uint256)",
273
+ "function getReserveTokens() view returns (address[])",
274
+ "function getReserve(address token) view returns (address tokenAddress,uint256 amount,uint256 value,bool isLP,bool isActive)",
275
+ "function pendingWithdrawals(uint256) view returns (address token,address recipient,uint256 amount,uint256 value,address requester,uint256 balanceBefore,uint256 recipientBalanceBefore,uint256 depositSnapshot,bool isLP,bool isEmergency,bool exists)",
276
+ "function nextWithdrawalId() view returns (uint256)",
277
+ "function manualPrices(address token) view returns (uint256 price,uint256 expiresAt,bool active)",
278
+ "function lpContributions(address,address) view returns (uint256)"
266
279
  ];
267
280
  var GovernanceBoostMerkle = [
268
281
  "function getProposalConfig(uint256) view returns (tuple(uint256 proposalId,address token,uint256 totalAmount,uint64 startTime,uint64 endTime,uint256 merkleRoot))",
@@ -274,6 +287,29 @@ var require_abi = __commonJS({
274
287
  "function create(uint256 proposalId, address token, uint256 perVoter, uint256 maxVoters)",
275
288
  "function fund(uint256 proposalId, uint256 amount)"
276
289
  ];
290
+ var BondDepository = [
291
+ // Core getters
292
+ "function payoutToken() view returns (address)",
293
+ "function principalToken() view returns (address)",
294
+ "function treasury() view returns (address)",
295
+ // Terms
296
+ "function terms() view returns (tuple(uint256 controlVariable,uint256 minimumPrice,uint256 maxPayout,uint256 maxDebt,uint256 vestingTerm,uint256 fee))",
297
+ "function totalDebt(address) view returns (uint256)",
298
+ // Pricing
299
+ "function bondPrice() view returns (uint256)",
300
+ "function bondPrice(address) view returns (uint256)",
301
+ "function bondPriceInUSD() view returns (uint256)",
302
+ "function currentDebt() view returns (uint256)",
303
+ "function debtRatio() view returns (uint256)",
304
+ "function standardizedDebtRatio() view returns (uint256)",
305
+ // User views
306
+ "function bondInfo(address) view returns (tuple(uint256 payout,uint256 vesting,uint256 lastBlock,uint256 pricePaid))",
307
+ "function pendingPayout(address) view returns (uint256)",
308
+ "function percentVestedFor(address) view returns (uint256)",
309
+ // Actions
310
+ "function deposit(uint256 _amount, uint256 _maxPrice) returns (uint256 payout_)",
311
+ "function redeem(address _recipient, bool _stake) returns (uint256)"
312
+ ];
277
313
  var Events = {
278
314
  ProposalCreated: "event ProposalCreated(uint256 id, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock, string description)"
279
315
  };
@@ -292,10 +328,10 @@ var require_abi = __commonJS({
292
328
  PersonalLibraryFacet,
293
329
  PersonalMarketplace,
294
330
  PersonalLicenseReceipt,
295
- TreasuryWrapper,
296
- // Protocol treasury (replaces SageTreasury)
331
+ SageTreasury,
297
332
  GovernanceBoostMerkle,
298
333
  GovernanceBoostDirect,
334
+ BondDepository,
299
335
  Events
300
336
  };
301
337
  }