UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

317 lines (275 loc) 9.75 kB
import type { EnhancedDataSetInfo, ProviderInfo } from '@filoz/synapse-sdk' import { PDPServer, PDPVerifier, RPC_URLS, Synapse, WarmStorageService } from '@filoz/synapse-sdk' import pc from 'picocolors' import { cleanupProvider } from '../synapse/service.js' import { cancel, createSpinner, intro, outro } from '../utils/cli-helpers.js' import { log } from '../utils/cli-logger.js' import { displayDataSetList, displayDataSetStatus } from './inspect.js' import type { DataSetCommandOptions, DataSetDetail, DataSetInspectionContext, PieceDetail } from './types.js' /** * Fetch piece-level metadata for a dataset without triggering PDP lookups. * * @param params - Context required to inspect the piece */ async function collectPieceDetail(params: { dataSetId: number pieceId: number pieceCid: string warmStorage: WarmStorageService }): Promise<PieceDetail> { const { dataSetId, pieceId, pieceCid, warmStorage } = params const metadata = await warmStorage.getPieceMetadata(dataSetId, pieceId).catch(() => ({}) as Record<string, string>) const pieceDetail: PieceDetail = { pieceId, pieceCid, metadata: { ...metadata }, } return pieceDetail } /** * Build the lightweight inspection context used when no dataset ID is provided. * Only metadata cached in Synapse responses is included so the command returns quickly. */ function buildSummaryContext(params: { address: string network: string dataSets: EnhancedDataSetInfo[] providers: ProviderInfo[] | null }): DataSetInspectionContext { const providerMap = new Map<number, ProviderInfo>() if (params.providers != null) { for (const provider of params.providers) { providerMap.set(provider.id, provider) } } const managedDataSets = params.dataSets.filter((dataSet) => dataSet.metadata?.source === 'filecoin-pin') const dataSetDetails: DataSetDetail[] = managedDataSets.map((dataSet) => { const detail: DataSetDetail = { base: dataSet, metadata: { ...(dataSet.metadata ?? {}) }, pieces: [], warnings: [], } const provider = providerMap.get(dataSet.providerId) if (provider != null) { detail.provider = provider } return detail }) return { address: params.address, network: params.network, dataSets: dataSetDetails, } } /** * Enrich a summary dataset entry with live information from WarmStorage and PDP. * * Populates leaf counts, total size estimates, piece metadata, and warning messages. */ async function loadDetailedDataSet(detail: DataSetDetail, synapse: Synapse): Promise<DataSetDetail> { const result: DataSetDetail = { base: detail.base, metadata: { ...detail.metadata }, pieces: detail.pieces.map((piece) => ({ ...piece, metadata: { ...piece.metadata }, })), warnings: [...detail.warnings], } if (detail.provider != null) { result.provider = detail.provider } if (detail.leafCount != null) { result.leafCount = detail.leafCount } if (detail.totalSizeBytes != null) { result.totalSizeBytes = detail.totalSizeBytes } let warmStorage: WarmStorageService try { warmStorage = await WarmStorageService.create(synapse.getProvider(), synapse.getWarmStorageAddress()) } catch (error) { result.warnings.push( `Unable to initialize Warm Storage service: ${error instanceof Error ? error.message : String(error)}` ) return result } const pdpVerifier = new PDPVerifier(synapse.getProvider(), warmStorage.getPDPVerifierAddress()) try { const rawLeafCount = await pdpVerifier.getDataSetLeafCount(result.base.pdpVerifierDataSetId) const leafCount = BigInt(rawLeafCount) result.leafCount = leafCount result.totalSizeBytes = leafCount * 32n } catch (error) { result.warnings.push(`Unable to fetch leaf count: ${error instanceof Error ? error.message : String(error)}`) } const serviceURL = result.provider?.products?.PDP?.data?.serviceURL if (serviceURL == null || serviceURL.trim() === '') { result.warnings.push('Provider does not expose a PDP service URL; piece details unavailable') return result } try { const pdpServer = new PDPServer(null, serviceURL) const dataSetData = await pdpServer.getDataSet(result.base.pdpVerifierDataSetId) const pieces = await Promise.all( dataSetData.pieces.map(async (piece) => { try { return await collectPieceDetail({ dataSetId: result.base.pdpVerifierDataSetId, pieceId: piece.pieceId, pieceCid: piece.pieceCid.toString(), warmStorage, }) } catch (error) { result.warnings.push( `Failed to inspect piece #${piece.pieceId}: ${error instanceof Error ? error.message : String(error)}` ) return { pieceId: piece.pieceId, pieceCid: piece.pieceCid.toString(), metadata: {}, } } }) ) pieces.sort((a, b) => a.pieceId - b.pieceId) result.pieces = pieces } catch (error) { result.warnings.push(`Failed to fetch piece list: ${error instanceof Error ? error.message : String(error)}`) } return result } /** * Resolve the private key from CLI options or environment variables. * * @throws Never returns when the key cannot be resolved; exits the process instead. */ async function ensurePrivateKey(options: DataSetCommandOptions): Promise<string> { const privateKey = options.privateKey ?? process.env.PRIVATE_KEY if (!privateKey) { log.line(pc.red('Error: Private key required via --private-key or PRIVATE_KEY env')) log.flush() cancel('Data set inspection cancelled') process.exit(1) } return privateKey } /** * Resolve the RPC endpoint, preferring CLI options over environment defaults. */ function resolveRpcUrl(options: DataSetCommandOptions): string { return options.rpcUrl ?? process.env.RPC_URL ?? RPC_URLS.calibration.websocket } /** * Entry point invoked by the Commander command. * * @param dataSetIdArg - Optional dataset identifier provided on the command line * @param options - Normalised CLI options */ export async function runDataSetCommand( dataSetIdArg: string | undefined, options: DataSetCommandOptions ): Promise<void> { const dataSetIdInput = dataSetIdArg ?? null const hasDataSetId = dataSetIdInput != null const shouldList = options.ls === true || !hasDataSetId const privateKey = await ensurePrivateKey(options) const rpcUrl = resolveRpcUrl(options) intro(pc.bold('Filecoin Onchain Cloud Data Sets')) const spinner = createSpinner() spinner.start('Connecting to Synapse...') let synapse: Synapse | null = null let provider: any = null try { synapse = await Synapse.create({ privateKey, rpcURL: rpcUrl }) const network = synapse.getNetwork() const signer = synapse.getSigner() const address = await signer.getAddress() if (/^wss?:\/\//i.test(rpcUrl)) { provider = synapse.getProvider() } spinner.message('Fetching data set information...') const [dataSets, storageInfo] = await Promise.all([ synapse.storage.findDataSets(address), synapse.storage.getStorageInfo().catch(() => null), ]) const context = buildSummaryContext({ address, network, dataSets, providers: storageInfo?.providers ?? null, }) if (hasDataSetId) { const dataSetId = Number.parseInt(dataSetIdInput, 10) if (Number.isNaN(dataSetId)) { spinner.stop('━━━ Data Sets ━━━') log.line(pc.red(`Invalid data set ID: ${dataSetIdInput}`)) log.flush() cancel('Invalid arguments') process.exitCode = 1 return } const targetIndex = context.dataSets.findIndex((item) => item.base.pdpVerifierDataSetId === dataSetId) if (targetIndex === -1) { spinner.stop('━━━ Data Sets ━━━') cancel('Data set not found') process.exitCode = 1 return } spinner.message('Collecting data set details...') const baseDetail = context.dataSets[targetIndex] if (baseDetail == null) { spinner.stop('━━━ Data Sets ━━━') cancel('Data set not found') process.exitCode = 1 return } const detailed = await loadDetailedDataSet(baseDetail, synapse) context.dataSets[targetIndex] = detailed spinner.stop('━━━ Data Sets ━━━') if (shouldList) { const filteredContext: DataSetInspectionContext = { ...context, dataSets: context.dataSets.filter((entry, index) => { if (entry.base.pdpVerifierDataSetId === dataSetId) { return false } if (index === targetIndex) { return false } return true }), } if (filteredContext.dataSets.length > 0) { displayDataSetList(filteredContext) log.line('') log.flush() } log.line('') log.flush() } const found = displayDataSetStatus(context, dataSetId) if (!found) { cancel('Data set not found') process.exitCode = 1 return } } else { spinner.stop('━━━ Data Sets ━━━') if (shouldList) { displayDataSetList(context) } } outro('Data set inspection complete') } catch (error) { spinner.stop(`${pc.red('✗')} Failed to inspect data sets`) log.line('') log.line(`${pc.red('Error:')} ${error instanceof Error ? error.message : String(error)}`) log.flush() cancel('Inspection failed') process.exitCode = 1 } finally { await cleanupProvider(provider) } }