package ipfs import ( "bytes" "encoding/json" "errors" "fmt" "io" "log" "mime/multipart" "net/http" "os" "path/filepath" "strings" "time" ) const ( // PinataPinFileURL is the Pinata API endpoint for pinning files PinataPinFileURL = "https://api.pinata.cloud/pinning/pinFileToIPFS" // PinataPinJSONURL is the Pinata API endpoint for pinning JSON PinataPinJSONURL = "https://api.pinata.cloud/pinning/pinJSONToIPFS" // PinataTimeout is the timeout for Pinata API calls PinataTimeout = 30 * time.Second ) // IPFSService handles interactions with IPFS via Pinata type IPFSService struct { apiKey string apiSecret string jwt string client *http.Client } // PinataResponse represents a response from the Pinata API type PinataResponse struct { IpfsHash string `json:"IpfsHash"` PinSize int `json:"PinSize"` Timestamp string `json:"Timestamp"` } // PinataMetadata represents metadata for a file pinned to IPFS type PinataMetadata struct { Name string `json:"name"` KeyValues map[string]string `json:"keyvalues"` } // PinataJSONRequest represents a request to pin JSON to IPFS type PinataJSONRequest struct { PinataMetadata PinataMetadata `json:"pinataMetadata"` PinataContent interface{} `json:"pinataContent"` } // NewIPFSService creates a new IPFS service func NewIPFSService() (*IPFSService, error) { apiKey := os.Getenv("PINATA_API_KEY") apiSecret := os.Getenv("PINATA_API_SECRET") jwt := os.Getenv("PINATA_JWT") if (apiKey == "" || apiSecret == "") && jwt == "" { return nil, errors.New("either PINATA_API_KEY and PINATA_API_SECRET or PINATA_JWT must be set") } client := &http.Client{ Timeout: PinataTimeout, } return &IPFSService{ apiKey: apiKey, apiSecret: apiSecret, jwt: jwt, client: client, }, nil } // PinFile uploads a file to IPFS via Pinata func (s *IPFSService) PinFile(filePath string, metadata map[string]string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", fmt.Errorf("failed to open file: %v", err) } defer file.Close() return s.PinFileContent(file, filepath.Base(filePath), metadata) } // PinFileContent uploads file content to IPFS via Pinata func (s *IPFSService) PinFileContent(fileContent io.Reader, fileName string, metadata map[string]string) (string, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) // Add the file part, err := writer.CreateFormFile("file", fileName) if err != nil { return "", fmt.Errorf("failed to create form file: %v", err) } _, err = io.Copy(part, fileContent) if err != nil { return "", fmt.Errorf("failed to copy file content: %v", err) } // Add metadata if provided if metadata != nil && len(metadata) > 0 { metadataJSON, err := json.Marshal(PinataMetadata{ Name: fileName, KeyValues: metadata, }) if err != nil { return "", fmt.Errorf("failed to marshal metadata: %v", err) } err = writer.WriteField("pinataMetadata", string(metadataJSON)) if err != nil { return "", fmt.Errorf("failed to write metadata field: %v", err) } } err = writer.Close() if err != nil { return "", fmt.Errorf("failed to close multipart writer: %v", err) } req, err := http.NewRequest("POST", PinataPinFileURL, body) if err != nil { return "", fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) // Set authentication headers if s.jwt != "" { req.Header.Set("Authorization", "Bearer "+s.jwt) } else { req.Header.Set("pinata_api_key", s.apiKey) req.Header.Set("pinata_secret_api_key", s.apiSecret) } resp, err := s.client.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("failed to pin file: %s, status: %d", string(body), resp.StatusCode) } var pinataResp PinataResponse err = json.NewDecoder(resp.Body).Decode(&pinataResp) if err != nil { return "", fmt.Errorf("failed to decode response: %v", err) } return pinataResp.IpfsHash, nil } // PinJSON uploads JSON data to IPFS via Pinata func (s *IPFSService) PinJSON(data interface{}, name string, metadata map[string]string) (string, error) { pinataMetadata := PinataMetadata{ Name: name, KeyValues: metadata, } pinataRequest := PinataJSONRequest{ PinataMetadata: pinataMetadata, PinataContent: data, } jsonData, err := json.Marshal(pinataRequest) if err != nil { return "", fmt.Errorf("failed to marshal JSON: %v", err) } req, err := http.NewRequest("POST", PinataPinJSONURL, bytes.NewBuffer(jsonData)) if err != nil { return "", fmt.Errorf("failed to create request: %v", err) } req.Header.Set("Content-Type", "application/json") // Set authentication headers if s.jwt != "" { req.Header.Set("Authorization", "Bearer "+s.jwt) } else { req.Header.Set("pinata_api_key", s.apiKey) req.Header.Set("pinata_secret_api_key", s.apiSecret) } resp, err := s.client.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("failed to pin JSON: %s, status: %d", string(body), resp.StatusCode) } var pinataResp PinataResponse err = json.NewDecoder(resp.Body).Decode(&pinataResp) if err != nil { return "", fmt.Errorf("failed to decode response: %v", err) } return pinataResp.IpfsHash, nil } // GetIPFSGatewayURL returns a public gateway URL for an IPFS hash func (s *IPFSService) GetIPFSGatewayURL(ipfsHash string) string { // Use Pinata gateway or other public gateways return fmt.Sprintf("https://gateway.pinata.cloud/ipfs/%s", ipfsHash) } // MockIPFSService is a mock implementation for testing and development type MockIPFSService struct{} // NewMockIPFSService creates a new mock IPFS service func NewMockIPFSService() *MockIPFSService { return &MockIPFSService{} } // PinFile mocks uploading a file to IPFS func (s *MockIPFSService) PinFile(filePath string, metadata map[string]string) (string, error) { log.Printf("Mock pinning file: %s with metadata: %v", filePath, metadata) // Generate a deterministic hash based on the file path for testing hashBase := strings.ReplaceAll(filepath.Base(filePath), ".", "") if len(hashBase) > 10 { hashBase = hashBase[:10] } ipfsHash := fmt.Sprintf("Qm%s123456789abcdef", hashBase) return ipfsHash, nil } // PinJSON mocks uploading JSON data to IPFS func (s *MockIPFSService) PinJSON(data interface{}, name string, metadata map[string]string) (string, error) { log.Printf("Mock pinning JSON with name: %s and metadata: %v", name, metadata) // Generate a deterministic hash based on the name for testing hashBase := name if len(hashBase) > 10 { hashBase = hashBase[:10] } ipfsHash := fmt.Sprintf("Qm%s123456789abcdef", hashBase) return ipfsHash, nil } // GetIPFSGatewayURL mocks a public gateway URL for an IPFS hash func (s *MockIPFSService) GetIPFSGatewayURL(ipfsHash string) string { return fmt.Sprintf("https://mock-gateway.pinata.cloud/ipfs/%s", ipfsHash) }