UNPKG

defarm-sdk

Version:

DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain

293 lines (254 loc) 8.09 kB
import type { TokenMetadata } from './TokenizationService'; export interface IPFSGatewayConfig { baseUrl: string; apiKey: string; timeout?: number; retries?: number; } export interface IPFSUploadResult { success: boolean; cid: string; url: string; size?: number; error?: string; } export interface IPFSPinResult { success: boolean; cid: string; pinned: boolean; error?: string; } /** * IPFS Gateway service for uploading asset metadata * Uses DeFarm's IPFS gateway with our API key */ export class IPFSGateway { private config: IPFSGatewayConfig; constructor(config: IPFSGatewayConfig) { this.config = { timeout: 30000, retries: 3, ...config }; } /** * Upload asset metadata to IPFS */ async uploadMetadata(metadata: TokenMetadata): Promise<IPFSUploadResult> { try { const jsonData = JSON.stringify(metadata, null, 2); const uploadResult = await this.uploadJson(jsonData, `${metadata.dfid}_metadata.json`); if (uploadResult.success) { // Pin the content to ensure persistence await this.pinContent(uploadResult.cid); } return uploadResult; } catch (error) { return { success: false, cid: '', url: '', error: error instanceof Error ? error.message : 'Unknown IPFS upload error' }; } } /** * Upload JSON data to IPFS */ async uploadJson(jsonData: string, fileName?: string): Promise<IPFSUploadResult> { const formData = new FormData(); const blob = new Blob([jsonData], { type: 'application/json' }); formData.append('file', blob, fileName || 'metadata.json'); return this.makeRequest('/api/v0/add', { method: 'POST', body: formData }); } /** * Upload file buffer to IPFS */ async uploadFile(fileBuffer: Buffer, fileName: string, contentType?: string): Promise<IPFSUploadResult> { const formData = new FormData(); const blob = new Blob([fileBuffer], { type: contentType || 'application/octet-stream' }); formData.append('file', blob, fileName); return this.makeRequest('/api/v0/add', { method: 'POST', body: formData }); } /** * Pin content to IPFS to ensure persistence */ async pinContent(cid: string): Promise<IPFSPinResult> { try { const response = await this.makeRequest(`/api/v0/pin/add?arg=${cid}`, { method: 'POST' }); return { success: true, cid, pinned: true }; } catch (error) { return { success: false, cid, pinned: false, error: error instanceof Error ? error.message : 'Unknown pin error' }; } } /** * Get content from IPFS */ async getContent(cid: string): Promise<{ success: boolean; content?: string; error?: string; }> { try { const response = await fetch(`${this.config.baseUrl}/ipfs/${cid}`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.config.apiKey}`, 'Accept': 'application/json' }, signal: AbortSignal.timeout(this.config.timeout || 30000) }); if (!response.ok) { throw new Error(`IPFS get failed: ${response.status} ${response.statusText}`); } const content = await response.text(); return { success: true, content }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown IPFS get error' }; } } /** * Test connection to IPFS gateway */ async testConnection(): Promise<boolean> { try { const testData = { test: true, timestamp: Date.now() }; const result = await this.uploadJson(JSON.stringify(testData), 'connection_test.json'); return result.success; } catch (error) { console.error('IPFS gateway connection test failed:', error); return false; } } /** * Get gateway status and stats */ async getGatewayStats(): Promise<{ success: boolean; stats?: { totalStorage: number; totalFiles: number; pinnedFiles: number; }; error?: string; }> { try { const response = await this.makeRequest('/api/v0/stats/repo', { method: 'POST' }); return { success: true, stats: { totalStorage: response.RepoSize || 0, totalFiles: response.NumObjects || 0, pinnedFiles: response.NumObjects || 0 // Approximation } }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown stats error' }; } } private async makeRequest(endpoint: string, options: RequestInit = {}): Promise<any> { let lastError: Error | null = null; for (let attempt = 0; attempt <= (this.config.retries || 3); attempt++) { try { const url = `${this.config.baseUrl}${endpoint}`; const requestOptions: RequestInit = { ...options, headers: { 'Authorization': `Bearer ${this.config.apiKey}`, ...options.headers }, signal: AbortSignal.timeout(this.config.timeout || 30000) }; const response = await fetch(url, requestOptions); if (!response.ok) { if (response.status === 429 && attempt < (this.config.retries || 3)) { const retryAfter = response.headers.get('Retry-After'); const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000; await this.delay(waitTime); continue; } const errorData = await response.text().catch(() => ''); throw new Error(`IPFS API request failed: ${response.status} ${response.statusText}. ${errorData}`); } // Handle different response types const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { const jsonResponse = await response.json(); // Handle IPFS add response format if (endpoint.includes('/add') && jsonResponse.Hash) { return { success: true, cid: jsonResponse.Hash, url: `${this.config.baseUrl}/ipfs/${jsonResponse.Hash}`, size: jsonResponse.Size }; } return jsonResponse; } else { const textResponse = await response.text(); // Try to parse as JSON if it looks like JSON if (textResponse.trim().startsWith('{') || textResponse.trim().startsWith('[')) { try { const jsonResponse = JSON.parse(textResponse); // Handle IPFS add response format if (endpoint.includes('/add') && jsonResponse.Hash) { return { success: true, cid: jsonResponse.Hash, url: `${this.config.baseUrl}/ipfs/${jsonResponse.Hash}`, size: jsonResponse.Size }; } return jsonResponse; } catch (parseError) { // If JSON parsing fails, return as text return { response: textResponse }; } } return { response: textResponse }; } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (error instanceof Error && error.name === 'AbortError') { lastError = new Error(`IPFS request timeout after ${this.config.timeout}ms`); } if (attempt < (this.config.retries || 3)) { await this.delay(Math.pow(2, attempt) * 1000); continue; } } } throw lastError || new Error('Unknown IPFS API error'); } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } }