diff --git a/packages/synapse-core/src/utils/rand.ts b/packages/synapse-core/src/utils/rand.ts index e682615a..8baaf931 100644 --- a/packages/synapse-core/src/utils/rand.ts +++ b/packages/synapse-core/src/utils/rand.ts @@ -45,3 +45,18 @@ export function randIndex(length: number): number { return fallbackRandIndex(length) } } + +/** + * Shuffles an array using the Fisher-Yates algorithm + * @param array - The array to shuffle + * @returns A new array with the elements shuffled + */ +export function shuffle(array: T[]): T[] { + // Fisher-Yates shuffle + const arr = array.slice() + for (let i = arr.length; i-- > 1; ) { + const j = randIndex(i + 1) + ;[arr[i], arr[j]] = [arr[j], arr[i]] + } + return arr +} diff --git a/packages/synapse-sdk/src/sp-registry/index.ts b/packages/synapse-sdk/src/sp-registry/index.ts index f2dec26d..ae639a74 100644 --- a/packages/synapse-sdk/src/sp-registry/index.ts +++ b/packages/synapse-sdk/src/sp-registry/index.ts @@ -14,6 +14,7 @@ export type { PDPServiceInfo, PRODUCTS, ProductType, + ProviderFilterOptions, ProviderInfo, ProviderRegistrationInfo, ServiceProduct, diff --git a/packages/synapse-sdk/src/sp-registry/service.ts b/packages/synapse-sdk/src/sp-registry/service.ts index 351d1a30..20f42634 100644 --- a/packages/synapse-sdk/src/sp-registry/service.ts +++ b/packages/synapse-sdk/src/sp-registry/service.ts @@ -24,8 +24,15 @@ import type { Chain } from '@filoz/synapse-core/chains' import * as SP from '@filoz/synapse-core/sp-registry' +import { shuffle } from '@filoz/synapse-core/utils' import type { Account, Address, Client, Hash, Transport } from 'viem' -import type { PDPOffering, ProductType, ProviderRegistrationInfo } from './types.ts' +import { + type PDPOffering, + PRODUCTS, + type ProductType, + type ProviderFilterOptions, + type ProviderRegistrationInfo, +} from './types.ts' export class SPRegistryService { private readonly _client: Client @@ -309,4 +316,48 @@ export class SPRegistryService { providerIds, }) } + + /** + * Filter providers based on criteria + * @param filter - Filtering options + * @returns Filtered list of providers + */ + async filterProviders(filter?: ProviderFilterOptions): Promise { + const providers = await this.getAllActiveProviders() + if (!filter) return providers + + if (filter.type !== undefined) { + const requestedTypeValue = PRODUCTS[filter.type] + if (requestedTypeValue === undefined) { + return [] // Invalid product type + } + } + + const typeKey = (filter.type ?? 'PDP').toLowerCase() + + const result = providers.filter((d) => { + switch (typeKey) { + case 'pdp': { + const offering = d[typeKey as keyof typeof d] as PDPOffering + return ( + (!filter.location || offering.location?.toLowerCase().includes(filter.location.toLowerCase())) && + (filter.minPieceSizeInBytes === undefined || + offering.maxPieceSizeInBytes >= BigInt(filter.minPieceSizeInBytes)) && + (filter.maxPieceSizeInBytes === undefined || + offering.minPieceSizeInBytes <= BigInt(filter.maxPieceSizeInBytes)) && + (filter.ipniIpfs === undefined || offering.ipniIpfs === filter.ipniIpfs) && + (filter.ipniPiece === undefined || offering.ipniPiece === filter.ipniPiece) && + (filter.maxStoragePricePerTibPerDay === undefined || + offering.storagePricePerTibPerDay <= BigInt(filter.maxStoragePricePerTibPerDay)) && + (filter.minProvingPeriodInEpochs === undefined || + offering.minProvingPeriodInEpochs >= BigInt(filter.minProvingPeriodInEpochs)) + ) + } + default: + return false // Unsupported product type + } + }) + + return filter.randomize ? shuffle(result) : result + } } diff --git a/packages/synapse-sdk/src/sp-registry/types.ts b/packages/synapse-sdk/src/sp-registry/types.ts index a43d162e..31354193 100644 --- a/packages/synapse-sdk/src/sp-registry/types.ts +++ b/packages/synapse-sdk/src/sp-registry/types.ts @@ -59,3 +59,23 @@ export interface PDPServiceInfo { capabilities: Record // Object map of capability key-value pairs isActive: boolean } + +/** * Options for filtering providers + */ +export interface ProviderFilterOptions { + type?: keyof typeof PRODUCTS // 'PDP' etc. + location?: string + + minPieceSizeInBytes?: number + maxPieceSizeInBytes?: number + + ipniIpfs?: boolean + ipniPiece?: boolean + + serviceStatus?: string // or union type below + + maxStoragePricePerTibPerDay?: number + minProvingPeriodInEpochs?: number + + randomize?: boolean +} diff --git a/packages/synapse-sdk/src/synapse.ts b/packages/synapse-sdk/src/synapse.ts index ffdca3ef..ec5f3c72 100644 --- a/packages/synapse-sdk/src/synapse.ts +++ b/packages/synapse-sdk/src/synapse.ts @@ -14,6 +14,7 @@ import { import { FilBeamService } from './filbeam/index.ts' import { PaymentsService } from './payments/index.ts' import { ChainRetriever, FilBeamRetriever } from './retriever/index.ts' +import type { ProviderFilterOptions } from './sp-registry/index.ts' import { SPRegistryService } from './sp-registry/index.ts' import type { StorageContext } from './storage/index.ts' import { StorageManager } from './storage/manager.ts' @@ -236,4 +237,22 @@ export class Synapse { console.warn('synapse.getStorageInfo() is deprecated. Use synapse.storage.getStorageInfo() instead.') return await this._storageManager.getStorageInfo() } + + /** + * Get providers with filtering options + * @param filter - Filtering options + * @returns Filtered list of providers + */ + async filterProviders(filter?: ProviderFilterOptions): Promise { + // Create SPRegistryService + try { + const _registryAddress = this._warmStorageService.getServiceProviderRegistryAddress() + const spRegistry = new SPRegistryService(this._client) + + const providers = await spRegistry.filterProviders(filter) + return providers + } catch (error) { + throw new Error(`Failed to filter providers: ${error instanceof Error ? error.message : String(error)}`) + } + } } diff --git a/packages/synapse-sdk/src/test/sp-registry-service.test.ts b/packages/synapse-sdk/src/test/sp-registry-service.test.ts index 18c62e5a..065cdd0b 100644 --- a/packages/synapse-sdk/src/test/sp-registry-service.test.ts +++ b/packages/synapse-sdk/src/test/sp-registry-service.test.ts @@ -372,4 +372,43 @@ describe('SPRegistryService', () => { } }) }) + + describe('Provider Filtering', () => { + it('should filter providers by multiple criteria', async () => { + server.use(Mocks.JSONRPC(Mocks.presets.basic)) + + // Test location filtering (case-insensitive partial match) + const byLocation = await service.filterProviders({ location: 'US' }) + assert.equal(byLocation.length, 2) // Both providers have 'US' location + + const byPrice = await service.filterProviders({ maxStoragePricePerTibPerDay: 999999 }) + assert.equal(byPrice.length, 0) + + // Test piece size filtering + const byPieceSize = await service.filterProviders({ + minPieceSizeInBytes: Number(SIZE_CONSTANTS.KiB), + maxPieceSizeInBytes: Number(SIZE_CONSTANTS.GiB), + }) + assert.equal(byPieceSize.length, 2) // Both providers support this range + + // Test no filters returns all + const all = await service.filterProviders() + assert.equal(all.length, 2) + }) + + it('should randomize results when requested', async () => { + server.use(Mocks.JSONRPC(Mocks.presets.basic)) + + const results = [] + for (let i = 0; i < 5; i++) { + const filtered = await service.filterProviders({ randomize: true }) + results.push(filtered.map((p) => p.serviceProvider)) + } + + // At least one result should have different order (with high probability) + const firstOrder = JSON.stringify(results[0]) + const hasDifferentOrder = results.some((r) => JSON.stringify(r) !== firstOrder) + assert.isTrue(hasDifferentOrder || results[0].length === 1, 'Results should be randomized') + }) + }) }) diff --git a/packages/synapse-sdk/src/test/synapse.test.ts b/packages/synapse-sdk/src/test/synapse.test.ts index f26cb4bf..f96dac44 100644 --- a/packages/synapse-sdk/src/test/synapse.test.ts +++ b/packages/synapse-sdk/src/test/synapse.test.ts @@ -7,7 +7,6 @@ import { calibration } from '@filoz/synapse-core/chains' import * as Mocks from '@filoz/synapse-core/mocks' import * as Piece from '@filoz/synapse-core/piece' -import { getPermissionFromTypeHash, type SessionKeyPermissions } from '@filoz/synapse-core/session-key' import { assert } from 'chai' import { setup } from 'iso-web/msw' import { HttpResponse, http } from 'msw' @@ -902,4 +901,16 @@ describe('Synapse', () => { }) }) }) + + describe('Provider Filtering', () => { + it('should filter providers through Synapse class', async () => { + server.use(Mocks.JSONRPC(Mocks.presets.basic)) + + const synapse = new Synapse({ client }) + // Test filtering by location + const filtered = await synapse.filterProviders({ location: 'US' }) + assert.equal(filtered.length, 2) // Both providers have 'US' location in preset + assert.exists(filtered[0].pdp.location) + }) + }) }) diff --git a/packages/synapse-sdk/src/types.ts b/packages/synapse-sdk/src/types.ts index 780235b1..cc50de90 100644 --- a/packages/synapse-sdk/src/types.ts +++ b/packages/synapse-sdk/src/types.ts @@ -10,7 +10,6 @@ import type { PieceCID } from '@filoz/synapse-core/piece' import type { PDPProvider } from '@filoz/synapse-core/sp-registry' import type { MetadataObject } from '@filoz/synapse-core/utils' import type { Account, Address, Client, Hex, Transport } from 'viem' - // Re-export PieceCID and PDPProvider types export type { PieceCID, PDPProvider } export type PrivateKey = string