πŸ“€ 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
     β”‚
     β–Ό
  SUCCESS

Phase 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

ParameterValue
AlgorithmAES-256-CBC
Key Size256 bits (32 bytes)
IV Size128 bits (16 bytes)
ModeCipher 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.