filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
387 lines (351 loc) • 12.5 kB
text/typescript
import {
METADATA_KEYS,
type ProviderInfo,
RPC_URLS,
type StorageContext,
type StorageServiceOptions,
Synapse,
type SynapseOptions,
} from '@filoz/synapse-sdk'
import type { Logger } from 'pino'
import type { Config } from '../config.js'
/**
* Default metadata for Synapse data sets created by filecoin-pin
*/
const DEFAULT_DATA_SET_METADATA = {
[METADATA_KEYS.WITH_IPFS_INDEXING]: '', // Enable IPFS indexing for all data sets
source: 'filecoin-pin', // Identify the source application
} as const
/**
* Default configuration for creating storage contexts
*/
const DEFAULT_STORAGE_CONTEXT_CONFIG = {
withCDN: false, // CDN not needed for Filecoin Pin currently
metadata: DEFAULT_DATA_SET_METADATA,
} as const
let synapseInstance: Synapse | null = null
let storageInstance: StorageContext | null = null
let currentProviderInfo: ProviderInfo | null = null
let activeProvider: any = null // Track the provider for cleanup
/**
* Reset the service instances (for testing)
*/
export function resetSynapseService(): void {
synapseInstance = null
storageInstance = null
currentProviderInfo = null
activeProvider = null
}
export interface SynapseService {
synapse: Synapse
storage: StorageContext
providerInfo: ProviderInfo
}
/**
* Initialize the Synapse SDK without creating storage context
*
* This function initializes the Synapse SDK connection without creating
* a storage context. This method is primarily a wrapper for handling our
* custom configuration needs and adding detailed logging.
*
* @param config - Application configuration with privateKey and RPC URL
* @param logger - Logger instance for detailed operation tracking
* @returns Initialized Synapse instance
*/
export async function initializeSynapse(config: Config, logger: Logger): Promise<Synapse> {
try {
// Log the configuration status
logger.info(
{
hasPrivateKey: config.privateKey != null,
rpcUrl: config.rpcUrl,
},
'Initializing Synapse'
)
// IMPORTANT: Private key is required for transaction signing
// In production, this should come from secure environment variables, or a wallet integration
if (config.privateKey == null) {
const error = new Error('PRIVATE_KEY environment variable is required for Synapse integration')
logger.error(
{
event: 'synapse.init.failed',
error: error.message,
},
'Synapse initialization failed: missing PRIVATE_KEY'
)
throw error
}
logger.info({ event: 'synapse.init' }, 'Initializing Synapse SDK')
// Configure Synapse with network settings
// Network options: 314 (mainnet) or 314159 (calibration testnet)
const synapseOptions: SynapseOptions = {
privateKey: config.privateKey,
rpcURL: config.rpcUrl ?? RPC_URLS.calibration.websocket, // Default to calibration testnet
}
// Optional: Override the default Warm Storage contract address
// Useful for testing with custom deployments
if (config.warmStorageAddress != null) {
synapseOptions.warmStorageAddress = config.warmStorageAddress
}
const synapse = await Synapse.create(synapseOptions)
// Store reference to the provider for cleanup if it's a WebSocket provider
if (synapseOptions.rpcURL && /^ws(s)?:\/\//i.test(synapseOptions.rpcURL)) {
activeProvider = synapse.getProvider()
}
// Get network info for logging
const network = synapse.getNetwork()
logger.info(
{
event: 'synapse.init',
network,
rpcUrl: synapseOptions.rpcURL,
},
'Synapse SDK initialized'
)
// Store instance for cleanup
synapseInstance = synapse
return synapse
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
logger.error(
{
event: 'synapse.init.failed',
error: errorMessage,
},
`Failed to initialize Synapse SDK: ${errorMessage}`
)
throw error
}
}
/**
* Create storage context for an initialized Synapse instance
*
* This creates a storage context with comprehensive callbacks for tracking
* the data set creation and provider selection process. This is primarily
* a wrapper around the Synapse SDK's storage context creation, adding logging
* and progress callbacks for better observability.
*
* @param synapse - Initialized Synapse instance
* @param logger - Logger instance for detailed operation tracking
* @param progressCallbacks - Optional callbacks for progress tracking
* @returns Storage context and provider information
*/
export async function createStorageContext(
synapse: Synapse,
logger: Logger,
progressCallbacks?: {
onProviderSelected?: (provider: any) => void
onDataSetCreationStarted?: (transaction: any) => void
onDataSetResolved?: (info: { dataSetId: number; isExisting: boolean }) => void
}
): Promise<{ storage: StorageContext; providerInfo: ProviderInfo }> {
try {
// Create storage context with comprehensive event tracking
// The storage context manages the data set and provider interactions
logger.info({ event: 'synapse.storage.create' }, 'Creating storage context')
// Optional override: allow selecting a specific provider via env vars
const envProviderAddress = process.env.PROVIDER_ADDRESS?.trim()
const envProviderIdRaw = process.env.PROVIDER_ID?.trim()
const envProviderId = envProviderIdRaw != null && envProviderIdRaw !== '' ? Number(envProviderIdRaw) : undefined
const createOptions: StorageServiceOptions = {
...DEFAULT_STORAGE_CONTEXT_CONFIG,
// Callbacks provide visibility into the storage lifecycle
// These are crucial for debugging and monitoring in production
callbacks: {
onProviderSelected: (provider) => {
// Store the provider info for later use
currentProviderInfo = provider
logger.info(
{
event: 'synapse.storage.provider_selected',
provider: {
id: provider.id,
serviceProvider: provider.serviceProvider,
name: provider.name,
serviceURL: provider.products?.PDP?.data?.serviceURL,
},
},
'Selected storage provider'
)
// Call progress callback if provided
if (progressCallbacks?.onProviderSelected) {
progressCallbacks.onProviderSelected(provider)
}
},
onDataSetResolved: (info) => {
logger.info(
{
event: 'synapse.storage.data_set_resolved',
dataSetId: info.dataSetId,
isExisting: info.isExisting,
},
info.isExisting ? 'Using existing data set' : 'Created new data set'
)
// Call progress callback if provided
if (progressCallbacks?.onDataSetResolved) {
progressCallbacks.onDataSetResolved(info)
}
},
onDataSetCreationStarted: (transaction, statusUrl) => {
logger.info(
{
event: 'synapse.storage.data_set_creation_started',
txHash: transaction.hash,
statusUrl,
},
'Data set creation transaction submitted'
)
// Call progress callback if provided
if (progressCallbacks?.onDataSetCreationStarted) {
progressCallbacks.onDataSetCreationStarted(transaction)
}
},
onDataSetCreationProgress: (status) => {
logger.info(
{
event: 'synapse.storage.data_set_creation_progress',
transactionMined: status.transactionMined,
dataSetLive: status.dataSetLive,
elapsedMs: status.elapsedMs,
},
'Data set creation progress'
)
},
},
}
// Apply provider override if present
if (envProviderAddress) {
createOptions.providerAddress = envProviderAddress
logger.info(
{ event: 'synapse.storage.provider_override', by: 'env', providerAddress: envProviderAddress },
'Overriding provider via PROVIDER_ADDRESS'
)
} else if (envProviderId != null && Number.isFinite(envProviderId)) {
createOptions.providerId = envProviderId
logger.info(
{ event: 'synapse.storage.provider_override', by: 'env', providerId: envProviderId },
'Overriding provider via PROVIDER_ID'
)
}
const storage = await synapse.storage.createContext(createOptions)
logger.info(
{
event: 'synapse.storage.created',
dataSetId: storage.dataSetId,
serviceProvider: storage.serviceProvider,
},
'Storage context created successfully'
)
// Store instance
storageInstance = storage
// Ensure we always have provider info
if (!currentProviderInfo) {
// This should not happen as provider is selected during context creation
throw new Error('Provider information not available after storage context creation')
}
return { storage, providerInfo: currentProviderInfo }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
logger.error(
{
event: 'synapse.storage.create.failed',
error: errorMessage,
},
`Failed to create storage context: ${errorMessage}`
)
throw error
}
}
/**
* Set up complete Synapse service with SDK and storage context
*
* This function demonstrates the complete setup flow for Synapse:
* 1. Validates required configuration (private key)
* 2. Creates Synapse instance with network configuration
* 3. Creates a storage context with comprehensive callbacks
* 4. Returns a service object for application use
*
* Our wrapping of Synapse initialization and storage context creation is
* primarily to handle our custom configuration needs and add detailed logging
* and progress tracking.
*
* @param config - Application configuration with privateKey and RPC URL
* @param logger - Logger instance for detailed operation tracking
* @param progressCallbacks - Optional callbacks for progress tracking
* @returns SynapseService with initialized Synapse and storage context
*/
export async function setupSynapse(
config: Config,
logger: Logger,
progressCallbacks?: {
onProviderSelected?: (provider: any) => void
onDataSetCreationStarted?: (transaction: any) => void
onDataSetResolved?: (info: { dataSetId: number; isExisting: boolean }) => void
}
): Promise<SynapseService> {
// Initialize SDK
const synapse = await initializeSynapse(config, logger)
// Create storage context
const { storage, providerInfo } = await createStorageContext(synapse, logger, progressCallbacks)
return { synapse, storage, providerInfo }
}
/**
* Get default storage context configuration for consistent data set creation
*
* @param overrides - Optional overrides to merge with defaults
* @returns Storage context configuration with defaults
*/
export function getDefaultStorageContextConfig(overrides: any = {}) {
return {
...DEFAULT_STORAGE_CONTEXT_CONFIG,
...overrides,
metadata: {
...DEFAULT_DATA_SET_METADATA,
...overrides.metadata,
},
}
}
/**
* Clean up a WebSocket provider connection.
* This is important for allowing the Node.js process to exit cleanly.
*
* @param provider - The provider to clean up
*/
export async function cleanupProvider(provider: any): Promise<void> {
if (provider && typeof provider.destroy === 'function') {
try {
await provider.destroy()
} catch {
// Ignore cleanup errors
}
}
}
/**
* Clean up WebSocket providers and other resources
*
* Call this when CLI commands are finishing to ensure proper cleanup
* and allow the process to terminate.
*/
export async function cleanupSynapseService(): Promise<void> {
if (activeProvider) {
await cleanupProvider(activeProvider)
}
// Clear references
synapseInstance = null
storageInstance = null
currentProviderInfo = null
activeProvider = null
}
/**
* Get the initialized Synapse service
*/
export function getSynapseService(): SynapseService | null {
if (synapseInstance == null || storageInstance == null || currentProviderInfo == null) {
return null
}
return {
synapse: synapseInstance,
storage: storageInstance,
providerInfo: currentProviderInfo,
}
}