defarm-sdk
Version:
DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain
293 lines (254 loc) • 8.09 kB
text/typescript
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));
}
}