Decentralized File Storage with UHRP
Duration: 75 minutes
Prerequisites: Node.js, basic TypeScript knowledge, understanding of decentralized storage and WalletClient
usage
Learning Goals:
- Understand UHRP (Universal Hash Resource Protocol)
- Upload and download files using StorageUploader/StorageDownloader
- Implement decentralized file management systems
- Handle file integrity verification and expiration
Introduction
UHRP (Universal Hash Resource Protocol) is a decentralized file storage system that uses content hashing for addressing and retrieval. The BSV SDK provides StorageUploader
and StorageDownloader
classes for seamless integration with UHRP storage networks.
Prerequisites
For Upload Operations
- BRC-100 compliant wallet (such as MetaNet Desktop Wallet) must be installed and running
- Wallet connection accessible via JSON API (typically http://localhost:3321)
- Sufficient wallet balance for transaction fees and storage costs
- UHRP storage service - This tutorial uses
https://nanostore.babbage.systems
For Download Operations Only
- No wallet connection required - downloads work independently
- Network access to resolve UHRP URLs via lookup services
Service Availability
Important Note: This tutorial uses https://nanostore.babbage.systems
, which is a working UHRP storage service. The examples demonstrate correct SDK usage patterns and will work with:
- A running BRC-100 compliant wallet (such as MetaNet Desktop Wallet)
- Sufficient wallet balance for storage fees
Performance Note: UHRP storage operations may take time to complete as they involve blockchain transactions and network propagation. Upload operations can take 10-30 seconds or more depending on network conditions.
Network Propagation: After uploading, files typically take 30-60 seconds to propagate across the UHRP network before they become available for download. This is normal behavior for decentralized storage systems and ensures content integrity verification.
Key Features
- Content-Addressed Storage: Files identified by their hash
- Decentralized Retrieval: Multiple storage providers
- Integrity Verification: Automatic hash validation
- Expiration Management: Time-based file retention
- Authenticated Upload: Wallet-based authentication
What You'll Build
- File upload system with UHRP
- Decentralized file retrieval
- File management dashboard
- Integrity verification system
Setting Up UHRP Storage
Basic File Upload
import { StorageUploader, WalletClient } from '@bsv/sdk'
async function basicFileUpload() {
const wallet = new WalletClient('auto', 'localhost')
const uploader = new StorageUploader({
storageURL: 'https://nanostore.babbage.systems',
wallet
})
// Create sample file
const fileData = new TextEncoder().encode('Hello, UHRP storage!')
const file = {
data: Array.from(fileData),
type: 'text/plain'
}
try {
const result = await uploader.publishFile({
file,
retentionPeriod: 60 * 24 * 7 // 7 days in minutes
})
console.log('File uploaded successfully!')
console.log('UHRP URL:', result.uhrpURL)
console.log('Published:', result.published)
return result
} catch (error) {
console.error('Upload failed:', error)
throw error
}
}
basicFileUpload().catch(console.error)
File Download and Verification
import { StorageDownloader } from '@bsv/sdk'
async function basicFileDownload(uhrpUrl: string) {
const downloader = new StorageDownloader({
networkPreset: 'mainnet'
})
try {
console.log('Downloading file:', uhrpUrl)
const result = await downloader.download(uhrpUrl)
console.log('File downloaded successfully!')
console.log('MIME Type:', result.mimeType)
console.log('Content length:', result.data.length, 'bytes')
// Convert to string if text file
if (result.mimeType?.startsWith('text/')) {
const content = new TextDecoder().decode(new Uint8Array(result.data))
console.log('Content:', content)
}
return result
} catch (error) {
console.error('Download failed:', error)
throw error
}
}
// Example usage (replace with actual UHRP URL)
// basicFileDownload('uhrp://abc123...').catch(console.error)
Complete File Management System
File Manager Class
import { StorageUploader, StorageDownloader, WalletClient } from '@bsv/sdk'
interface FileMetadata {
uhrpUrl: string
originalName: string
mimeType: string
size: number
uploadDate: Date
expiryDate: Date
tags: string[]
}
class UHRPFileManager {
private uploader: StorageUploader
private downloader: StorageDownloader
private fileRegistry: Map<string, FileMetadata> = new Map()
constructor(storageURL: string, wallet?: WalletClient) {
this.uploader = new StorageUploader({
storageURL,
wallet: wallet || new WalletClient('auto', 'localhost')
})
this.downloader = new StorageDownloader({
networkPreset: 'mainnet'
})
}
async uploadFile(
fileData: Uint8Array,
fileName: string,
mimeType: string,
retentionDays: number = 30,
tags: string[] = []
): Promise<FileMetadata> {
const file = {
data: Array.from(fileData),
type: mimeType
}
const retentionMinutes = retentionDays * 24 * 60
try {
const result = await this.uploader.publishFile({
file,
retentionPeriod: retentionMinutes
})
const metadata: FileMetadata = {
uhrpUrl: result.uhrpURL,
originalName: fileName,
mimeType,
size: fileData.length,
uploadDate: new Date(),
expiryDate: new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000),
tags
}
this.fileRegistry.set(result.uhrpURL, metadata)
console.log(`File "${fileName}" uploaded successfully`)
console.log('UHRP URL:', result.uhrpURL)
return metadata
} catch (error) {
console.error(`Failed to upload "${fileName}":`, error)
throw error
}
}
async downloadFile(uhrpUrl: string): Promise<{
data: Uint8Array
metadata: FileMetadata | null
}> {
try {
const result = await this.downloader.download(uhrpUrl)
const metadata = this.fileRegistry.get(uhrpUrl) || null
console.log('File downloaded:', uhrpUrl)
return {
data: new Uint8Array(result.data),
metadata
}
} catch (error) {
console.error('Download failed:', error)
throw error
}
}
async getFileInfo(uhrpUrl: string): Promise<any> {
try {
return await this.uploader.findFile(uhrpUrl)
} catch (error) {
console.error('Failed to get file info:', error)
throw error
}
}
async renewFile(uhrpUrl: string, additionalDays: number): Promise<any> {
const additionalMinutes = additionalDays * 24 * 60
try {
const result = await this.uploader.renewFile(uhrpUrl, additionalMinutes)
// Update local metadata if exists
const metadata = this.fileRegistry.get(uhrpUrl)
if (metadata) {
metadata.expiryDate = new Date(Date.now() + additionalDays * 24 * 60 * 60 * 1000)
this.fileRegistry.set(uhrpUrl, metadata)
}
console.log(`File renewed for ${additionalDays} days`)
return result
} catch (error) {
console.error('Failed to renew file:', error)
throw error
}
}
listFiles(tag?: string): FileMetadata[] {
const files = Array.from(this.fileRegistry.values())
if (tag) {
return files.filter(file => file.tags.includes(tag))
}
return files
}
getExpiringFiles(daysAhead: number = 7): FileMetadata[] {
const cutoffDate = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000)
return Array.from(this.fileRegistry.values())
.filter(file => file.expiryDate <= cutoffDate)
.sort((a, b) => a.expiryDate.getTime() - b.expiryDate.getTime())
}
}
async function demonstrateFileManager() {
const fileManager = new UHRPFileManager('https://nanostore.babbage.systems')
console.log('=== UHRP File Manager Demo ===')
// Upload different types of files
const textData = new TextEncoder().encode('This is a text document for UHRP storage.')
const jsonData = new TextEncoder().encode(JSON.stringify({
message: 'Hello from UHRP',
timestamp: new Date().toISOString(),
data: [1, 2, 3, 4, 5]
}))
try {
// Upload text file
const textFile = await fileManager.uploadFile(
textData,
'document.txt',
'text/plain',
30,
['document', 'text']
)
// Upload JSON file
const jsonFile = await fileManager.uploadFile(
jsonData,
'data.json',
'application/json',
60,
['data', 'json']
)
console.log('\n=== File Registry ===')
const allFiles = fileManager.listFiles()
allFiles.forEach(file => {
console.log(`${file.originalName}: ${file.uhrpUrl}`)
})
// Test download
console.log('\n=== Testing Download ===')
const downloadResult = await fileManager.downloadFile(textFile.uhrpUrl)
const content = new TextDecoder().decode(downloadResult.data)
console.log('Downloaded content:', content)
// Check expiring files
console.log('\n=== Expiring Files ===')
const expiringFiles = fileManager.getExpiringFiles(365) // Next year
expiringFiles.forEach(file => {
console.log(`${file.originalName} expires: ${file.expiryDate.toISOString()}`)
})
return { textFile, jsonFile, allFiles }
} catch (error) {
console.error('Demo failed:', error)
}
}
demonstrateFileManager().catch(console.error)
Advanced Features
Batch Operations
import { StorageUploader, StorageDownloader, WalletClient } from '@bsv/sdk'
class BatchFileOperations {
private uploader: StorageUploader
private downloader: StorageDownloader
constructor(storageURL: string, wallet?: WalletClient) {
this.uploader = new StorageUploader({
storageURL,
wallet: wallet || new WalletClient('auto', 'localhost')
})
this.downloader = new StorageDownloader()
}
async batchUpload(files: Array<{
data: Uint8Array
name: string
type: string
retention?: number
}>): Promise<Array<{
success: boolean
file: string
uhrpUrl?: string
error?: string
}>> {
console.log(`Starting batch upload of ${files.length} files...`)
const results = await Promise.allSettled(
files.map(async (file) => {
const fileObj = {
data: Array.from(file.data),
type: file.type
}
const result = await this.uploader.publishFile({
file: fileObj,
retentionPeriod: (file.retention || 30) * 24 * 60
})
return { file: file.name, result }
})
)
return results.map((result, index) => {
const fileName = files[index].name
if (result.status === 'fulfilled') {
return {
success: true,
file: fileName,
uhrpUrl: result.value.result.uhrpURL
}
} else {
return {
success: false,
file: fileName,
error: result.reason.message
}
}
})
}
async batchDownload(uhrpUrls: string[]): Promise<Array<{
success: boolean
url: string
data?: Uint8Array
error?: string
}>> {
console.log(`Starting batch download of ${uhrpUrls.length} files...`)
const results = await Promise.allSettled(
uhrpUrls.map(url => this.downloader.download(url))
)
return results.map((result, index) => {
const url = uhrpUrls[index]
if (result.status === 'fulfilled') {
return {
success: true,
url,
data: new Uint8Array(result.value.data)
}
} else {
return {
success: false,
url,
error: result.reason.message
}
}
})
}
}
async function demonstrateBatchOperations() {
const batchOps = new BatchFileOperations('https://nanostore.babbage.systems')
// Prepare test files
const testFiles = [
{
data: new TextEncoder().encode('File 1 content'),
name: 'file1.txt',
type: 'text/plain'
},
{
data: new TextEncoder().encode('File 2 content'),
name: 'file2.txt',
type: 'text/plain'
},
{
data: new TextEncoder().encode(JSON.stringify({ test: 'data' })),
name: 'data.json',
type: 'application/json'
}
]
console.log('=== Batch Upload Demo ===')
const uploadResults = await batchOps.batchUpload(testFiles)
uploadResults.forEach(result => {
if (result.success) {
console.log(`✅ ${result.file}: ${result.uhrpUrl}`)
} else {
console.log(`❌ ${result.file}: ${result.error}`)
}
})
// Extract successful URLs for download test
const successfulUrls = uploadResults
.filter(r => r.success && r.uhrpUrl)
.map(r => r.uhrpUrl!)
if (successfulUrls.length > 0) {
console.log('\n=== Batch Download Demo ===')
const downloadResults = await batchOps.batchDownload(successfulUrls)
downloadResults.forEach(result => {
if (result.success) {
console.log(`✅ Downloaded: ${result.url} (${result.data?.length} bytes)`)
} else {
console.log(`❌ Failed: ${result.url} - ${result.error}`)
}
})
}
return { uploadResults, downloadResults: [] }
}
demonstrateBatchOperations().catch(console.error)
Troubleshooting
Common Issues and Solutions
"No wallet available" Error
Problem: StorageUploader fails with "No wallet available over any communication substrate" Solution:
- Install and run a BRC-100 compliant wallet (e.g., MetaNet Desktop Wallet)
- Ensure wallet is accessible at http://localhost:3321
- Verify wallet is fully synced and has sufficient balance
"401 Unauthorized" Error
Problem: Upload operations fail with HTTP 401 errors Solution:
- Verify your wallet is properly authenticated
- Check that the UHRP storage service is available
- Ensure your wallet has sufficient balance for storage fees
"Invalid parameter UHRP url" Error
Problem: Download operations fail with invalid URL error Solution:
- Verify the UHRP URL format (should start with
uhrp://
) - Check that the file hasn’t expired
- Ensure network connectivity for UHRP lookup services
Download Works but Upload Fails
Problem: StorageDownloader works but StorageUploader fails Solution: This is expected behavior without a wallet connection. StorageDownloader works independently, while StorageUploader requires wallet authentication.
Service Unavailable
Problem: UHRP storage service returns errors or is unreachable Solution:
- Try alternative UHRP storage services
- Check service status and availability
- Consider setting up your own UHRP storage infrastructure
Best Practices
1. File Management
- Use meaningful file names and metadata
- Implement proper retention policies
- Tag files for easy organization and retrieval
2. Error Handling
- Always validate file integrity after download
- Implement retry logic for network failures
- Handle storage quota and payment requirements
3. Performance
- Use batch operations for multiple files
- Implement caching for frequently accessed files
- Monitor file expiration and renewal needs
4. Security
- Encrypt sensitive files before upload
- Use authenticated storage endpoints
- Validate file types and sizes
Summary
In this tutorial, you learned how to:
✅ Upload files to UHRP storage with StorageUploader
✅ Download and verify files with StorageDownloader
✅ Build file management systems with metadata tracking
✅ Implement batch operations for multiple files
✅ Handle file expiration and renewal
UHRP provides a robust foundation for decentralized file storage with content addressing and integrity verification.
Next Steps
- Learn about Identity Management and Certificates
- Explore AuthFetch for Authenticated HTTP Requests
- Review Security Best Practices
UHRP provides a robust foundation for decentralized file storage with content addressing and integrity verification.
The WalletClient
provides the authentication and payment capabilities needed for UHRP operations.
Setting Up UHRP with WalletClient
The WalletClient
handles authentication automatically when you create StorageUploader
and StorageDownloader
instances.
How WalletClient
Enables UHRP
When you use UHRP with WalletClient
:
- You can upload files to decentralized storage networks.
- You can download files from decentralized storage networks.
- You can manage file metadata and track file expiration.
- You can implement batch operations for multiple files.