@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.
@@ -20,7 +20,7 @@ var require_package = __commonJS({
20
20
  "package.json"(exports2, module2) {
21
21
  module2.exports = {
22
22
  name: "@sage-protocol/sdk",
23
- version: "0.1.6",
23
+ version: "0.1.7",
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
  }
@@ -8093,6 +8105,952 @@ var require_doppler = __commonJS({
8093
8105
  }
8094
8106
  });
8095
8107
 
8108
+ // src/services/utils/cache.js
8109
+ var require_cache = __commonJS({
8110
+ "src/services/utils/cache.js"(exports2, module2) {
8111
+ var SimpleCache = class {
8112
+ constructor(options = {}) {
8113
+ this.enabled = options.enabled !== false;
8114
+ this.defaultTTL = options.ttl || 3e4;
8115
+ this.maxSize = options.maxSize || 100;
8116
+ this._store = /* @__PURE__ */ new Map();
8117
+ }
8118
+ /**
8119
+ * Get value from cache
8120
+ * @param {string} key - Cache key
8121
+ * @returns {any|null} - Cached value or null if expired/missing
8122
+ */
8123
+ get(key) {
8124
+ if (!this.enabled) return null;
8125
+ const entry = this._store.get(key);
8126
+ if (!entry) return null;
8127
+ if (Date.now() > entry.expires) {
8128
+ this._store.delete(key);
8129
+ return null;
8130
+ }
8131
+ return entry.data;
8132
+ }
8133
+ /**
8134
+ * Set value in cache with TTL
8135
+ * @param {string} key - Cache key
8136
+ * @param {any} data - Data to cache
8137
+ * @param {number} ttl - Time to live in milliseconds (optional)
8138
+ */
8139
+ set(key, data, ttl) {
8140
+ if (!this.enabled) return;
8141
+ if (this._store.size >= this.maxSize && !this._store.has(key)) {
8142
+ const firstKey = this._store.keys().next().value;
8143
+ this._store.delete(firstKey);
8144
+ }
8145
+ this._store.set(key, {
8146
+ data,
8147
+ expires: Date.now() + (ttl || this.defaultTTL)
8148
+ });
8149
+ }
8150
+ /**
8151
+ * Clear all cached entries
8152
+ */
8153
+ clear() {
8154
+ this._store.clear();
8155
+ }
8156
+ /**
8157
+ * Delete specific key
8158
+ * @param {string} key - Cache key to delete
8159
+ */
8160
+ delete(key) {
8161
+ this._store.delete(key);
8162
+ }
8163
+ /**
8164
+ * Get cache statistics
8165
+ * @returns {object} - Cache stats (size, enabled)
8166
+ */
8167
+ stats() {
8168
+ return {
8169
+ size: this._store.size,
8170
+ maxSize: this.maxSize,
8171
+ enabled: this.enabled
8172
+ };
8173
+ }
8174
+ };
8175
+ module2.exports = { SimpleCache };
8176
+ }
8177
+ });
8178
+
8179
+ // src/services/utils/retry.js
8180
+ var require_retry = __commonJS({
8181
+ "src/services/utils/retry.js"(exports2, module2) {
8182
+ async function retryWithBackoff(fn, options = {}) {
8183
+ const {
8184
+ attempts = 3,
8185
+ baseDelay = 1e3,
8186
+ maxDelay = 1e4,
8187
+ onRetry = null
8188
+ } = options;
8189
+ let lastError;
8190
+ for (let i = 0; i < attempts; i++) {
8191
+ try {
8192
+ return await fn();
8193
+ } catch (error) {
8194
+ lastError = error;
8195
+ if (i === attempts - 1) {
8196
+ break;
8197
+ }
8198
+ const delay = Math.min(baseDelay * Math.pow(2, i), maxDelay);
8199
+ if (onRetry) {
8200
+ onRetry({
8201
+ attempt: i + 1,
8202
+ totalAttempts: attempts,
8203
+ delay,
8204
+ error
8205
+ });
8206
+ }
8207
+ await sleep(delay);
8208
+ }
8209
+ }
8210
+ throw lastError;
8211
+ }
8212
+ function sleep(ms) {
8213
+ return new Promise((resolve) => setTimeout(resolve, ms));
8214
+ }
8215
+ module2.exports = { retryWithBackoff, sleep };
8216
+ }
8217
+ });
8218
+
8219
+ // src/errors/index.js
8220
+ var require_errors2 = __commonJS({
8221
+ "src/errors/index.js"(exports2, module2) {
8222
+ var SageSDKError = class extends Error {
8223
+ constructor(message, code, retryable = false, originalError = null) {
8224
+ super(message);
8225
+ this.name = this.constructor.name;
8226
+ this.code = code;
8227
+ this.retryable = retryable;
8228
+ this.originalError = originalError;
8229
+ }
8230
+ };
8231
+ var SubgraphError = class extends SageSDKError {
8232
+ /**
8233
+ * @param {string} message - Error message
8234
+ * @param {'TIMEOUT'|'NETWORK'|'INVALID_RESPONSE'|'NOT_FOUND'|'QUERY_FAILED'} code - Error code
8235
+ * @param {boolean} retryable - Whether error is retryable
8236
+ * @param {Error} originalError - Original error (optional)
8237
+ */
8238
+ constructor(message, code, retryable = false, originalError = null) {
8239
+ super(message, code, retryable, originalError);
8240
+ }
8241
+ };
8242
+ var IPFSError = class extends SageSDKError {
8243
+ /**
8244
+ * @param {string} message - Error message
8245
+ * @param {'TIMEOUT'|'PIN_FAILED'|'INVALID_CID'|'NOT_FOUND'|'GATEWAY_FAILED'|'UPLOAD_FAILED'} code - Error code
8246
+ * @param {boolean} retryable - Whether error is retryable
8247
+ * @param {Error} originalError - Original error (optional)
8248
+ */
8249
+ constructor(message, code, retryable = false, originalError = null) {
8250
+ super(message, code, retryable, originalError);
8251
+ }
8252
+ };
8253
+ function formatErrorMessage(error) {
8254
+ if (!error) return "An unknown error occurred";
8255
+ if (error instanceof SubgraphError || error instanceof IPFSError) {
8256
+ switch (error.code) {
8257
+ case "TIMEOUT":
8258
+ return "Request timed out. Please try again.";
8259
+ case "NETWORK":
8260
+ return "Network error. Check your connection and try again.";
8261
+ case "INVALID_RESPONSE":
8262
+ return "Received invalid data from server.";
8263
+ case "NOT_FOUND":
8264
+ return "Content not found.";
8265
+ case "PIN_FAILED":
8266
+ return "Failed to pin content to IPFS. Please try again.";
8267
+ case "INVALID_CID":
8268
+ return "Invalid content identifier (CID).";
8269
+ case "GATEWAY_FAILED":
8270
+ return "All IPFS gateways failed. Content may be temporarily unavailable.";
8271
+ case "UPLOAD_FAILED":
8272
+ return "Failed to upload content. Please try again.";
8273
+ case "QUERY_FAILED":
8274
+ return "Failed to query data. Please try again.";
8275
+ default:
8276
+ return error.message || "An error occurred";
8277
+ }
8278
+ }
8279
+ return error.message || "An error occurred";
8280
+ }
8281
+ function isRetryable(error) {
8282
+ if (error instanceof SageSDKError) {
8283
+ return error.retryable;
8284
+ }
8285
+ if (error.name === "AbortError" || error.name === "TimeoutError") {
8286
+ return true;
8287
+ }
8288
+ const message = String(error.message || "").toLowerCase();
8289
+ const retryablePatterns = [
8290
+ "timeout",
8291
+ "network",
8292
+ "econnrefused",
8293
+ "econnreset",
8294
+ "etimedout",
8295
+ "fetch failed"
8296
+ ];
8297
+ return retryablePatterns.some((pattern) => message.includes(pattern));
8298
+ }
8299
+ module2.exports = {
8300
+ SageSDKError,
8301
+ SubgraphError,
8302
+ IPFSError,
8303
+ formatErrorMessage,
8304
+ isRetryable
8305
+ };
8306
+ }
8307
+ });
8308
+
8309
+ // src/services/subgraph/client.js
8310
+ var require_client = __commonJS({
8311
+ "src/services/subgraph/client.js"(exports2, module2) {
8312
+ var subgraph = require_subgraph();
8313
+ var { SimpleCache } = require_cache();
8314
+ var { retryWithBackoff } = require_retry();
8315
+ var { SubgraphError } = require_errors2();
8316
+ var SubgraphService = class {
8317
+ /**
8318
+ * @param {object} config - Service configuration
8319
+ * @param {string} config.url - Subgraph GraphQL endpoint
8320
+ * @param {number} [config.timeout=10000] - Request timeout in ms
8321
+ * @param {number} [config.retries=3] - Number of retry attempts
8322
+ * @param {object} [config.cache] - Cache configuration
8323
+ * @param {boolean} [config.cache.enabled=true] - Enable caching
8324
+ * @param {number} [config.cache.ttl=30000] - Cache TTL in ms
8325
+ * @param {number} [config.cache.maxSize=100] - Max cache entries
8326
+ */
8327
+ constructor(config) {
8328
+ if (!config || !config.url) {
8329
+ throw new Error("SubgraphService requires a url in config");
8330
+ }
8331
+ this.url = config.url;
8332
+ this.timeout = config.timeout || 1e4;
8333
+ this.retries = config.retries || 3;
8334
+ this._cache = new SimpleCache({
8335
+ enabled: config.cache?.enabled !== false,
8336
+ ttl: config.cache?.ttl || 3e4,
8337
+ maxSize: config.cache?.maxSize || 100
8338
+ });
8339
+ }
8340
+ /**
8341
+ * Get list of SubDAOs
8342
+ * @param {object} [options] - Query options
8343
+ * @param {number} [options.limit=50] - Max results
8344
+ * @param {number} [options.skip=0] - Results to skip
8345
+ * @param {boolean} [options.cache=true] - Use cache
8346
+ * @returns {Promise<Array>} - List of SubDAOs
8347
+ */
8348
+ async getSubDAOs(options = {}) {
8349
+ const { limit = 50, skip = 0, cache = true } = options;
8350
+ const cacheKey = `subdaos:${limit}:${skip}`;
8351
+ if (cache) {
8352
+ const cached = this._cache.get(cacheKey);
8353
+ if (cached) return cached;
8354
+ }
8355
+ try {
8356
+ const result = await retryWithBackoff(
8357
+ () => this._querySubDAOs({ limit, skip }),
8358
+ { attempts: this.retries }
8359
+ );
8360
+ if (cache) {
8361
+ this._cache.set(cacheKey, result);
8362
+ }
8363
+ return result;
8364
+ } catch (error) {
8365
+ throw new SubgraphError(
8366
+ `Failed to fetch SubDAOs: ${error.message}`,
8367
+ "QUERY_FAILED",
8368
+ true,
8369
+ error
8370
+ );
8371
+ }
8372
+ }
8373
+ /**
8374
+ * Get list of proposals
8375
+ * @param {object} [options] - Query options
8376
+ * @param {string} [options.governor] - Filter by governor address
8377
+ * @param {string} [options.subdao] - Filter by SubDAO address
8378
+ * @param {string[]} [options.states] - Filter by proposal states
8379
+ * @param {number} [options.fromTimestamp] - Filter by creation time (>=)
8380
+ * @param {number} [options.toTimestamp] - Filter by creation time (<=)
8381
+ * @param {number} [options.limit=20] - Max results
8382
+ * @param {number} [options.skip=0] - Results to skip
8383
+ * @param {boolean} [options.cache=true] - Use cache
8384
+ * @returns {Promise<Array>} - List of proposals
8385
+ */
8386
+ async getProposals(options = {}) {
8387
+ const {
8388
+ governor,
8389
+ subdao,
8390
+ states,
8391
+ fromTimestamp,
8392
+ toTimestamp,
8393
+ limit = 20,
8394
+ skip = 0,
8395
+ cache = true
8396
+ } = options;
8397
+ const cacheKey = `proposals:${JSON.stringify({
8398
+ governor,
8399
+ subdao,
8400
+ states,
8401
+ fromTimestamp,
8402
+ toTimestamp,
8403
+ limit,
8404
+ skip
8405
+ })}`;
8406
+ if (cache) {
8407
+ const cached = this._cache.get(cacheKey);
8408
+ if (cached) return cached;
8409
+ }
8410
+ try {
8411
+ const result = await retryWithBackoff(
8412
+ () => this._queryProposals({
8413
+ governor,
8414
+ subdao,
8415
+ states,
8416
+ fromTimestamp,
8417
+ toTimestamp,
8418
+ limit,
8419
+ skip
8420
+ }),
8421
+ { attempts: this.retries }
8422
+ );
8423
+ if (cache) {
8424
+ this._cache.set(cacheKey, result);
8425
+ }
8426
+ return result;
8427
+ } catch (error) {
8428
+ throw new SubgraphError(
8429
+ `Failed to fetch proposals: ${error.message}`,
8430
+ "QUERY_FAILED",
8431
+ true,
8432
+ error
8433
+ );
8434
+ }
8435
+ }
8436
+ /**
8437
+ * Get proposal by ID
8438
+ * @param {string} id - Proposal ID
8439
+ * @param {object} [options] - Query options
8440
+ * @param {boolean} [options.cache=true] - Use cache
8441
+ * @returns {Promise<object|null>} - Proposal or null
8442
+ */
8443
+ async getProposalById(id2, options = {}) {
8444
+ const { cache = true } = options;
8445
+ const cacheKey = `proposal:${id2}`;
8446
+ if (cache) {
8447
+ const cached = this._cache.get(cacheKey);
8448
+ if (cached !== null) return cached;
8449
+ }
8450
+ try {
8451
+ const result = await retryWithBackoff(
8452
+ () => subgraph.getProposalById({ url: this.url, id: id2 }),
8453
+ { attempts: this.retries }
8454
+ );
8455
+ if (cache) {
8456
+ this._cache.set(cacheKey, result);
8457
+ }
8458
+ return result;
8459
+ } catch (error) {
8460
+ throw new SubgraphError(
8461
+ `Failed to fetch proposal ${id2}: ${error.message}`,
8462
+ "QUERY_FAILED",
8463
+ true,
8464
+ error
8465
+ );
8466
+ }
8467
+ }
8468
+ /**
8469
+ * Get libraries
8470
+ * @param {object} [options] - Query options
8471
+ * @param {string} [options.subdao] - Filter by SubDAO address
8472
+ * @param {number} [options.limit=50] - Max results
8473
+ * @param {number} [options.skip=0] - Results to skip
8474
+ * @param {boolean} [options.cache=true] - Use cache
8475
+ * @returns {Promise<Array>} - List of libraries
8476
+ */
8477
+ async getLibraries(options = {}) {
8478
+ const { subdao, limit = 50, skip = 0, cache = true } = options;
8479
+ const cacheKey = `libraries:${subdao || "all"}:${limit}:${skip}`;
8480
+ if (cache) {
8481
+ const cached = this._cache.get(cacheKey);
8482
+ if (cached) return cached;
8483
+ }
8484
+ try {
8485
+ const result = await retryWithBackoff(
8486
+ () => subgraph.listLibraries({ url: this.url, subdao, first: limit, skip }),
8487
+ { attempts: this.retries }
8488
+ );
8489
+ if (cache) {
8490
+ this._cache.set(cacheKey, result);
8491
+ }
8492
+ return result;
8493
+ } catch (error) {
8494
+ throw new SubgraphError(
8495
+ `Failed to fetch libraries: ${error.message}`,
8496
+ "QUERY_FAILED",
8497
+ true,
8498
+ error
8499
+ );
8500
+ }
8501
+ }
8502
+ /**
8503
+ * Get prompts by tag
8504
+ * @param {object} options - Query options
8505
+ * @param {string} options.tagsHash - Tag hash to filter by
8506
+ * @param {string} [options.registry] - Filter by registry address
8507
+ * @param {number} [options.limit=50] - Max results
8508
+ * @param {number} [options.skip=0] - Results to skip
8509
+ * @param {boolean} [options.cache=true] - Use cache
8510
+ * @returns {Promise<Array>} - List of prompts
8511
+ */
8512
+ async getPromptsByTag(options) {
8513
+ if (!options || !options.tagsHash) {
8514
+ throw new Error("tagsHash is required");
8515
+ }
8516
+ const { tagsHash, registry, limit = 50, skip = 0, cache = true } = options;
8517
+ const cacheKey = `prompts-tag:${tagsHash}:${registry || "all"}:${limit}:${skip}`;
8518
+ if (cache) {
8519
+ const cached = this._cache.get(cacheKey);
8520
+ if (cached) return cached;
8521
+ }
8522
+ try {
8523
+ const result = await retryWithBackoff(
8524
+ () => subgraph.listPromptsByTag({
8525
+ url: this.url,
8526
+ tagsHash,
8527
+ registry,
8528
+ first: limit,
8529
+ skip
8530
+ }),
8531
+ { attempts: this.retries }
8532
+ );
8533
+ if (cache) {
8534
+ this._cache.set(cacheKey, result);
8535
+ }
8536
+ return result;
8537
+ } catch (error) {
8538
+ throw new SubgraphError(
8539
+ `Failed to fetch prompts by tag: ${error.message}`,
8540
+ "QUERY_FAILED",
8541
+ true,
8542
+ error
8543
+ );
8544
+ }
8545
+ }
8546
+ /**
8547
+ * Get registry prompts
8548
+ * @param {object} options - Query options
8549
+ * @param {string} options.registry - Registry address
8550
+ * @param {number} [options.limit=50] - Max results
8551
+ * @param {number} [options.skip=0] - Results to skip
8552
+ * @param {boolean} [options.cache=true] - Use cache
8553
+ * @returns {Promise<Array>} - List of prompts
8554
+ */
8555
+ async getRegistryPrompts(options) {
8556
+ if (!options || !options.registry) {
8557
+ throw new Error("registry is required");
8558
+ }
8559
+ const { registry, limit = 50, skip = 0, cache = true } = options;
8560
+ const cacheKey = `registry-prompts:${registry}:${limit}:${skip}`;
8561
+ if (cache) {
8562
+ const cached = this._cache.get(cacheKey);
8563
+ if (cached) return cached;
8564
+ }
8565
+ try {
8566
+ const result = await retryWithBackoff(
8567
+ () => subgraph.listRegistryPrompts({
8568
+ url: this.url,
8569
+ registry,
8570
+ first: limit,
8571
+ skip
8572
+ }),
8573
+ { attempts: this.retries }
8574
+ );
8575
+ if (cache) {
8576
+ this._cache.set(cacheKey, result);
8577
+ }
8578
+ return result;
8579
+ } catch (error) {
8580
+ throw new SubgraphError(
8581
+ `Failed to fetch registry prompts: ${error.message}`,
8582
+ "QUERY_FAILED",
8583
+ true,
8584
+ error
8585
+ );
8586
+ }
8587
+ }
8588
+ /**
8589
+ * Clear cache
8590
+ */
8591
+ clearCache() {
8592
+ this._cache.clear();
8593
+ }
8594
+ /**
8595
+ * Get cache statistics
8596
+ * @returns {object} - Cache stats
8597
+ */
8598
+ getCacheStats() {
8599
+ return this._cache.stats();
8600
+ }
8601
+ // Private methods
8602
+ async _querySubDAOs({ limit, skip }) {
8603
+ const data = await subgraph.query(
8604
+ this.url,
8605
+ `
8606
+ query($first: Int!, $skip: Int!) {
8607
+ subDAOs(first: $first, skip: $skip, orderBy: createdAt, orderDirection: desc) {
8608
+ id
8609
+ address
8610
+ name
8611
+ description
8612
+ governor
8613
+ registry
8614
+ token
8615
+ createdAt
8616
+ }
8617
+ }
8618
+ `,
8619
+ { first: limit, skip }
8620
+ );
8621
+ return data?.subDAOs || [];
8622
+ }
8623
+ async _queryProposals({ governor, subdao, states, fromTimestamp, toTimestamp, limit, skip }) {
8624
+ return await subgraph.listProposalsFiltered({
8625
+ url: this.url,
8626
+ governor,
8627
+ states,
8628
+ fromTimestamp,
8629
+ toTimestamp,
8630
+ first: limit,
8631
+ skip
8632
+ });
8633
+ }
8634
+ };
8635
+ module2.exports = { SubgraphService };
8636
+ }
8637
+ });
8638
+
8639
+ // src/services/ipfs/client.js
8640
+ var require_client2 = __commonJS({
8641
+ "src/services/ipfs/client.js"(exports2, module2) {
8642
+ var ipfs = require_ipfs();
8643
+ var { SimpleCache } = require_cache();
8644
+ var { retryWithBackoff } = require_retry();
8645
+ var { IPFSError } = require_errors2();
8646
+ var IPFSService = class {
8647
+ /**
8648
+ * @param {object} config - Service configuration
8649
+ * @param {string} config.workerBaseUrl - IPFS worker base URL
8650
+ * @param {string} config.gateway - Primary IPFS gateway URL
8651
+ * @param {object} [config.signer] - ethers v6 signer for worker auth
8652
+ * @param {Function} [config.getAuth] - Function to get auth credentials
8653
+ * @param {string} [config.workerToken] - Bearer token for worker auth
8654
+ * @param {number} [config.timeout=15000] - Request timeout in ms
8655
+ * @param {number} [config.retries=2] - Number of retry attempts
8656
+ * @param {object} [config.cache] - Cache configuration
8657
+ * @param {boolean} [config.cache.enabled=true] - Enable caching
8658
+ * @param {number} [config.cache.ttl=300000] - Cache TTL in ms (default 5min for immutable CIDs)
8659
+ * @param {number} [config.cache.maxSize=50] - Max cache entries
8660
+ */
8661
+ constructor(config) {
8662
+ if (!config) {
8663
+ throw new Error("IPFSService requires a config object");
8664
+ }
8665
+ this.workerBaseUrl = config.workerBaseUrl;
8666
+ this.gateway = config.gateway;
8667
+ this.timeout = config.timeout || 15e3;
8668
+ this.retries = config.retries || 2;
8669
+ this._client = ipfs.createClient({
8670
+ workerBaseUrl: config.workerBaseUrl,
8671
+ gateway: config.gateway,
8672
+ workerSigner: config.signer,
8673
+ workerGetAuth: config.getAuth,
8674
+ workerToken: config.workerToken,
8675
+ timeoutMs: this.timeout,
8676
+ retries: this.retries
8677
+ });
8678
+ this._cache = new SimpleCache({
8679
+ enabled: config.cache?.enabled !== false,
8680
+ ttl: config.cache?.ttl || 3e5,
8681
+ // 5 minutes
8682
+ maxSize: config.cache?.maxSize || 50
8683
+ });
8684
+ }
8685
+ /**
8686
+ * Upload content to IPFS worker
8687
+ * @param {any} content - Content to upload (will be JSON stringified)
8688
+ * @param {object} [options] - Upload options
8689
+ * @param {string} [options.name] - Content name
8690
+ * @param {boolean} [options.warm=false] - Warm gateways after upload
8691
+ * @returns {Promise<string>} - CID of uploaded content
8692
+ */
8693
+ async upload(content, options = {}) {
8694
+ const { name = "upload", warm = false } = options;
8695
+ try {
8696
+ const result = await retryWithBackoff(
8697
+ () => this._client.uploadJson(content, name, {
8698
+ provider: "worker",
8699
+ warm
8700
+ }),
8701
+ { attempts: this.retries }
8702
+ );
8703
+ return result.cid;
8704
+ } catch (error) {
8705
+ throw new IPFSError(
8706
+ `Failed to upload content: ${error.message}`,
8707
+ "UPLOAD_FAILED",
8708
+ true,
8709
+ error
8710
+ );
8711
+ }
8712
+ }
8713
+ /**
8714
+ * Fetch JSON content by CID with parallel gateway fetching
8715
+ * @param {string} cid - IPFS CID
8716
+ * @param {object} [options] - Fetch options
8717
+ * @param {boolean} [options.cache=true] - Use cache
8718
+ * @param {number} [options.timeout=5000] - Timeout per gateway in ms
8719
+ * @param {string[]} [options.extraGateways] - Additional gateways to try
8720
+ * @returns {Promise<any|null>} - Parsed JSON content or null
8721
+ */
8722
+ async fetchByCID(cid, options = {}) {
8723
+ if (!cid) {
8724
+ throw new IPFSError("CID is required", "INVALID_CID", false);
8725
+ }
8726
+ const { cache = true, timeout = 5e3, extraGateways = [] } = options;
8727
+ if (cache) {
8728
+ const cached = this._cache.get(cid);
8729
+ if (cached) return cached;
8730
+ }
8731
+ const urls = this._client.buildGatewayUrls(cid, extraGateways);
8732
+ try {
8733
+ const result = await this._parallelFetch(urls, timeout);
8734
+ if (result === null) {
8735
+ throw new IPFSError(
8736
+ `All gateways failed for CID ${cid}`,
8737
+ "GATEWAY_FAILED",
8738
+ true
8739
+ );
8740
+ }
8741
+ if (cache) {
8742
+ this._cache.set(cid, result);
8743
+ }
8744
+ return result;
8745
+ } catch (error) {
8746
+ if (error instanceof IPFSError) throw error;
8747
+ throw new IPFSError(
8748
+ `Failed to fetch CID ${cid}: ${error.message}`,
8749
+ "GATEWAY_FAILED",
8750
+ true,
8751
+ error
8752
+ );
8753
+ }
8754
+ }
8755
+ /**
8756
+ * Pin CIDs to IPFS worker
8757
+ * @param {string|string[]} cids - CID or array of CIDs to pin
8758
+ * @param {object} [options] - Pin options
8759
+ * @param {boolean} [options.warm=false] - Warm gateways after pinning
8760
+ * @returns {Promise<void>}
8761
+ */
8762
+ async pin(cids, options = {}) {
8763
+ const { warm = false } = options;
8764
+ const cidList = Array.isArray(cids) ? cids : [cids];
8765
+ if (cidList.length === 0) {
8766
+ throw new IPFSError("At least one CID is required", "INVALID_CID", false);
8767
+ }
8768
+ try {
8769
+ await retryWithBackoff(
8770
+ () => this._client.pin({ cids: cidList, warm }),
8771
+ { attempts: this.retries }
8772
+ );
8773
+ } catch (error) {
8774
+ throw new IPFSError(
8775
+ `Failed to pin CIDs: ${error.message}`,
8776
+ "PIN_FAILED",
8777
+ true,
8778
+ error
8779
+ );
8780
+ }
8781
+ }
8782
+ /**
8783
+ * Warm gateways for a CID (prefetch)
8784
+ * @param {string} cid - CID to warm
8785
+ * @param {object} [options] - Warm options
8786
+ * @param {string[]} [options.gateways] - Specific gateways to warm
8787
+ * @returns {Promise<object>} - Warm result
8788
+ */
8789
+ async warm(cid, options = {}) {
8790
+ const { gateways } = options;
8791
+ try {
8792
+ return await this._client.warmGateways(cid, { gateways });
8793
+ } catch (error) {
8794
+ return { warmed: [] };
8795
+ }
8796
+ }
8797
+ /**
8798
+ * Clear cache
8799
+ */
8800
+ clearCache() {
8801
+ this._cache.clear();
8802
+ }
8803
+ /**
8804
+ * Get cache statistics
8805
+ * @returns {object} - Cache stats
8806
+ */
8807
+ getCacheStats() {
8808
+ return this._cache.stats();
8809
+ }
8810
+ // Private methods
8811
+ /**
8812
+ * Fetch from multiple gateways in parallel
8813
+ * @param {string[]} urls - Gateway URLs to try
8814
+ * @param {number} timeout - Timeout per gateway in ms
8815
+ * @returns {Promise<any|null>} - First successful result or null
8816
+ * @private
8817
+ */
8818
+ async _parallelFetch(urls, timeout) {
8819
+ if (!urls || urls.length === 0) {
8820
+ return null;
8821
+ }
8822
+ const fetchOne = async (url) => {
8823
+ const controller = new AbortController();
8824
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
8825
+ try {
8826
+ const response = await fetch(url, {
8827
+ signal: controller.signal,
8828
+ headers: { Accept: "application/json" }
8829
+ });
8830
+ clearTimeout(timeoutId);
8831
+ if (!response.ok) {
8832
+ return null;
8833
+ }
8834
+ const text = await response.text();
8835
+ try {
8836
+ return JSON.parse(text);
8837
+ } catch {
8838
+ return text;
8839
+ }
8840
+ } catch (error) {
8841
+ clearTimeout(timeoutId);
8842
+ return null;
8843
+ }
8844
+ };
8845
+ const results = await Promise.allSettled(urls.map((url) => fetchOne(url)));
8846
+ for (const result of results) {
8847
+ if (result.status === "fulfilled" && result.value !== null) {
8848
+ return result.value;
8849
+ }
8850
+ }
8851
+ return null;
8852
+ }
8853
+ };
8854
+ module2.exports = { IPFSService };
8855
+ }
8856
+ });
8857
+
8858
+ // src/hooks/useSubDAOs.js
8859
+ var require_useSubDAOs = __commonJS({
8860
+ "src/hooks/useSubDAOs.js"(exports2, module2) {
8861
+ var useSWR = __require("swr");
8862
+ function useSubDAOs(subgraphService, options = {}) {
8863
+ const {
8864
+ limit = 50,
8865
+ skip = 0,
8866
+ cache = true,
8867
+ refreshInterval,
8868
+ revalidateOnFocus = true,
8869
+ revalidateOnReconnect = true
8870
+ } = options;
8871
+ const cacheKey = subgraphService ? ["subdaos", subgraphService.url, limit, skip] : null;
8872
+ const fetcher = async () => {
8873
+ if (!subgraphService) {
8874
+ throw new Error("SubgraphService is required");
8875
+ }
8876
+ return await subgraphService.getSubDAOs({ limit, skip, cache });
8877
+ };
8878
+ return useSWR(cacheKey, fetcher, {
8879
+ refreshInterval,
8880
+ revalidateOnFocus,
8881
+ revalidateOnReconnect,
8882
+ dedupingInterval: cache ? 3e4 : 0
8883
+ // Match service cache TTL
8884
+ });
8885
+ }
8886
+ module2.exports = { useSubDAOs };
8887
+ }
8888
+ });
8889
+
8890
+ // src/hooks/useProposals.js
8891
+ var require_useProposals = __commonJS({
8892
+ "src/hooks/useProposals.js"(exports2, module2) {
8893
+ var useSWR = __require("swr");
8894
+ function useProposals(subgraphService, options = {}) {
8895
+ const {
8896
+ governor,
8897
+ subdao,
8898
+ states,
8899
+ fromTimestamp,
8900
+ toTimestamp,
8901
+ limit = 20,
8902
+ skip = 0,
8903
+ cache = true,
8904
+ refreshInterval,
8905
+ revalidateOnFocus = true,
8906
+ revalidateOnReconnect = true
8907
+ } = options;
8908
+ const cacheKey = subgraphService ? [
8909
+ "proposals",
8910
+ subgraphService.url,
8911
+ governor,
8912
+ subdao,
8913
+ states?.join(","),
8914
+ fromTimestamp,
8915
+ toTimestamp,
8916
+ limit,
8917
+ skip
8918
+ ] : null;
8919
+ const fetcher = async () => {
8920
+ if (!subgraphService) {
8921
+ throw new Error("SubgraphService is required");
8922
+ }
8923
+ return await subgraphService.getProposals({
8924
+ governor,
8925
+ subdao,
8926
+ states,
8927
+ fromTimestamp,
8928
+ toTimestamp,
8929
+ limit,
8930
+ skip,
8931
+ cache
8932
+ });
8933
+ };
8934
+ return useSWR(cacheKey, fetcher, {
8935
+ refreshInterval,
8936
+ revalidateOnFocus,
8937
+ revalidateOnReconnect,
8938
+ dedupingInterval: cache ? 3e4 : 0
8939
+ // Match service cache TTL
8940
+ });
8941
+ }
8942
+ module2.exports = { useProposals };
8943
+ }
8944
+ });
8945
+
8946
+ // src/hooks/useFetchCID.js
8947
+ var require_useFetchCID = __commonJS({
8948
+ "src/hooks/useFetchCID.js"(exports2, module2) {
8949
+ var useSWR = __require("swr");
8950
+ function useFetchCID(ipfsService, cid, options = {}) {
8951
+ const {
8952
+ cache = true,
8953
+ timeout = 5e3,
8954
+ extraGateways = [],
8955
+ refreshInterval,
8956
+ revalidateOnFocus = false,
8957
+ // CIDs are immutable
8958
+ revalidateOnReconnect = false
8959
+ // CIDs are immutable
8960
+ } = options;
8961
+ const cacheKey = ipfsService && cid ? ["ipfs-cid", cid] : null;
8962
+ const fetcher = async () => {
8963
+ if (!ipfsService) {
8964
+ throw new Error("IPFSService is required");
8965
+ }
8966
+ if (!cid) {
8967
+ throw new Error("CID is required");
8968
+ }
8969
+ return await ipfsService.fetchByCID(cid, {
8970
+ cache,
8971
+ timeout,
8972
+ extraGateways
8973
+ });
8974
+ };
8975
+ return useSWR(cacheKey, fetcher, {
8976
+ refreshInterval,
8977
+ revalidateOnFocus,
8978
+ revalidateOnReconnect,
8979
+ dedupingInterval: cache ? 3e5 : 0,
8980
+ // Match service cache TTL (5min)
8981
+ // CIDs are immutable, so we can cache errors too
8982
+ shouldRetryOnError: true,
8983
+ errorRetryInterval: 5e3,
8984
+ errorRetryCount: 3
8985
+ });
8986
+ }
8987
+ module2.exports = { useFetchCID };
8988
+ }
8989
+ });
8990
+
8991
+ // src/hooks/useUpload.js
8992
+ var require_useUpload = __commonJS({
8993
+ "src/hooks/useUpload.js"(exports2, module2) {
8994
+ var { useState, useCallback } = __require("react");
8995
+ function useUpload(ipfsService) {
8996
+ const [isUploading, setIsUploading] = useState(false);
8997
+ const [error, setError] = useState(null);
8998
+ const [data, setData] = useState(null);
8999
+ const upload = useCallback(
9000
+ async (content, options = {}) => {
9001
+ if (!ipfsService) {
9002
+ const err = new Error("IPFSService is required");
9003
+ setError(err);
9004
+ throw err;
9005
+ }
9006
+ setIsUploading(true);
9007
+ setError(null);
9008
+ try {
9009
+ const cid = await ipfsService.upload(content, options);
9010
+ setData(cid);
9011
+ return cid;
9012
+ } catch (err) {
9013
+ setError(err);
9014
+ throw err;
9015
+ } finally {
9016
+ setIsUploading(false);
9017
+ }
9018
+ },
9019
+ [ipfsService]
9020
+ );
9021
+ const reset = useCallback(() => {
9022
+ setIsUploading(false);
9023
+ setError(null);
9024
+ setData(null);
9025
+ }, []);
9026
+ return {
9027
+ upload,
9028
+ isUploading,
9029
+ error,
9030
+ data,
9031
+ reset
9032
+ };
9033
+ }
9034
+ module2.exports = { useUpload };
9035
+ }
9036
+ });
9037
+
9038
+ // src/hooks/index.js
9039
+ var require_hooks = __commonJS({
9040
+ "src/hooks/index.js"(exports2, module2) {
9041
+ var { useSubDAOs } = require_useSubDAOs();
9042
+ var { useProposals } = require_useProposals();
9043
+ var { useFetchCID } = require_useFetchCID();
9044
+ var { useUpload } = require_useUpload();
9045
+ module2.exports = {
9046
+ useSubDAOs,
9047
+ useProposals,
9048
+ useFetchCID,
9049
+ useUpload
9050
+ };
9051
+ }
9052
+ });
9053
+
8096
9054
  // src/index.js
8097
9055
  var require_src = __commonJS({
8098
9056
  "src/index.js"(exports2, module2) {
@@ -8133,6 +9091,16 @@ var require_src = __commonJS({
8133
9091
  openzeppelin: require_openzeppelin()
8134
9092
  }
8135
9093
  };
9094
+ var { SubgraphService } = require_client();
9095
+ var { IPFSService } = require_client2();
9096
+ var serviceErrors = require_errors2();
9097
+ var { SimpleCache } = require_cache();
9098
+ var { retryWithBackoff } = require_retry();
9099
+ var hooks = null;
9100
+ try {
9101
+ hooks = require_hooks();
9102
+ } catch (err) {
9103
+ }
8136
9104
  module2.exports = {
8137
9105
  version: pkg.version,
8138
9106
  getProvider: utils.getProvider,
@@ -8163,6 +9131,18 @@ var require_src = __commonJS({
8163
9131
  errors,
8164
9132
  doppler,
8165
9133
  adapters,
9134
+ // New service layer exports
9135
+ services: {
9136
+ SubgraphService,
9137
+ IPFSService
9138
+ },
9139
+ serviceErrors,
9140
+ serviceUtils: {
9141
+ SimpleCache,
9142
+ retryWithBackoff
9143
+ },
9144
+ // React hooks (optional - requires react + swr peer dependencies)
9145
+ hooks,
8166
9146
  // Legacy exports (deprecated): maintain compatibility while consumers migrate
8167
9147
  resolveGovernanceContext: async function legacyResolveGovernanceContext(args) {
8168
9148
  console.warn("[@sage-protocol/sdk] resolveGovernanceContext is deprecated. Use governance helpers instead.");