diff --git a/packages/synapse-sdk/src/storage/context.ts b/packages/synapse-sdk/src/storage/context.ts index a9ec29b3d..57224a087 100644 --- a/packages/synapse-sdk/src/storage/context.ts +++ b/packages/synapse-sdk/src/storage/context.ts @@ -34,6 +34,7 @@ import { SPRegistryService } from '../sp-registry/index.ts' import type { ProviderInfo } from '../sp-registry/types.ts' import type { Synapse } from '../synapse.ts' import type { + CreateContextsOptions, DownloadOptions, EnhancedDataSetInfo, MetadataEntry, @@ -58,6 +59,8 @@ import { import { combineMetadata, metadataMatches, objectToEntries, validatePieceMetadata } from '../utils/metadata.ts' import type { WarmStorageService } from '../warm-storage/index.ts' +const NO_REMAINING_PROVIDERS_ERROR_MESSAGE = 'No approved service providers available' + export class StorageContext { private readonly _synapse: Synapse private readonly _provider: ProviderInfo @@ -175,6 +178,95 @@ export class StorageContext { this._pdpServer = new PDPServer(authHelper, provider.products.PDP.data.serviceURL) } + /** + * Creates new storage contexts with specified options + * Each context corresponds to a different data set + */ + static async createContexts( + synapse: Synapse, + warmStorageService: WarmStorageService, + options: CreateContextsOptions + ): Promise { + const count = options?.count ?? 2 + const resolutions: ProviderSelectionResult[] = [] + const clientAddress = await synapse.getClient().getAddress() + const registryAddress = warmStorageService.getServiceProviderRegistryAddress() + const spRegistry = new SPRegistryService(synapse.getProvider(), registryAddress) + if (options.dataSetIds) { + const selections = [] + for (const dataSetId of new Set(options.dataSetIds)) { + selections.push( + StorageContext.resolveByDataSetId(dataSetId, warmStorageService, spRegistry, clientAddress, { + withCDN: options.withCDN, + withIpni: options.withIpni, + dev: options.dev, + metadata: options.metadata, + }) + ) + if (selections.length >= count) { + break + } + } + resolutions.push(...(await Promise.all(selections))) + } + const resolvedProviderIds = resolutions.map((resolution) => resolution.provider.id) + if (resolutions.length < count) { + if (options.providerIds) { + const selections = [] + // NOTE: Set.difference is unavailable in some targets + for (const providerId of [...new Set(options.providerIds)].filter( + (providerId) => !resolvedProviderIds.includes(providerId) + )) { + selections.push( + StorageContext.resolveByProviderId( + clientAddress, + providerId, + options.metadata ?? {}, + warmStorageService, + spRegistry, + options.forceCreateDataSets + ) + ) + resolvedProviderIds.push(providerId) + if (selections.length + resolutions.length >= count) { + break + } + } + resolutions.push(...(await Promise.all(selections))) + } + } + if (resolutions.length < count) { + const excludeProviderIds = [...(options.excludeProviderIds ?? []), ...resolvedProviderIds] + for (let i = resolutions.length; i < count; i++) { + try { + const resolution = await StorageContext.smartSelectProvider( + clientAddress, + options.metadata ?? {}, + warmStorageService, + spRegistry, + excludeProviderIds, + options.forceCreateDataSets ?? false, + options.withIpni ?? false, + options.dev ?? false + ) + excludeProviderIds.push(resolution.provider.id) + resolutions.push(resolution) + } catch (error) { + if (error instanceof Error && error.message.includes(NO_REMAINING_PROVIDERS_ERROR_MESSAGE)) { + break + } + throw error + } + } + } + return await Promise.all( + resolutions.map( + async (resolution) => + await StorageContext.createWithSelectedProvider(resolution, synapse, warmStorageService, options) + ) + ) + } + /** * Static factory method to create a StorageContext * Handles provider selection and data set selection/creation @@ -191,6 +283,15 @@ export class StorageContext { // Resolve provider and data set based on options const resolution = await StorageContext.resolveProviderAndDataSet(synapse, warmStorageService, spRegistry, options) + return await StorageContext.createWithSelectedProvider(resolution, synapse, warmStorageService, options) + } + + private static async createWithSelectedProvider( + resolution: ProviderSelectionResult, + synapse: Synapse, + warmStorageService: WarmStorageService, + options: StorageServiceOptions = {} + ): Promise { // Notify callback about provider selection try { options.callbacks?.onProviderSelected?.(resolution.provider) @@ -227,8 +328,7 @@ export class StorageContext { spRegistry: SPRegistryService, options: StorageServiceOptions ): Promise { - const client = synapse.getClient() - const clientAddress = await client.getAddress() + const clientAddress = await synapse.getClient().getAddress() // Handle explicit data set ID selection (highest priority) if (options.dataSetId != null && options.forceCreateDataSet !== true) { @@ -274,10 +374,10 @@ export class StorageContext { requestedMetadata, warmStorageService, spRegistry, - options.excludeProviderIds, - options.forceCreateDataSet, - options.withIpni, - options.dev + options.excludeProviderIds ?? [], + options.forceCreateDataSet ?? false, + options.withIpni ?? false, + options.dev ?? false ) } @@ -491,10 +591,10 @@ export class StorageContext { requestedMetadata: Record, warmStorageService: WarmStorageService, spRegistry: SPRegistryService, - excludeProviderIds?: number[], - forceCreateDataSet?: boolean, - withIpni?: boolean, - dev?: boolean + excludeProviderIds: number[], + forceCreateDataSet: boolean, + withIpni: boolean, + dev: boolean ): Promise { // Strategy: // 1. Try to find existing data sets first @@ -503,9 +603,15 @@ export class StorageContext { // Get client's data sets const dataSets = await warmStorageService.getClientDataSetsWithDetails(signerAddress) + const skipProviderIds = new Set(excludeProviderIds) // Filter for managed data sets with matching metadata const managedDataSets = dataSets.filter( - (ps) => ps.isLive && ps.isManaged && ps.pdpEndEpoch === 0 && metadataMatches(ps.metadata, requestedMetadata) + (ps) => + ps.isLive && + ps.isManaged && + ps.pdpEndEpoch === 0 && + metadataMatches(ps.metadata, requestedMetadata) && + !skipProviderIds.has(ps.providerId) ) if (managedDataSets.length > 0 && !forceCreateDataSet) { @@ -518,10 +624,12 @@ export class StorageContext { // Create async generator that yields providers lazily async function* generateProviders(): AsyncGenerator { - const skipProviderIds = new Set(excludeProviderIds) - // First, yield providers from existing data sets (in sorted order) for (const dataSet of sorted) { + if (skipProviderIds.has(dataSet.providerId)) { + continue + } + skipProviderIds.add(dataSet.providerId) const provider = await spRegistry.getProvider(dataSet.providerId) if (provider == null) { @@ -530,18 +638,21 @@ export class StorageContext { ) continue } - if (!skipProviderIds.has(provider.id)) { - skipProviderIds.add(provider.id) - yield provider + + if (withIpni && provider.products.PDP?.data.ipniIpfs === false) { + continue + } + + if (!dev && provider.products.PDP?.capabilities?.dev != null) { + continue } + + yield provider } } try { - const selectedProvider = await StorageContext.selectProviderWithPing(generateProviders(), { - dev, - withIpni, - }) + const selectedProvider = await StorageContext.selectProviderWithPing(generateProviders()) // Find the first matching data set ID for this provider // Match by provider ID (stable identifier in the registry) @@ -575,15 +686,18 @@ export class StorageContext { const approvedIds = await warmStorageService.getApprovedProviderIds() const approvedProviders = await spRegistry.getProviders(approvedIds) const allProviders = approvedProviders.filter( - (provider: ProviderInfo) => excludeProviderIds?.includes(provider.id) !== true + (provider: ProviderInfo) => + (!withIpni || provider.products.PDP?.data.ipniIpfs === true) && + (dev || provider.products.PDP?.capabilities?.dev == null) && + !excludeProviderIds.includes(provider.id) ) if (allProviders.length === 0) { - throw createError('StorageContext', 'smartSelectProvider', 'No approved service providers available') + throw createError('StorageContext', 'smartSelectProvider', NO_REMAINING_PROVIDERS_ERROR_MESSAGE) } // Random selection from all providers - const provider = await StorageContext.selectRandomProvider(allProviders, withIpni, dev) + const provider = await StorageContext.selectRandomProvider(allProviders) return { provider, @@ -600,11 +714,7 @@ export class StorageContext { * @param dev - Include dev providers * @returns Selected provider */ - private static async selectRandomProvider( - providers: ProviderInfo[], - withIpni?: boolean, - dev?: boolean - ): Promise { + private static async selectRandomProvider(providers: ProviderInfo[]): Promise { if (providers.length === 0) { throw createError('StorageContext', 'selectRandomProvider', 'No providers available') } @@ -620,10 +730,7 @@ export class StorageContext { } } - return await StorageContext.selectProviderWithPing(generateRandomProviders(), { - withIpni, - dev, - }) + return await StorageContext.selectProviderWithPing(generateRandomProviders()) } /** @@ -633,23 +740,11 @@ export class StorageContext { * @returns The first provider that responds * @throws If all providers fail */ - private static async selectProviderWithPing( - providers: AsyncIterable, - options?: { withIpni?: boolean; dev?: boolean } - ): Promise { + private static async selectProviderWithPing(providers: AsyncIterable): Promise { let providerCount = 0 - const { withIpni, dev } = options ?? {} // Try providers in order until we find one that responds to ping for await (const provider of providers) { - if (withIpni && provider?.products.PDP?.data.ipniIpfs === false) { - continue - } - - if (!dev && provider?.products.PDP?.capabilities?.dev != null) { - continue - } - providerCount++ try { // Create a temporary PDPServer for this specific provider's endpoint diff --git a/packages/synapse-sdk/src/storage/manager.ts b/packages/synapse-sdk/src/storage/manager.ts index 5d78fb87e..e940f73a4 100644 --- a/packages/synapse-sdk/src/storage/manager.ts +++ b/packages/synapse-sdk/src/storage/manager.ts @@ -25,6 +25,7 @@ import { asPieceCID, downloadAndValidate } from '../piece/index.ts' import { SPRegistryService } from '../sp-registry/index.ts' import type { Synapse } from '../synapse.ts' import type { + CreateContextsOptions, DownloadOptions, EnhancedDataSetInfo, PieceCID, @@ -230,6 +231,40 @@ export class StorageManager { return await StorageContext.performPreflightCheck(this._warmStorageService, this._synapse.payments, size, withCDN) } + /** + * Creates storage contexts for multi-provider storage deals and other operations. + * + * By storing data with multiple independent providers, you reduce dependency on any + * single provider and improve overall data availability. Use contexts together as a group. + * + * Contexts are selected by priority: + * 1. Specified datasets (`dataSetIds`) - uses their existing providers + * 2. Specified providers (`providerIds` or `providerAddresses`) - finds or creates matching datasets + * 3. Automatically selected from remaining approved providers + * + * For automatic selection, existing datasets matching the `metadata` are reused unless + * `forceCreateDataSets` is true. Providers are randomly chosen to distribute across the network. + * + * @param synapse - Synapse instance + * @param warmStorageService - Warm storage service instance + * @param options - Configuration options + * @param options.count - Maximum number of contexts to create (default: 2) + * @param options.dataSetIds - Specific dataset IDs to include + * @param options.providerIds - Specific provider IDs to use + * @param options.metadata - Metadata to match when finding/creating datasets + * @param options.forceCreateDataSets - Always create new datasets instead of reusing existing ones + * @param options.excludeProviderIds - Provider IDs to skip during selection + * @returns Promise resolving to array of storage contexts + */ + async createContexts(options?: CreateContextsOptions): Promise { + return await StorageContext.createContexts(this._synapse, this._warmStorageService, { + ...options, + withCDN: options?.withCDN ?? this._withCDN, + withIpni: options?.withIpni ?? this._withIpni, + dev: options?.dev ?? this._dev, + }) + } + /** * Create a new storage context with specified options */ diff --git a/packages/synapse-sdk/src/test/mocks/jsonrpc/service-registry.ts b/packages/synapse-sdk/src/test/mocks/jsonrpc/service-registry.ts index fd52ee935..a314bac27 100644 --- a/packages/synapse-sdk/src/test/mocks/jsonrpc/service-registry.ts +++ b/packages/synapse-sdk/src/test/mocks/jsonrpc/service-registry.ts @@ -1,7 +1,11 @@ /** biome-ignore-all lint/style/noNonNullAssertion: testing */ +import { encodePDPCapabilities } from '@filoz/synapse-core/utils' import type { ExtractAbiFunction } from 'abitype' -import { decodeFunctionData, encodeAbiParameters, type Hex } from 'viem' +import { assert } from 'chai' +import type { Hex } from 'viem' +import { decodeFunctionData, encodeAbiParameters } from 'viem' +import type { PDPServiceInfo } from '../../../sp-registry/types.ts' import { CONTRACT_ABIS } from '../../../utils/constants.ts' import type { AbiToType, JSONRPCOptions } from './types.ts' @@ -22,6 +26,11 @@ export type getProviderWithProduct = ExtractAbiFunction< 'getProviderWithProduct' > +export type getProvidersByProductType = ExtractAbiFunction< + typeof CONTRACT_ABIS.SERVICE_PROVIDER_REGISTRY, + 'getProvidersByProductType' +> + export interface ServiceRegistryOptions { getProviderByAddress?: (args: AbiToType) => AbiToType getProviderIdByAddress?: ( @@ -31,6 +40,158 @@ export interface ServiceRegistryOptions { getProviderWithProduct?: ( args: AbiToType ) => AbiToType + getProvidersByProductType?: ( + args: AbiToType + ) => AbiToType +} + +export type ServiceProviderInfoView = AbiToType[0] +export type ProviderWithProduct = AbiToType[0] + +const EMPTY_PROVIDER_INFO = { + serviceProvider: '0x0000000000000000000000000000000000000000', + payee: '0x0000000000000000000000000000000000000000', + name: '', + description: '', + isActive: false, +} as const + +const EMPTY_PROVIDER_INFO_VIEW: ServiceProviderInfoView = { + providerId: 0n, + info: EMPTY_PROVIDER_INFO, +} + +const EMPTY_PROVIDER_WITH_PRODUCT: [ProviderWithProduct] = [ + { + providerId: 0n, + providerInfo: EMPTY_PROVIDER_INFO, + product: { + productType: 0, + capabilityKeys: [], + isActive: false, + }, + productCapabilityValues: [] as Hex[], + }, +] + +export function mockServiceProviderRegistry( + providers: ServiceProviderInfoView[], + services?: (PDPServiceInfo | null)[] +): ServiceRegistryOptions { + assert.isAtMost(services?.length ?? 0, providers.length) + return { + getProvider: ([providerId]) => { + if (providerId < 0n || providerId > providers.length) { + throw new Error('Provider does not exist') + } + for (const provider of providers) { + if (providerId === provider.providerId) { + return [provider] + } + } + throw new Error('Provider not found') + }, + getProviderWithProduct: ([providerId, productType]) => { + if (!services) { + return EMPTY_PROVIDER_WITH_PRODUCT + } + for (let i = 0; i < services.length; i++) { + if (providers[i].providerId === providerId) { + const providerInfo = providers[i].info + const service = services[i] + if (service == null) { + return [ + { + providerId, + providerInfo, + product: { + productType, + capabilityKeys: [], + isActive: false, + }, + productCapabilityValues: [] as Hex[], + }, + ] + } + const [capabilityKeys, productCapabilityValues] = encodePDPCapabilities(service.offering) + return [ + { + providerId, + providerInfo, + product: { + productType, + capabilityKeys, + isActive: true, + }, + productCapabilityValues, + }, + ] + } + } + return EMPTY_PROVIDER_WITH_PRODUCT + }, + getProvidersByProductType: ([productType, onlyActive, offset, limit]) => { + if (!services) { + return [ + { + providers: [] as ProviderWithProduct[], + hasMore: false, + }, + ] + } + const filteredProviders: ProviderWithProduct[] = [] + for (let i = 0; i < services.length; i++) { + const providerInfoView = providers[i] + const providerId = providerInfoView.providerId + const providerInfo = providers[i].info + if (onlyActive && !providerInfo.isActive) { + continue + } + const service = services[i] + if (service == null || !service.isActive) { + continue + } + if (productType !== 0) { + // this mock currently only supports PDP + continue + } + const [capabilityKeys, productCapabilityValues] = encodePDPCapabilities(service.offering) + filteredProviders.push({ + providerId, + providerInfo, + product: { + productType: 0, // PDP + capabilityKeys, + isActive: service.isActive, + }, + productCapabilityValues, + }) + } + const hasMore = offset + limit >= filteredProviders.length + return [ + { + providers: filteredProviders.slice(Number(offset), Number(offset + limit)), + hasMore, + }, + ] + }, + getProviderByAddress: ([address]) => { + for (const provider of providers) { + if (address === provider.info.serviceProvider) { + return [provider] + } + } + return [EMPTY_PROVIDER_INFO_VIEW] + }, + getProviderIdByAddress: ([address]) => { + for (const provider of providers) { + if (address === provider.info.serviceProvider) { + return [provider.providerId] + } + } + return [0n] + }, + } } /** 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 64a3d7a93..cfac3692a 100644 --- a/packages/synapse-sdk/src/test/sp-registry-service.test.ts +++ b/packages/synapse-sdk/src/test/sp-registry-service.test.ts @@ -55,7 +55,6 @@ describe('SPRegistryService', () => { return { providerId: 1, info: { - id: BigInt(1), serviceProvider: mockProviderAddress, payee: mockProviderAddress, name: 'Test Provider', diff --git a/packages/synapse-sdk/src/test/storage.test.ts b/packages/synapse-sdk/src/test/storage.test.ts index 202ed0954..f4d6e06e0 100644 --- a/packages/synapse-sdk/src/test/storage.test.ts +++ b/packages/synapse-sdk/src/test/storage.test.ts @@ -146,7 +146,7 @@ const mockProvider: ProviderInfo = createSimpleProvider({ function createMockWarmStorageService(dataSets?: any[], overrides: any = {}) { return { getClientDataSetsWithDetails: async () => dataSets ?? [], - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, getDataSetMetadata: async () => ({}), getApprovedProviderIds: async () => [], ...overrides, @@ -1011,7 +1011,7 @@ describe('StorageService', () => { getProviderIdByAddress: async () => 7, getApprovedProvider: async () => mockProviders[0], getAllApprovedProviders: async () => mockProviders, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any // Mock fetch for ping validation @@ -1127,7 +1127,7 @@ describe('StorageService', () => { getClientDataSetsWithDetails: async () => [], // No data sets getProviderIdByAddress: async () => 11, getNextClientDataSetId: async () => 1, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = await StorageContext.create(mockSynapse, mockWarmStorageService, { @@ -1166,7 +1166,7 @@ describe('StorageService', () => { return [] }, getNextClientDataSetId: async () => 1, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any await StorageContext.create(mockSynapse, mockWarmStorageService, { @@ -1223,7 +1223,7 @@ describe('StorageService', () => { getAllApprovedProvidersCalled = true throw new Error('Should not fetch all providers when data sets exist') }, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any // Mock fetch for ping validation - existing provider should succeed @@ -1264,7 +1264,7 @@ describe('StorageService', () => { return mockProviders }, getNextClientDataSetId: async () => 1, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any await StorageContext.create(mockSynapse, mockWarmStorageService, {}) @@ -1347,11 +1347,11 @@ describe('StorageService', () => { }) } return { - serviceProvider: '0x0000000000000000000000000000000000000000', + serviceProvider: ADDRESSES.calibration.spRegistry, serviceURL: '', } }, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any try { @@ -1425,7 +1425,7 @@ describe('StorageService', () => { return null }, getAllApprovedProviders: async () => [provider2WithDifferentPayee], - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, isProviderIdApproved: async (id: number) => id === 2, getApprovedProviderIds: async () => [2], getDataSetMetadata: async (dataSetId: number) => { @@ -1528,7 +1528,7 @@ describe('StorageService', () => { return null }, getAllApprovedProviders: async () => [TEST_PROVIDERS.provider1, provider3], - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, getNextClientDataSetId: async () => 2, isProviderIdApproved: async (id: number) => id === 1 || id === 3, getApprovedProviderIds: async () => [1, 3], @@ -1590,7 +1590,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -1632,7 +1632,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -1676,7 +1676,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -1721,7 +1721,7 @@ describe('StorageService', () => { it('should enforce maximum size limit in preflightUpload', async () => { const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -1762,7 +1762,7 @@ describe('StorageService', () => { } as unknown as Synapse const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapseWithDownload, @@ -1791,7 +1791,7 @@ describe('StorageService', () => { } as unknown as Synapse const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapseWithError, @@ -1829,7 +1829,7 @@ describe('StorageService', () => { } as unknown as Synapse const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapseWithOptions, @@ -1858,7 +1858,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -1949,7 +1949,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2011,7 +2011,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2084,7 +2084,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2149,7 +2149,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2270,7 +2270,7 @@ describe('StorageService', () => { it('should handle upload piece failure', async () => { const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2304,7 +2304,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2353,7 +2353,7 @@ describe('StorageService', () => { throw new Error('Data set not managed by this WarmStorage') }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( mockSynapse, @@ -2514,7 +2514,7 @@ describe('StorageService', () => { return idx >= 0 ? idx + 1 : 0 }, getApprovedProvider: async (id: number) => testProviders[id - 1] ?? null, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any let pingCallCount = 0 @@ -2570,7 +2570,7 @@ describe('StorageService', () => { getAllApprovedProviders: async () => testProviders, getProviderIdByAddress: async () => 0, getApprovedProvider: async () => null, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any let pingCallCount = 0 @@ -2645,7 +2645,7 @@ describe('StorageService', () => { getProviderIdByAddress: async () => 1, getApprovedProvider: async () => testProvider, getAllApprovedProviders: async () => [], // Return empty list to prevent fallback - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const originalFetch = global.fetch @@ -2743,7 +2743,7 @@ describe('StorageService', () => { getProviderIdByAddress: async () => 1, getApprovedProvider: async () => testProvider, getAllApprovedProviders: async () => [], // Return empty list to prevent fallback - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any let pingCount = 0 @@ -2819,7 +2819,7 @@ describe('StorageService', () => { getProviderInfo: async () => { throw new Error('Provider not found') }, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const mockWarmStorageService = createMockWarmStorageService() const service = new StorageContext( @@ -2853,7 +2853,7 @@ describe('StorageService', () => { it('should successfully fetch data set pieces', async () => { const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, getPDPVerifierAddress: () => ADDRESSES.calibration.pdpVerifier, } as any @@ -2919,7 +2919,7 @@ describe('StorageService', () => { it('should handle empty data set pieces', async () => { const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, getPDPVerifierAddress: () => ADDRESSES.calibration.pdpVerifier, } as any @@ -2960,7 +2960,7 @@ describe('StorageService', () => { it('should handle invalid CID in response', async () => { const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, getPDPVerifierAddress: () => ADDRESSES.calibration.pdpVerifier, } as any @@ -3006,7 +3006,7 @@ describe('StorageService', () => { it('should handle PDP server errors', async () => { const mockWarmStorageService = { - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, getPDPVerifierAddress: () => ADDRESSES.calibration.pdpVerifier, } as any @@ -3057,7 +3057,7 @@ describe('StorageService', () => { const mockWarmStorageService = { getMaxProvingPeriod: async () => 2880, getChallengeWindow: async () => 60, - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3107,7 +3107,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3163,7 +3163,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3220,7 +3220,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3274,7 +3274,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3332,7 +3332,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3410,7 +3410,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( @@ -3464,7 +3464,7 @@ describe('StorageService', () => { /* no-op */ }, getDataSet: async (): Promise => ({ clientDataSetId: 1n }), - getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001', + getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry, } as any const service = new StorageContext( diff --git a/packages/synapse-sdk/src/test/synapse.test.ts b/packages/synapse-sdk/src/test/synapse.test.ts index 9ba1fc3b5..c7cef986d 100644 --- a/packages/synapse-sdk/src/test/synapse.test.ts +++ b/packages/synapse-sdk/src/test/synapse.test.ts @@ -4,6 +4,8 @@ * Basic tests for Synapse class */ +import { capabilitiesListToObject, encodePDPCapabilities } from '@filoz/synapse-core/utils' +import type { PDPOffering } from '@filoz/synapse-core/warm-storage' import { assert } from 'chai' import { ethers } from 'ethers' import { setup } from 'iso-web/msw' @@ -12,9 +14,12 @@ import pDefer from 'p-defer' import { type Address, bytesToHex, type Hex, isAddressEqual, numberToBytes, parseUnits, stringToHex } from 'viem' import { PaymentsService } from '../payments/index.ts' import { PDP_PERMISSIONS } from '../session/key.ts' +import type { PDPServiceInfo } from '../sp-registry/types.ts' import { Synapse } from '../synapse.ts' import { makeDataSetCreatedLog } from './mocks/events.ts' import { ADDRESSES, JSONRPC, PRIVATE_KEYS, presets } from './mocks/jsonrpc/index.ts' +import type { ServiceProviderInfoView } from './mocks/jsonrpc/service-registry.ts' +import { mockServiceProviderRegistry } from './mocks/jsonrpc/service-registry.ts' import { createDataSetHandler, dataSetCreationStatusHandler, type PDPMockOptions } from './mocks/pdp/handlers.ts' import { PING } from './mocks/ping.ts' @@ -266,10 +271,7 @@ describe('Synapse', () => { }, payments: { ...presets.basic.payments, - operatorApprovals: (args) => { - const token = args[0] - const client = args[1] - const operator = args[2] + operatorApprovals: ([token, client, operator]) => { assert.equal(token, ADDRESSES.calibration.usdfcToken) assert.equal(client, signerAddress) assert.equal(operator, ADDRESSES.calibration.warmStorage) @@ -282,9 +284,7 @@ describe('Synapse', () => { BigInt(28800), // maxLockupPeriod ] }, - accounts: (args) => { - const token = args[0] - const user = args[1] + accounts: ([token, user]) => { assert.equal(user, signerAddress) assert.equal(token, ADDRESSES.calibration.usdfcToken) return [BigInt(127001 * 635000000), BigInt(0), BigInt(0), BigInt(0)] @@ -746,4 +746,270 @@ describe('Synapse', () => { } }) }) + + describe('createContexts', () => { + let synapse: Synapse + const mockProviders: ServiceProviderInfoView[] = [ + { + providerId: 1n, + info: { + serviceProvider: ADDRESSES.serviceProvider1, + payee: ADDRESSES.serviceProvider1, + name: 'serviceProvider1', + description: 'mockProviders[0]', + isActive: true, + }, + }, + { + providerId: 2n, + info: { + serviceProvider: ADDRESSES.serviceProvider2, + payee: ADDRESSES.serviceProvider2, + name: 'serviceProvider2', + description: 'mockProviders[1]', + isActive: true, + }, + }, + ] + const offering1: PDPOffering = { + serviceURL: 'http://serviceProvider1.com', + minPieceSizeInBytes: 0n, + maxPieceSizeInBytes: 1n << 32n, + ipniPiece: false, + ipniIpfs: false, + storagePricePerTibPerDay: 1000n, + minProvingPeriodInEpochs: 3n, + location: 'narnia', + paymentTokenAddress: ADDRESSES.calibration.usdfcToken, + } + const offering2: PDPOffering = { + serviceURL: 'http://serviceProvider2.org', + minPieceSizeInBytes: 0n, + maxPieceSizeInBytes: 1n << 32n, + ipniPiece: false, + ipniIpfs: false, + storagePricePerTibPerDay: 1000n, + minProvingPeriodInEpochs: 3n, + location: 'krypton', + paymentTokenAddress: ADDRESSES.calibration.usdfcToken, + } + + const mockPDPProducts: PDPServiceInfo[] = [ + { + offering: offering1, + capabilities: capabilitiesListToObject(...encodePDPCapabilities(offering1)), + isActive: true, + }, + { + offering: offering2, + capabilities: capabilitiesListToObject(...encodePDPCapabilities(offering2)), + isActive: true, + }, + ] + + beforeEach(async () => { + server.use( + JSONRPC({ + ...presets.basic, + serviceRegistry: mockServiceProviderRegistry(mockProviders, mockPDPProducts), + }) + ) + synapse = await Synapse.create({ signer }) + for (const { offering } of mockPDPProducts) { + const pdpOptions: PDPMockOptions = { + baseUrl: offering.serviceURL, + } + server.use(PING(pdpOptions)) + } + }) + + it('selects specified providerIds', async () => { + const contexts = await synapse.storage.createContexts({ + providerIds: [mockProviders[0].providerId, mockProviders[1].providerId].map(Number), + }) + assert.equal(contexts.length, 2) + assert.equal(BigInt(contexts[0].provider.id), mockProviders[0].providerId) + assert.equal(BigInt(contexts[1].provider.id), mockProviders[1].providerId) + // should create new data sets + assert.equal((contexts[0] as any)._dataSetId, undefined) + assert.equal((contexts[1] as any)._dataSetId, undefined) + }) + + it('uses existing data set specified by providerId when metadata matches', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + providerIds: [mockProviders[0].providerId].map(Number), + metadata, + count: 1, + }) + assert.equal(contexts.length, 1) + assert.equal(BigInt(contexts[0].provider.id), mockProviders[0].providerId) + // should use existing data set + assert.equal((contexts[0] as any)._dataSetId, 1n) + }) + + it('force creates new data set specified by providerId even when metadata matches', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + providerIds: [mockProviders[0].providerId].map(Number), + metadata, + count: 1, + forceCreateDataSets: true, + }) + assert.equal(contexts.length, 1) + assert.equal(BigInt(contexts[0].provider.id), mockProviders[0].providerId) + // should create new data set + assert.equal((contexts[0] as any)._dataSetId, undefined) + }) + + it('fails when provided an invalid providerId', async () => { + try { + await synapse.storage.createContexts({ + providerIds: [3, 4], + }) + assert.fail('Expected createContexts to fail for invalid specified providerIds') + } catch (error: any) { + assert.include(error.message, 'Provider does not exist') + } + }) + + it('selects providers specified by data set id', async () => { + const contexts1 = await synapse.storage.createContexts({ + count: 1, + dataSetIds: [1], + }) + assert.equal(contexts1.length, 1) + assert.equal(contexts1[0].provider.id, 1) + assert.equal((contexts1[0] as any)._dataSetId, 1n) + }) + + it('fails when provided an invalid data set id', async () => { + for (const dataSetId of [0, 2]) { + try { + await synapse.storage.createContexts({ + count: 1, + dataSetIds: [dataSetId], + }) + assert.fail('Expected createContexts to fail for invalid specified data set id') + } catch (error: any) { + assert.equal( + error?.message, + `StorageContext resolveByDataSetId failed: Data set ${dataSetId} not found, not owned by ${ADDRESSES.client1}, or not managed by the current WarmStorage contract` + ) + } + } + }) + + it('does not create multiple contexts for the same data set from duplicate dataSetIds', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + count: 2, + dataSetIds: [1, 1], + metadata, + }) + assert.equal(contexts.length, 2) + assert.equal((contexts[0] as any)._dataSetId, 1) + assert.notEqual((contexts[0] as any)._dataSetId, (contexts[1] as any)._dataSetId) + // should also use different providers in this case + assert.notEqual(contexts[0].provider.id, contexts[1].provider.id) + }) + + it('does not create multiple contexts for the same data set from duplicate providerIds', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + count: 2, + providerIds: [mockProviders[0].providerId, mockProviders[0].providerId].map(Number), + metadata, + }) + assert.equal(contexts.length, 2) + assert.equal((contexts[0] as any)._dataSetId, 1) + assert.notEqual((contexts[0] as any)._dataSetId, (contexts[1] as any)._dataSetId) + }) + + it('does not create multiple contexts for a specified data set when providerId also provided', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + count: 2, + dataSetIds: [1, 1], + providerIds: [mockProviders[0].providerId, mockProviders[0].providerId].map(Number), + metadata, + }) + assert.equal(contexts.length, 2) + assert.equal((contexts[0] as any)._dataSetId, 1) + assert.notEqual((contexts[0] as any)._dataSetId, (contexts[1] as any)._dataSetId) + }) + + it('selects existing data set by default when metadata matches', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + count: 1, + metadata, + }) + assert.equal(contexts.length, 1) + assert.equal(contexts[0].provider.id, 1) + assert.equal((contexts[0] as any)._dataSetId, 1n) + }) + + it('avoids existing data set when provider is excluded even when metadata matches', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + count: 1, + metadata, + excludeProviderIds: [1], + }) + assert.equal(contexts.length, 1) + assert.notEqual(contexts[0].provider.id, 1) + }) + + it('creates new data set context when forced even when metadata matches', async () => { + const metadata = { + environment: 'test', + withCDN: '', + } + const contexts = await synapse.storage.createContexts({ + count: 1, + metadata, + forceCreateDataSets: true, + }) + assert.equal(contexts.length, 1) + assert.equal((contexts[0] as any)._dataSetId, undefined) + }) + + it('can select new data sets from different providers using default params', async () => { + const contexts = await synapse.storage.createContexts() + assert.equal(contexts.length, 2) + assert.equal((contexts[0] as any)._dataSetId, undefined) + assert.equal((contexts[1] as any)._dataSetId, undefined) + assert.notEqual(contexts[0].provider.id, contexts[1].provider.id) + }) + + it('can attempt to create numerous contexts, returning fewer', async () => { + const contexts = await synapse.storage.createContexts({ + count: 100, + }) + assert.equal(contexts.length, 2) + assert.notEqual(contexts[0].provider.id, contexts[1].provider.id) + }) + }) }) diff --git a/packages/synapse-sdk/src/test/test-utils.ts b/packages/synapse-sdk/src/test/test-utils.ts index ffb772f88..14244bda5 100644 --- a/packages/synapse-sdk/src/test/test-utils.ts +++ b/packages/synapse-sdk/src/test/test-utils.ts @@ -19,6 +19,7 @@ import { bytesToHex, type Hex, numberToBytes, stringToHex } from 'viem' import type { SPRegistryService } from '../sp-registry/index.ts' import type { ProviderInfo } from '../sp-registry/types.ts' import { CONTRACT_ABIS, CONTRACT_ADDRESSES, SIZE_CONSTANTS, TIME_CONSTANTS } from '../utils/constants.ts' +import { ADDRESSES } from './mocks/jsonrpc/index.ts' /** * Addresses used by testing @@ -150,7 +151,7 @@ export function createMockProvider(chainId: number = 314159): ethers.Provider { } // serviceProviderRegistry() - function selector: 0x05f892ec if (data?.startsWith('0x05f892ec') === true) { - return ethers.AbiCoder.defaultAbiCoder().encode(['address'], ['0x0000000000000000000000000000000000000001']) + return ethers.AbiCoder.defaultAbiCoder().encode(['address'], [ADDRESSES.calibration.spRegistry]) } // sessionKeyRegistry() - function selector: 0x9f6aa572 if (data?.startsWith('0x9f6aa572') === true) { @@ -458,7 +459,7 @@ export function createCustomMulticall3Mock( customAddresses?.usdfcToken ?? CONTRACT_ADDRESSES.USDFC.calibration, // usdfcToken customAddresses?.filCDN ?? '0x0000000000000000000000000000000000000000', // filCDN (not used) customAddresses?.viewContract ?? MOCK_ADDRESSES.WARM_STORAGE_VIEW, // viewContract - customAddresses?.spRegistry ?? '0x0000000000000000000000000000000000000001', // spRegistry + customAddresses?.spRegistry ?? ADDRESSES.calibration.spRegistry, // spRegistry customAddresses?.sessionKeyRegistry ?? MOCK_ADDRESSES.SESSION_KEY_REGISTRY, // sessionKeyRegistry ] @@ -624,10 +625,7 @@ export function setupProviderRegistryMocks( if (callData.startsWith('0xab2b3ae5')) { return { success: true, - returnData: ethers.AbiCoder.defaultAbiCoder().encode( - ['address'], - ['0x0000000000000000000000000000000000000001'] - ), + returnData: ethers.AbiCoder.defaultAbiCoder().encode(['address'], [ADDRESSES.calibration.spRegistry]), } } } @@ -645,7 +643,7 @@ export function setupProviderRegistryMocks( // Mock getProvider(uint256) calls to SPRegistry // Check if it's to the SPRegistry address - if (callData.startsWith('0x5c42d079') && target === '0x0000000000000000000000000000000000000001') { + if (callData.startsWith('0x5c42d079') && target === ADDRESSES.calibration.spRegistry) { const providerId = parseInt(callData.slice(10, 74), 16) const provider = providers.find((p) => p.id === providerId) if (provider) { @@ -679,7 +677,7 @@ export function setupProviderRegistryMocks( } // Mock getProviderWithProduct(uint256, uint8) calls to SPRegistry - if (callData.startsWith('0xadd33358') && target === '0x0000000000000000000000000000000000000001') { + if (callData.startsWith('0xadd33358') && target === ADDRESSES.calibration.spRegistry) { const providerId = parseInt(callData.slice(10, 74), 16) const provider = providers.find((p) => p.id === providerId) diff --git a/packages/synapse-sdk/src/types.ts b/packages/synapse-sdk/src/types.ts index a912bd18b..55e2dde01 100644 --- a/packages/synapse-sdk/src/types.ts +++ b/packages/synapse-sdk/src/types.ts @@ -300,6 +300,36 @@ export interface StorageContextCallbacks { onDataSetResolved?: (info: { isExisting: boolean; dataSetId: number; provider: ProviderInfo }) => void } +export interface CreateContextsOptions { + /** Number of contexts to create (optional, defaults to 2) */ + count?: number + /** + * Specific data set IDs to use + */ + dataSetIds?: number[] + /** + * Specific provider IDs to use + */ + providerIds?: number[] + /** Do not select any of these providers */ + excludeProviderIds?: number[] + /** Whether to enable CDN services */ + withCDN?: boolean + withIpni?: boolean + dev?: boolean + /** + * Custom metadata for the data sets (key-value pairs) + * When smart-selecting data sets, this metadata will be used to match. + */ + metadata?: Record + /** Create new data sets, even if candidates exist */ + forceCreateDataSets?: boolean + /** Callbacks for creation process (will need to change to handle multiples) */ + callbacks?: StorageContextCallbacks + /** Maximum number of uploads to process in a single batch (default: 32, minimum: 1) */ + uploadBatchSize?: number +} + /** * Options for creating or selecting a storage context * @@ -535,8 +565,6 @@ export interface ProviderSelectionResult { provider: ProviderInfo /** Selected data set ID */ dataSetId: number - /** Whether this is a new data set that was created */ - isNewDataSet?: boolean /** Whether this is an existing data set */ isExisting?: boolean /** Data set metadata */