From ae5b2adc07c8a5362b3838fa2f5c0fdba78c2c38 Mon Sep 17 00:00:00 2001 From: agnik Date: Sat, 13 Dec 2025 18:40:50 +0530 Subject: [PATCH] feat: Replace Pinata IPFS with Web3.Storage (free alternative) Fixes #25 - Find an alternative to IPFS ## Changes: - Added new StorageService with Web3.Storage as primary provider - Web3.Storage offers 5GB free storage with no cost restrictions - Maintains backward compatibility with existing Pinata setup - Falls back to Pinata if Web3.Storage is not configured - Added multi-gateway fallback for image loading reliability - Updated all files using IPFS upload functionality ## New Environment Variable: - WEB3_STORAGE_TOKEN: Get free at https://web3.storage ## Benefits: - Free 5GB storage (vs Pinata's limited free tier) - Same IPFS hash format (content-addressable) - Backed by Filecoin for persistence - Multiple gateway fallbacks for reliability --- .env.stencil | 8 + lib/pages/mint_nft/mint_nft_images.dart | 2 +- .../create_organisation.dart | 2 +- lib/pages/register_user_page.dart | 2 +- lib/pages/tree_details_page.dart | 2 +- lib/utils/services/ipfs_services.dart | 31 ---- lib/utils/services/storage_service.dart | 167 ++++++++++++++++++ .../profile_section_widget.dart | 13 +- .../user_profile_viewer_widget.dart | 11 +- 9 files changed, 192 insertions(+), 46 deletions(-) delete mode 100644 lib/utils/services/ipfs_services.dart create mode 100644 lib/utils/services/storage_service.dart diff --git a/.env.stencil b/.env.stencil index d474d43..cecfca7 100644 --- a/.env.stencil +++ b/.env.stencil @@ -1,7 +1,15 @@ #API KEYS AND SECRETS +# Web3.Storage - FREE IPFS storage (recommended) +# Get your free token at: https://web3.storage +# 5GB free storage, no cost restrictions +WEB3_STORAGE_TOKEN= + +# Pinata - Legacy IPFS provider (optional, paid after free tier) +# Only needed if Web3.Storage is not configured PINATA_API_KEY= PINATA_API_SECRET= + ALCHEMY_API_KEY= #APPLICATION CONFIGURATION diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart index 195f305..f97b70d 100644 --- a/lib/pages/mint_nft/mint_nft_images.dart +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -7,7 +7,7 @@ import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; -import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; +import 'package:tree_planting_protocol/utils/services/storage_service.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; class MultipleImageUploadPage extends StatefulWidget { diff --git a/lib/pages/organisations_pages/create_organisation.dart b/lib/pages/organisations_pages/create_organisation.dart index b7a9fb7..e283ab6 100644 --- a/lib/pages/organisations_pages/create_organisation.dart +++ b/lib/pages/organisations_pages/create_organisation.dart @@ -6,7 +6,7 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_contract_write_functions.dart'; -import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; // Add this import for your IPFS function +import 'package:tree_planting_protocol/utils/services/storage_service.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; class CreateOrganisationPage extends StatefulWidget { diff --git a/lib/pages/register_user_page.dart b/lib/pages/register_user_page.dart index c9cc03e..94a5026 100644 --- a/lib/pages/register_user_page.dart +++ b/lib/pages/register_user_page.dart @@ -7,7 +7,7 @@ import 'package:tree_planting_protocol/components/transaction_dialog.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart'; -import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; +import 'package:tree_planting_protocol/utils/services/storage_service.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; class RegisterUserPage extends StatefulWidget { diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index bd10eea..8c65fbb 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -8,7 +8,7 @@ import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart'; import 'package:tree_planting_protocol/utils/services/conversion_functions.dart'; -import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; +import 'package:tree_planting_protocol/utils/services/storage_service.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/map_widgets/static_map_display_widget.dart'; import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart'; diff --git a/lib/utils/services/ipfs_services.dart b/lib/utils/services/ipfs_services.dart deleted file mode 100644 index ade2787..0000000 --- a/lib/utils/services/ipfs_services.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:http/http.dart' as http; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -String apiKey = dotenv.get('PINATA_API_KEY', fallback: ""); -String apiSecret = dotenv.get('PINATA_API_SECRET', fallback: ""); - -Future uploadToIPFS( - File imageFile, Function(bool) setUploadingState) async { - setUploadingState(true); - - var url = Uri.parse("https://api.pinata.cloud/pinning/pinFileToIPFS"); - var request = http.MultipartRequest("POST", url); - request.headers.addAll({ - "pinata_api_key": apiKey, - "pinata_secret_api_key": apiSecret, - }); - - request.files.add(await http.MultipartFile.fromPath("file", imageFile.path)); - var response = await request.send(); - - setUploadingState(false); - - if (response.statusCode == 200) { - var jsonResponse = json.decode(await response.stream.bytesToString()); - return "https://gateway.pinata.cloud/ipfs/${jsonResponse['IpfsHash']}"; - } else { - return null; - } -} diff --git a/lib/utils/services/storage_service.dart b/lib/utils/services/storage_service.dart new file mode 100644 index 0000000..fe81ca8 --- /dev/null +++ b/lib/utils/services/storage_service.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; + +/// Storage service that supports multiple providers for decentralized file storage. +/// Currently supports: Web3.Storage (free IPFS pinning via Filecoin) +/// +/// Web3.Storage provides: +/// - 5GB free storage +/// - IPFS-compatible content addressing (same hash format) +/// - No cost restrictions for reasonable usage +/// - Backed by Filecoin for persistence + +class StorageService { + // Web3.Storage API token (get free at https://web3.storage) + static String get _web3StorageToken => + dotenv.get('WEB3_STORAGE_TOKEN', fallback: ""); + + // Legacy Pinata keys (kept for backward compatibility) + static String get _pinataApiKey => + dotenv.get('PINATA_API_KEY', fallback: ""); + static String get _pinataApiSecret => + dotenv.get('PINATA_API_SECRET', fallback: ""); + + /// Upload a file to decentralized storage and return the gateway URL. + /// + /// Tries Web3.Storage first (free), falls back to Pinata if configured. + /// Returns the full gateway URL with the IPFS hash. + static Future uploadFile( + File file, + Function(bool) setUploadingState, + ) async { + setUploadingState(true); + + try { + // Try Web3.Storage first (free option) + if (_web3StorageToken.isNotEmpty) { + final result = await _uploadToWeb3Storage(file); + if (result != null) { + setUploadingState(false); + return result; + } + } + + // Fall back to Pinata if Web3.Storage fails or isn't configured + if (_pinataApiKey.isNotEmpty && _pinataApiSecret.isNotEmpty) { + final result = await _uploadToPinata(file); + if (result != null) { + setUploadingState(false); + return result; + } + } + + logger.e('No storage provider configured. Please set WEB3_STORAGE_TOKEN or PINATA_API_KEY in .env'); + setUploadingState(false); + return null; + } catch (e) { + logger.e('Error uploading file: $e'); + setUploadingState(false); + return null; + } + } + + /// Upload to Web3.Storage (free IPFS pinning) + /// Get your free token at: https://web3.storage + static Future _uploadToWeb3Storage(File file) async { + try { + final url = Uri.parse('https://api.web3.storage/upload'); + + final request = http.MultipartRequest('POST', url); + request.headers.addAll({ + 'Authorization': 'Bearer $_web3StorageToken', + }); + + request.files.add( + await http.MultipartFile.fromPath('file', file.path), + ); + + final streamedResponse = await request.send(); + final response = await http.Response.fromStream(streamedResponse); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final cid = jsonResponse['cid']; + logger.d('Web3.Storage upload successful. CID: $cid'); + // Use w3s.link gateway (Web3.Storage's fast gateway) + return 'https://w3s.link/ipfs/$cid'; + } else { + logger.e('Web3.Storage upload failed: ${response.statusCode} - ${response.body}'); + return null; + } + } catch (e) { + logger.e('Web3.Storage upload error: $e'); + return null; + } + } + + /// Upload to Pinata (legacy, paid after free tier) + static Future _uploadToPinata(File file) async { + try { + final url = Uri.parse('https://api.pinata.cloud/pinning/pinFileToIPFS'); + + final request = http.MultipartRequest('POST', url); + request.headers.addAll({ + 'pinata_api_key': _pinataApiKey, + 'pinata_secret_api_key': _pinataApiSecret, + }); + + request.files.add( + await http.MultipartFile.fromPath('file', file.path), + ); + + final streamedResponse = await request.send(); + final response = await http.Response.fromStream(streamedResponse); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final ipfsHash = jsonResponse['IpfsHash']; + logger.d('Pinata upload successful. Hash: $ipfsHash'); + return 'https://gateway.pinata.cloud/ipfs/$ipfsHash'; + } else { + logger.e('Pinata upload failed: ${response.statusCode} - ${response.body}'); + return null; + } + } catch (e) { + logger.e('Pinata upload error: $e'); + return null; + } + } + + /// Extract the IPFS CID/hash from a gateway URL + /// Works with any IPFS gateway URL format + static String? extractCidFromUrl(String url) { + // Match patterns like: + // https://w3s.link/ipfs/bafybeig... + // https://gateway.pinata.cloud/ipfs/Qm... + // https://ipfs.io/ipfs/Qm... + final regex = RegExp(r'/ipfs/([a-zA-Z0-9]+)'); + final match = regex.firstMatch(url); + return match?.group(1); + } + + /// Get alternative gateway URLs for a given IPFS URL + /// Useful for fallback if one gateway is slow/down + static List getAlternativeGateways(String originalUrl) { + final cid = extractCidFromUrl(originalUrl); + if (cid == null) return [originalUrl]; + + return [ + 'https://w3s.link/ipfs/$cid', // Web3.Storage gateway (fast) + 'https://ipfs.io/ipfs/$cid', // Protocol Labs gateway + 'https://cloudflare-ipfs.com/ipfs/$cid', // Cloudflare gateway + 'https://dweb.link/ipfs/$cid', // dweb.link gateway + ]; + } +} + +// Legacy function for backward compatibility +// This wraps the new StorageService +Future uploadToIPFS( + File imageFile, + Function(bool) setUploadingState, +) async { + return StorageService.uploadFile(imageFile, setUploadingState); +} diff --git a/lib/widgets/profile_widgets/profile_section_widget.dart b/lib/widgets/profile_widgets/profile_section_widget.dart index 715b777..dfe23d7 100644 --- a/lib/widgets/profile_widgets/profile_section_widget.dart +++ b/lib/widgets/profile_widgets/profile_section_widget.dart @@ -7,6 +7,7 @@ import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart'; +import 'package:tree_planting_protocol/utils/services/storage_service.dart'; class VerificationDetails { final String verifier; @@ -387,14 +388,14 @@ class _ProfileSectionWidgetState extends State { logger.e("Error: $error"); logger.e("Stack trace: $stackTrace"); - // Try alternative IPFS gateway if original fails + // Try alternative IPFS gateways if original fails String originalUrl = _userProfileData!.profilePhoto; - if (originalUrl.contains('pinata.cloud')) { - String ipfsHash = - originalUrl.split('/ipfs/').last; - String alternativeUrl = - 'https://ipfs.io/ipfs/$ipfsHash'; + final alternatives = StorageService.getAlternativeGateways(originalUrl); + + if (alternatives.length > 1) { + // Try the second gateway (first is usually the original) + String alternativeUrl = alternatives[1]; logger.d( "Trying alternative IPFS gateway: $alternativeUrl"); diff --git a/lib/widgets/profile_widgets/user_profile_viewer_widget.dart b/lib/widgets/profile_widgets/user_profile_viewer_widget.dart index cc8482e..ab65403 100644 --- a/lib/widgets/profile_widgets/user_profile_viewer_widget.dart +++ b/lib/widgets/profile_widgets/user_profile_viewer_widget.dart @@ -6,6 +6,7 @@ import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart'; +import 'package:tree_planting_protocol/utils/services/storage_service.dart'; import 'package:tree_planting_protocol/widgets/profile_widgets/profile_section_widget.dart'; class UserProfileViewerWidget extends StatefulWidget { @@ -164,12 +165,12 @@ class _UserProfileViewerWidgetState extends State { 'Access-Control-Allow-Origin': '*', }, errorBuilder: (context, error, stackTrace) { - // Try alternative IPFS gateway if original fails + // Try alternative IPFS gateways if original fails String originalUrl = _userProfileData!.profilePhoto; - if (originalUrl.contains('pinata.cloud')) { - String ipfsHash = originalUrl.split('/ipfs/').last; - String alternativeUrl = - 'https://ipfs.io/ipfs/$ipfsHash'; + final alternatives = StorageService.getAlternativeGateways(originalUrl); + + if (alternatives.length > 1) { + String alternativeUrl = alternatives[1]; return Image.network( alternativeUrl,