Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/synapse-sdk/src/storage/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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,
Expand Down Expand Up @@ -172,6 +173,56 @@ export class StorageContext {
this._pdpServer = new PDPServer(authHelper, provider.products.PDP.data.serviceURL)
}

static async createContexts(
synapse: Synapse,
warmStorageService: WarmStorageService,
options: CreateContextsOptions = {}
): Promise<StorageContext[]> {
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)
const providerResolver = new ProviderResolver(warmStorageService, spRegistry)
if (options?.providerIds) {
for (const providerId of options.providerIds) {
const resolution = await StorageContext.resolveByProviderId(
clientAddress,
providerId,
options.metadata ?? {},
warmStorageService,
providerResolver,
options.forceCreateDataSets
)
resolutions.push(resolution)
if (resolutions.length >= count) {
break
}
}
} else if (options?.providerAddresses) {
for (const providerAddress of options.providerAddresses) {
const resolution = await StorageContext.resolveByProviderAddress(
providerAddress,
warmStorageService,
providerResolver,
clientAddress,
options.metadata ?? {},
options.forceCreateDataSets
)
resolutions.push(resolution)
if (resolutions.length >= count) {
break
}
}
}
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
Expand All @@ -194,6 +245,15 @@ export class StorageContext {
options
)

return await StorageContext.createWithSelectedProvider(resolution, synapse, warmStorageService, options)
}

static async createWithSelectedProvider(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're exposting this, we're essentially exposing ProviderSelectionResult as a public type that someone might use. So we should look at this flow a little more critically.

  • We use -1 to signal that a new dataSet needs to be made, but we could also just omit it for that case I think.
  • The type has a bizzaro isNewDataSet that's not used anywhere. Let's remove that.
  • isExisting is only used for the callback, but also is now overloading our use of -1 in dataSetId, so we don't really need it now either
  • The type really is a combination of Provider+DataSet (which is what a Context is supposed to be structured around), so how about we rename it.

Here's an interesting option, make it an "intent" for how we want the context to be:

  interface StorageContextIntent {
    provider: ProviderInfo
    dataSetIntent:
      | { action: 'create', metadata: Record<string, string> }
      | { action: 'reuse', id: number, metadata: Record<string, string> }
  }

Simple variation that's not to different to now:

  interface ProviderDataSetSelection {
    provider: ProviderInfo
    dataSetId?: number  // undefined = needs creation
    dataSetMetadata: Record<string, string>
  }

But I think I like the explicit descriptiveness of an intent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to make this method private

resolution: ProviderSelectionResult,
synapse: Synapse,
warmStorageService: WarmStorageService,
options: StorageServiceOptions = {}
): Promise<StorageContext> {
// Notify callback about provider selection
try {
options.callbacks?.onProviderSelected?.(resolution.provider)
Expand Down
10 changes: 10 additions & 0 deletions packages/synapse-sdk/src/storage/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -231,6 +232,15 @@ export class StorageManager {
return await StorageContext.performPreflightCheck(this._warmStorageService, this._synapse.payments, size, withCDN)
}

async createContexts(options?: CreateContextsOptions): Promise<StorageContext[]> {
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
*/
Expand Down
82 changes: 82 additions & 0 deletions packages/synapse-sdk/src/test/mocks/jsonrpc/service-registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** biome-ignore-all lint/style/noNonNullAssertion: testing */

import type { ExtractAbiFunction } from 'abitype'
import { assert } from 'chai'
import { decodeFunctionData, encodeAbiParameters, type Hex } from 'viem'
import { CONTRACT_ABIS } from '../../../utils/constants.ts'
import type { AbiToType, JSONRPCOptions } from './types.ts'
Expand Down Expand Up @@ -28,6 +29,87 @@ export interface ServiceRegistryOptions {
getProvider?: (args: AbiToType<getProvider['inputs']>) => AbiToType<getProvider['outputs']>
}

export type ServiceProviderInfoView = AbiToType<getProvider['outputs']>[0]
export type PDPServiceInfoView = AbiToType<getPDPService['outputs']>

const EMPTY_PROVIDER: ServiceProviderInfoView = {
providerId: 0n,
info: {
serviceProvider: '0x0000000000000000000000000000000000000000',
payee: '0x0000000000000000000000000000000000000000',
name: '',
description: '',
isActive: false,
},
}

const EMPTY_PDP_SERVICE: PDPServiceInfoView = [
{
serviceURL: '',
minPieceSizeInBytes: 0n,
maxPieceSizeInBytes: 0n,
ipniPiece: false,
ipniIpfs: false,
storagePricePerTibPerMonth: 0n,
minProvingPeriodInEpochs: 0n,
location: '',
paymentTokenAddress: '0x0000000000000000000000000000000000000000',
},
[], // capabilityKeys
false, // isActive
]

export function mockServiceProviderRegistry(
providers: ServiceProviderInfoView[],
services?: (PDPServiceInfoView | 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')
},
getPDPService: ([providerId]) => {
if (!services) {
return EMPTY_PDP_SERVICE
}
for (let i = 0; i < services.length; i++) {
if (providers[i].providerId === providerId) {
const service = services[i]
if (service == null) {
return EMPTY_PDP_SERVICE
}
return service
}
}
return EMPTY_PDP_SERVICE
},
getProviderByAddress: ([address]) => {
for (const provider of providers) {
if (address === provider.info.serviceProvider) {
return [provider]
}
}
return [EMPTY_PROVIDER]
},
getProviderIdByAddress: ([address]) => {
for (const provider of providers) {
if (address === provider.info.serviceProvider) {
return [provider.providerId]
}
}
return [0n]
},
}
}

/**
* Handle service provider registry calls
*/
Expand Down
31 changes: 15 additions & 16 deletions packages/synapse-sdk/src/test/sp-registry-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ describe('SPRegistryService', () => {
return {
providerId: 1,
info: {
id: BigInt(1),
serviceProvider: mockProviderAddress,
payee: mockProviderAddress,
name: 'Test Provider',
Expand All @@ -64,26 +63,13 @@ describe('SPRegistryService', () => {
}
throw new Error('Provider not found')
},
getProviderProducts: async (id: number) => {
if (id === 1) {
return [
{
productType: 0, // PDP
isActive: true,
capabilityKeys: [],
productData: '0x', // Encoded PDP offering
},
]
}
return []
},
providerHasProduct: async (id: number, productType: number) => {
return id === 1 && productType === 0
},
getPDPService: async (id: number) => {
if (id === 1) {
return {
offering: {
pdpOffering: {
serviceURL: 'https://provider.example.com',
minPieceSizeInBytes: SIZE_CONSTANTS.KiB,
maxPieceSizeInBytes: SIZE_CONSTANTS.GiB,
Expand Down Expand Up @@ -415,7 +401,20 @@ describe('SPRegistryService', () => {
// Override to return provider without products
;(service as any)._getRegistryContract = () => ({
...createMockContract(),
getProviderProducts: async () => [],
getPDPService: async () => ({
pdpOffering: {
serviceURL: '',
minPieceSizeInBytes: 0,
maxPieceSizeInBytes: 0,
ipniPiece: false,
ipniIpfs: false,
minProvingPeriodInEpochs: 0,
storagePricePerTibPerMonth: 0,
location: '',
},
capabilityKeys: [],
isActive: false,
}),
})

const provider = await service.getProvider(1)
Expand Down
3 changes: 2 additions & 1 deletion packages/synapse-sdk/src/test/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StorageContext } from '../storage/context.ts'
import type { Synapse } from '../synapse.ts'
import type { PieceCID, ProviderInfo, UploadResult } from '../types.ts'
import { SIZE_CONSTANTS } from '../utils/constants.ts'
import { ADDRESSES } from './mocks/jsonrpc/index.ts'
import { createMockProviderInfo, createSimpleProvider, setupProviderRegistryMocks } from './test-utils.ts'

// Create a mock Ethereum provider that doesn't try to connect
Expand Down Expand Up @@ -143,7 +144,7 @@ function createMockWarmStorageService(providers: ProviderInfo[], dataSets: any[]
return provider?.id ?? 0
},
getApprovedProvider: async (id: number) => providers.find((p) => p.id === id) ?? null,
getServiceProviderRegistryAddress: () => '0x0000000000000000000000000000000000000001',
getServiceProviderRegistryAddress: () => ADDRESSES.calibration.spRegistry,
getApprovedProviderIds: async () => providers.map((p) => p.id),
isProviderIdApproved: async (id: number) => providers.some((p) => p.id === id),
getDataSetMetadata: async (dataSetId: number) => {
Expand Down
Loading
Loading