π€ Seller Flow
Learn how sellers upload, encrypt, and publish datasets on SourceNet marketplace using Walrus Protocol for decentralized storage.
Overview
The seller flow consists of three main phases:
- Upload: Upload dataset file with encryption
- Review: Preview and verify dataset information
- Publish: Create on-chain DataPod and list on marketplace
Complete Flow Diagram
βββββββββββ
β Seller β
ββββββ¬βββββ
β
β 1. POST /api/seller/upload (file, metadata)
βΌ
ββββββββββββββββββββ
β Backend API β
β uploadController β
ββββββ¬ββββββββββββββ
β
β 2. Validate file & metadata
β 3. Generate data hash (SHA-256)
β 4. Encrypt file with AES-256
β
βΌ
ββββββββββββββββββββ
β Walrus Storage β
ββββββ¬ββββββββββββββ
β 5. Upload encrypted file β Returns blob_id
β
βΌ
ββββββββββββββββββββ
β PostgreSQL β
β DataPod β
ββββββ¬ββββββββββββββ
β 6. Store DataPod:
β - sellerId
β - blobId (Walrus)
β - metadata
β - status: "draft"
β
βΌ
Return upload_id to Seller
β 7. Seller reviews and confirms
β
β 8. POST /api/seller/publish
βΌ
ββββββββββββββββββββ
β Backend API β
β publishControllerβ
ββββββ¬ββββββββββββββ
β
β 9. Update status to "published"
β
βΌ
ββββββββββββββββββββ
β WebSocket β
ββββββ¬ββββββββββββββ
β 10. Broadcast to marketplace
β
βΌ
SUCCESSPhase 1: File Upload
Frontend: Upload Form
'use client';
import { useState } from 'react';
export default function UploadForm() {
const [file, setFile] = useState<File | null>(null);
const [metadata, setMetadata] = useState({
title: '',
description: '',
category: '',
price: ''
});
const [uploading, setUploading] = useState(false);
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
setUploading(true);
try {
const formData = new FormData();
formData.append('file', file!);
formData.append('metadata', JSON.stringify(metadata));
const response = await fetch('/api/seller/upload', {
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
},
body: formData
});
const { uploadId } = await response.json();
// Navigate to preview page
router.push(`/seller/preview/${uploadId}`);
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
}
};
return (
<form onSubmit={handleUpload}>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
accept=".csv,.json,.parquet"
/>
<input
placeholder="Title"
value={metadata.title}
onChange={(e) => setMetadata({...metadata, title: e.target.value})}
/>
<textarea
placeholder="Description"
value={metadata.description}
onChange={(e) => setMetadata({...metadata, description: e.target.value})}
/>
<select
value={metadata.category}
onChange={(e) => setMetadata({...metadata, category: e.target.value})}
>
<option value="">Select category</option>
<option value="finance">Finance</option>
<option value="healthcare">Healthcare</option>
<option value="ecommerce">E-commerce</option>
</select>
<input
type="number"
placeholder="Price (SUI)"
value={metadata.price}
onChange={(e) => setMetadata({...metadata, price: e.target.value})}
/>
<button type="submit" disabled={uploading || !file}>
{uploading ? 'Uploading...' : 'Upload Dataset'}
</button>
</form>
);
}Backend: Upload Handler
import multer from 'multer';
import crypto from 'crypto';
import { WalrusService } from '../services/walrus.service';
import { DataPod } from '../models';
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 500 * 1024 * 1024 } // 500MB
});
export async function uploadDataset(req, res) {
const file = req.file;
const metadata = JSON.parse(req.body.metadata);
const sellerId = req.user.id;
// 1. Validate file
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// 2. Validate metadata
const validated = uploadSchema.parse(metadata);
// 3. Generate encryption key
const encryptionKey = crypto.randomBytes(32); // 256-bit
const iv = crypto.randomBytes(16);
// 4. Encrypt file
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
const encryptedData = Buffer.concat([
cipher.update(file.buffer),
cipher.final()
]);
// 5. Generate data hash
const dataHash = crypto
.createHash('sha256')
.update(file.buffer)
.digest('hex');
// 6. Upload to Walrus
const walrusResult = await WalrusService.uploadToWalrus(
encryptedData,
'datapods'
);
// 7. Generate preview (first 100 rows)
const preview = await generatePreview(file.buffer, file.mimetype);
// 8. Hash encryption key
const keyHash = crypto
.createHash('sha256')
.update(encryptionKey)
.digest('hex');
// 9. Store in database
const datapod = await DataPod.create({
seller_id: sellerId,
title: validated.title,
description: validated.description,
category: validated.category,
price_sui: validated.price,
walrus_blob_id: walrusResult.cid,
walrus_storage_size: walrusResult.size,
encryption_key_hash: keyHash,
encryption_iv: iv.toString('hex'),
file_type: file.mimetype,
file_size: file.size,
data_hash: dataHash,
schema: preview.schema,
sample_data: preview.sample,
published: false // Draft status
});
// 10. Store encryption key securely
await EncryptionKeyStore.save(datapod.id, encryptionKey);
res.json({ uploadId: datapod.id, preview });
}Phase 2: Preview & Review
Preview Dataset
export default function PreviewPage({ params }: { params: { id: string } }) {
const [datapod, setDatapod] = useState(null);
useEffect(() => {
fetch(`/api/seller/datapods/${params.id}`)
.then(res => res.json())
.then(data => setDatapod(data));
}, [params.id]);
if (!datapod) return <div>Loading...</div>;
return (
<div>
<h1>{datapod.title}</h1>
<p>{datapod.description}</p>
<div>
<h2>Dataset Info</h2>
<p>Category: {datapod.category}</p>
<p>Price: {datapod.price_sui} SUI</p>
<p>Size: {formatBytes(datapod.file_size)}</p>
<p>Type: {datapod.file_type}</p>
</div>
<div>
<h2>Schema</h2>
<pre>{JSON.stringify(datapod.schema, null, 2)}</pre>
</div>
<div>
<h2>Sample Data (First 10 Rows)</h2>
<table>
<thead>
<tr>
{Object.keys(datapod.sample_data[0]).map(key => (
<th key={key}>{key}</th>
))}
</tr>
</thead>
<tbody>
{datapod.sample_data.map((row, i) => (
<tr key={i}>
{Object.values(row).map((val, j) => (
<td key={j}>{val}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<button onClick={handlePublish}>
Publish to Marketplace
</button>
</div>
);
}Phase 3: Publish to Marketplace
Publish Handler
async function publishDatapod(req, res) {
const { datapodId } = req.body;
const sellerId = req.user.id;
// 1. Verify ownership
const datapod = await DataPod.findOne({
where: { id: datapodId, seller_id: sellerId }
});
if (!datapod) {
return res.status(404).json({ error: 'DataPod not found' });
}
if (datapod.published) {
return res.status(400).json({ error: 'Already published' });
}
// 2. Update status
await datapod.update({ published: true });
// 3. Broadcast to WebSocket clients
io.emit('datapod.published', {
id: datapod.id,
title: datapod.title,
category: datapod.category,
price: datapod.price_sui,
seller: {
id: req.user.id,
name: req.user.name
}
});
// 4. Invalidate cache
await CacheService.delete(`marketplace:datapods`);
res.json({
success: true,
datapod: {
id: datapod.id,
published: true
}
});
}Data Encryption Details
Encryption Algorithm
| Parameter | Value |
|---|---|
| Algorithm | AES-256-CBC |
| Key Size | 256 bits (32 bytes) |
| IV Size | 128 bits (16 bytes) |
| Mode | Cipher Block Chaining (CBC) |
Key Management
- Generation: Cryptographically secure random bytes
- Storage: Encrypted with master key, stored separately from data
- Per-Purchase Re-encryption: New encrypted copy for each buyer
- Never Exposed: Keys never sent to client or logged
Walrus Storage Integration
Upload to Walrus
class WalrusService {
static async uploadToWalrus(data: Buffer, folder: string) {
const response = await fetch(
`${process.env.WALRUS_API_URL}/v1/store`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream'
},
body: data
}
);
if (!response.ok) {
throw new Error('Walrus upload failed');
}
const result = await response.json();
return {
cid: result.newlyCreated?.blobObject?.blobId || result.alreadyCertified?.blobId,
size: result.newlyCreated?.blobObject?.size || result.alreadyCertified?.size,
url: `${process.env.WALRUS_BLOB_ENDPOINT}/${result.blobId}`
};
}
}π‘ Why Walrus?
- β Decentralized - no single point of failure
- β Immutable - data cannot be altered
- β High availability - erasure coding ensures uptime
- β Cost-effective - lower than traditional cloud storage
Seller Dashboard
View Published DataPods
async function getSellerDatapods(req, res) {
const sellerId = req.user.id;
const { page = 1, limit = 10, status } = req.query;
const where = { seller_id: sellerId };
if (status) {
where.published = status === 'published';
}
const datapods = await DataPod.findAndCountAll({
where,
limit,
offset: (page - 1) * limit,
order: [['created_at', 'DESC']],
include: [
{
model: Purchase,
attributes: [],
required: false
}
],
attributes: {
include: [
[sequelize.fn('COUNT', sequelize.col('purchases.id')), 'purchase_count'],
[sequelize.fn('SUM', sequelize.col('purchases.amount_sui')), 'total_revenue']
]
},
group: ['DataPod.id']
});
res.json({
datapods: datapods.rows,
total: datapods.count,
page,
pages: Math.ceil(datapods.count / limit)
});
}Best Practices
- File Validation: Always validate file type, size, and format
- Metadata Validation: Use Zod schemas for type-safe validation
- Preview Generation: Show sample data to help buyers evaluate quality
- Pricing Strategy: Consider dataset size, quality, and market demand
- Categories: Choose appropriate categories for better discoverability
- Descriptions: Write clear, detailed descriptions of dataset contents
β Next Steps
Learn about the Buyer Flow to understand how buyers purchase and download datasets, or explore Smart Contracts for on-chain logic.