Cover photo

Blockchain Storage Considerations: Building on Net Protocol

Net Library is a social decentralized onchain media library powered by Net Protocol and built with Neynar's App Studio on Base.

This article contains a copy of the upload-system md file currently being used by Net Library to insure that all file uploads are executed accurately. This is the second version of this article.

Everything detailed below will make much more sense within the context of a working knowledge of Net Library - a mini app that I'm currently developing, and Net Protocol, a free onchain public good developed by Aspyn Palatnick (@AspynPalatnick on X; @aspyn on Farcaster).

If you're not familiar with Net Library, check it out here on Farcaster, on the Base App here, or via a web browswer here

If you're not familiar with Net Protocol, go here

⚠️⚠️ If you connect via a web browser or The Base App, make sure to link that wallet to your Farcaster account. This allows Net Library to automatically detect your Net Library Membership across all three apps.

⚠️⚠️ The Net Library Upload System Architecture outlined below is subject to change as needed. I'm currently vibe coding this app so if bugs appear or there's issues related to uploads I might have to tweak/change this file.

This article is meant for education purposes only. I hope this helps anyone who's interested in building cool stuff on top of Net Protocol!


Upload System Architecture

Last Updated: v1.42.6 - Emoji font support for Satori rendering, improved receipt layout with smart image grids. All uploads MUST use the relay system. There is no fallback to direct wallet uploads.


Table of Contents

  1. Overview

  2. The Relay System

  3. Upload Entry Points

  4. Platform-Specific Behavior

    • Platform Detection

    • Connector Isolation

    • EIP-6963 Discovery

    • WalletGuard

  5. Wallet Types & Transaction Handling

    • Smart Wallet vs EOA

    • Transaction Flow

    • Funding Flow

    • Smart Wallet Detection

  6. Upload Protections

    • Concurrent Upload Prevention

    • Cancellable Operations

    • Payment Recovery

  7. Content Retrieval & Encoding

  8. Storage Types

  9. Session Management

  10. Archival Pattern

  11. Key Files Reference

  12. Dead Code Status

  13. UX Considerations


Overview

The upload system uses Net Protocol's x402 relay for ALL onchain storage. Users fund an "Upload Wallet" with USDC, which is converted to ETH and held in a backend wallet. This backend wallet pays gas fees automatically, eliminating the need for users to sign multiple transactions per upload.

Key Principle

User funds USDC once → Backend wallet pays gas → One-click uploads

Signature Requirements

Scenario

Signatures Required

Wallet funded + session cached

0 (fully automatic)

Wallet funded + session expired

1 (new session signature)

Wallet needs funding

2 (USDC transfer + session)


The Relay System

How It Works

  1. User funds Upload Wallet with USDC (any amount, presets: $0.10, $0.25, $5.00)

  2. x402 protocol converts USDC → ETH automatically at market rate

  3. Backend wallet (deterministically derived from user's operatorAddress) stores ETH

  4. Session signature (EIP-712) authorizes the backend to act on user's behalf for 1 hour

  5. Backend submits transactions using stored ETH for gas

  6. Content stored onchain via Net Protocol storage contracts

Backend Wallet Derivation

Backend Wallet Address = deterministic_derive(operatorAddress)
  • Same wallet address → Same backend wallet → Same balance

  • Different wallet addresses → Different backend wallets → Separate balances

Configuration (from use-relay-upload-v2.ts)

const RELAY_CONFIG = {
  apiUrl: "https://www.netprotocol.app",
  chainId: 8453, // Base
  usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  x402FacilitatorAddress: "0x6462F8f301Bd08c206ee611aA00015bA41acc1D0",
  verifyMaxRetries: 5,
  verifyRetryDelayMs: 2000,
};

Upload Entry Points

All uploads flow through useRelayUploadV2 hook → uploadFile() function.

⚠️ CRITICAL: "Archives" ARE Uploads

Grid Archives and Social Archives are uploads. They store content onchain via the relay system. When modifying upload logic, ALWAYS check ALL paths below, including archives.

Complete Upload Path Inventory

🔴 MANDATORY CHECK LIST - When editing upload logic, verify ALL of these:

Category

User Action

Handler/Hook

File Location

Standard Uploads

Single file upload

handleUpload()uploadFile()

upload-tab.tsx

Batch/multi-file

handleMultiFileUpload()uploadFile() loop

upload-tab.tsx

Drag & drop

Same as single/batch

upload-tab.tsx

Text editor save

handleTextEditorUpload()uploadFile()

upload-tab.tsx

Image editor save

handleCreatedImageUpload()uploadFile()

upload-tab.tsx

Retry failed file

retryFailedFile()uploadFile()

upload-tab.tsx

Archives (ALSO UPLOADS!)

Grid archive

handleArchive()uploadFile()

grid-archive-modal.tsx, use-grid-archive.ts

Social receipt archive

handleReceiptUpload()uploadFile()

upload-tab.tsx, social-receipt-archive.tsx

Import (No Relay)

CDN link import

handleCdnLinkUpload()

upload-tab.tsx (metadata only, no onchain storage)

Key Files by Upload Type

Standard Uploads:
├── src/features/app/components/upload-tab.tsx (main upload UI)
├── src/hooks/use-relay-upload-v2.ts (single file hook)
└── src/hooks/use-relay-batch-upload-v2.ts (batch hook, cost estimation)

Grid Archives:
├── src/features/app/components/grid-archive-modal.tsx (archive UI)
├── src/hooks/use-grid-archive.ts (archive logic)
└── src/app/api/grids/archive/route.ts (API endpoint)

Social Archives:
├── src/features/app/components/social-receipt-archive.tsx (archive UI)
├── src/app/api/social/receipt/route.tsx (Satori receipt image generation)
├── src/app/api/social/fetch/route.ts (Twitter/Farcaster content fetching)
├── src/lib/emoji-font.ts (v1.42.6 - Emoji font loader for Satori)
└── Uses uploadFile() from use-relay-upload-v2.ts

⚠️ **Satori Image Rendering (v1.42.4+)**: Receipt images are generated via Satori.
   - Images MUST have explicit width/height on both container and img element
   - **v1.42.6**: Emoji font support via emoji-font.ts (Noto Color Emoji + Twemoji fallback)
   - Receipt layout: URL cleanup (trailing t.co stripped), improved opacity (timestamp 0.8, footer 0.7)

Attribution Fields

All upload paths must correctly pass these fields to createDiscoveryEntry():

  • operator - Backend wallet address (for CDN URL generation)

  • uploaderAddress - User's wallet address (for attribution/credit)

Note: The batch upload in upload-tab.tsx uses useRelayUploadV2.uploadFile() in a sequential loop, NOT useRelayBatchUploadV2.uploadBatch(). The batch hook is used by multi-file-upload.tsx only for cost estimation (estimateBatchCost).

Code Location

All handlers are in upload-tab.tsx which imports:

import { useRelayUploadV2 as useRelayUpload } from "@/hooks/use-relay-upload-v2";

Platform-Specific Behavior

Platform Detection

// From src/lib/app-mode.ts

function detectFarcasterFrame(): boolean {
  // Checks (priority order):
  // 1. iframe context (window.parent !== window)
  // 2. URL params: fc_action, fid, frame
  // 3. User agent: "warpcast" or "farcaster"
}

function detectCoinbaseWallet(): boolean {
  // Checks:
  // 1. User agent: "coinbasewallet" or "coinbase"
  // 2. Injected provider: window.ethereum.isCoinbaseWallet or isCoinbaseBrowser
}

function detectAppMode(): "farcaster" | "base-app" | "web" | "detecting" {
  if (typeof window === "undefined") return "detecting"; // SSR
  if (detectFarcasterFrame()) return "farcaster";
  if (detectCoinbaseWallet()) return "base-app";
  return "web";
}

Platform → Wallet Connector Mapping

Platform

Connector Used

Connection Method

Notes

Warpcast (Farcaster)

farcasterMiniApp()

Frame SDK

Only Farcaster connector enabled

Coinbase Wallet app

coinbaseWallet({ preference: "all", version: "4" })

Injected provider

Only Coinbase connector enabled

Web browser

None (EIP-6963 discovery)

EIP-6963

Wallets self-announce via standard

CRITICAL: Connector Isolation

Each platform uses ONLY its designated connector. Mixing connectors caused transaction simulation issues:

  • Farcaster mode: [farcasterMiniApp()] only

  • Base App mode: [coinbaseWallet()] only

  • Web mode: [] (empty - relies on EIP-6963 multiInjectedProviderDiscovery)

This is configured in neynar-wagmi-provider.tsx.

Web Mode: EIP-6963 Wallet Discovery

In web browser mode, the app uses NO explicit connectors. Instead, it relies entirely on EIP-6963 (multiInjectedProviderDiscovery) to detect installed wallet extensions.

Why? The Coinbase Wallet SDK was hijacking connections meant for MetaMask/Rainbow when added as an explicit connector. By using EIP-6963 only, each wallet properly announces itself and the user can choose.

// From neynar-wagmi-provider.tsx - Web mode connector config
if (appMode === "web") {
  // Disable Coinbase auto-injection safeguards
  disableCoinbaseAutoInjection();

  // Return empty array - ALL wallets discovered via EIP-6963
  return [];
}

// wagmi config
createConfig({
  // ...
  multiInjectedProviderDiscovery: appMode === "web", // Only enabled in web mode
});

Discovered Wallets (via EIP-6963):

  • MetaMask (io.metamask)

  • Rainbow (me.rainbow)

  • Coinbase Wallet (com.coinbase.wallet)

  • Any other EIP-6963 compliant wallet

WalletGuard: Enforcing Wallet Preference

The WalletGuard component prevents unwanted wallet connections by enforcing user preferences:

// From neynar-wagmi-provider.tsx
function WalletGuard({ children }) {
  const { address, connector, isConnected } = useAccount();
  const { disconnect } = useDisconnect();

  // If connected wallet doesn't match saved preference, disconnect it
  if (isConnected && savedPreference && connector.id !== savedPreference) {
    disconnect();
  }

  // Also verify address matches if we have a saved address
  if (isConnected && savedAddress && address !== savedAddress) {
    disconnect();
  }
}

Preference Storage:

  • net-library-wallet-preference - Stores connector ID (e.g., "io.metamask")

  • net-library-wallet-address - Stores connected address (lowercase)

  • Cleared on explicit disconnect

  • Prevents Coinbase from hijacking MetaMask connections

Critical UX Note

Each platform may connect a DIFFERENT wallet, resulting in:

  • Different operatorAddress values

  • Different backend wallets

  • Separate Upload Wallet balances

However, the library and uploads are shared across platforms (content is tied to the user, not the wallet).


Wallet Types & Transaction Handling

Smart Wallets vs EOA

The relay hook detects wallet type and handles transactions accordingly:

// Smart Wallet (EIP-5792 compatible, e.g., Coinbase Smart Wallet)
const { sendCallsAsync } = useSendCalls();

// EOA (Externally Owned Account, traditional wallet)
const { writeContractAsync } = useWriteContract();

Transaction Flow

Smart Wallet (Farcaster/Base App):
1. Try sendCallsAsync() (EIP-5792 batched transactions)
2. Poll getCallsStatus() for tx hash (Smart Wallets don't return hash immediately)
3. If sendCallsAsync fails → Check if user rejection or simulation failure
4. If simulation failure → Fall back to writeContractAsync()

EOA (Web browser):
1. Use writeContractAsync() directly
2. Returns tx hash immediately (no polling needed)

Detailed Fallback Logic (from fundBackendWallet)

try {
  // Try Smart Wallet first
  const result = await sendCallsAsync({ calls: [...], chainId: 8453 });
  // Poll for tx hash...
} catch (smartWalletError) {
  // Check for user rejection (don't fallback)
  if (errMsg.includes("user rejected")) throw;

  // Check for simulation failure
  if (isSimulationFailure(errMsg)) {
    // Verify actual USDC balance
    const balance = await checkUserUsdcBalance();
    if (balance < amount) throw new Error("Insufficient USDC");

    // If balance is sufficient, it's a simulation problem
    if (!walletDeployed) {
      throw new Error("Smart Wallet needs activation");
    }
  }

  // Fall back to EOA method
  actualTxHash = await writeContractAsync({ ... });
}

Funding Flow (USDC → Backend Wallet)

// 1. Get x402 payment info from /api/relay/fund (returns payTo address)
// 2. Check user's USDC balance on Base chain
// 3. Send USDC directly to payTo address:
//    - Smart Wallet: sendCallsAsync() → poll getCallsStatus() for tx hash
//    - EOA: writeContractAsync() → get tx hash directly
// 4. Save tx hash to localStorage for recovery (in case verify fails)
// 5. Wait 3 seconds for tx to propagate
// 6. Verify payment with retries (5 attempts, 2s delay each)
// 7. x402 converts USDC → ETH at market rate
// 8. ETH deposited to user's backend wallet
// 9. Backend wallet ready to pay gas

Balance-Based Verification Fallback

If tx hash polling times out (60 attempts, 1s each), the system falls back to balance verification:

// Fallback: Check if backend wallet balance increased
let balanceAttempts = 0;
while (balanceAttempts < 30) {
  const newBalance = await checkBalance();
  if (newBalance > originalBalance + 0.00001) {
    return true; // Funding succeeded
  }
  await sleep(1000);
  balanceAttempts++;
}

Smart Wallet Detection

Smart Wallets (ERC-4337) may not be deployed until their first transaction. This causes simulation failures that look like "insufficient funds" errors.

// From relay-upload-utils.ts
async function isSmartWalletDeployed(address: Address): Promise<boolean> {
  const code = await basePublicClient.getCode({ address });
  return code !== undefined && code !== "0x" && code.length > 2;
}

Simulation Failure Detection

Some errors look like "insufficient funds" but are actually Smart Wallet simulation failures:

function isSimulationFailure(errorMsg: string): boolean {
  const lowerMsg = errorMsg.toLowerCase();
  return (
    lowerMsg.includes("error generating transaction") ||
    lowerMsg.includes("simulation failed") ||
    lowerMsg.includes("execution reverted") ||
    lowerMsg.includes("aa21") || // First tx with undeployed account
    lowerMsg.includes("aa25") || // Invalid account signature
    lowerMsg.includes("useroperation")
  );
}

When a simulation failure is detected, the hooks:

  1. Check actual USDC balance to confirm it's not a real funds issue

  2. If undeployed Smart Wallet: suggest smaller amount or wallet activation

  3. If deployed but still failing: suggest retry (temporary network issue)


Upload Protections

Both upload hooks include these safety mechanisms:

Concurrent Upload Prevention

const uploadInProgressRef = useRef<boolean>(false);

// At start of upload
if (uploadInProgressRef.current) {
  return { success: false, error: "Upload already in progress" };
}
uploadInProgressRef.current = true;

// In finally block
uploadInProgressRef.current = false;

Cancellable Operations

const abortControllerRef = useRef<AbortController | null>(null);

// At start of upload
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;

// In loops
if (signal.aborted) {
  throw new Error("Upload cancelled");
}

// Cancel function exposed to UI
const cancelUpload = () => {
  abortControllerRef.current?.abort();
  uploadInProgressRef.current = false;
};

Cleanup on Unmount

useEffect(() => {
  return () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
  };
}, []);

Payment Recovery

If a USDC payment is sent but verification fails (network error, user closes browser), the system can recover:

1. Fast Recovery (localStorage) Pending payments are stored in localStorage for quick recovery:

// Stored in: net-relay-pending-{address}
[{ txHash: "0x...", timestamp: 1234567890, amount: 0.10 }, ...]

// Recovery function
const recoverPendingPayments = async () => {
  const pending = localStorage.getItem(`net-relay-pending-${address}`);
  for (const payment of pending) {
    // Skip payments older than 1 hour
    if (Date.now() - payment.timestamp > 3600000) continue;

    const result = await verifyPaymentWithRetries(payment.txHash, 3, 1500);
    if (result.success) {
      // Remove from pending, update balance
    }
  }
};

2. Blockchain Scan Recovery (fallback) If localStorage recovery fails, scan blockchain for unverified USDC transfers:

// Calls /api/relay/recover which:
// 1. Scans Base chain for USDC transfers to facilitator
// 2. Attempts to verify each unprocessed payment
// 3. Returns { recovered: number, total: number }

Exposed Functions:

  • recoverPendingPayments() - Fast localStorage recovery

  • recoverUnverifiedPayments() - Full recovery (localStorage + blockchain scan)


Content Retrieval & Encoding

When reading content from Net Protocol storage, data can be returned in different encodings:

Encoding Detection Pattern

// Used in book-file, book-content, and net-protocol.ts
if (dataStr.startsWith("0x")) {
  // Hex encoded - decode from hex
  decoded = hexToBuffer(dataStr);
} else if (/^[A-Za-z0-9+/=]+$/.test(dataStr) && dataStr.length > 20) {
  // Base64 encoded - decode from base64
  decoded = Buffer.from(dataStr, "base64");
} else {
  // Plain text - use as-is
  decoded = dataStr;
}

Key Files for Content Retrieval

File

Purpose

src/app/api/book-file/route.ts

Raw file serving (PDF, audio, video, text)

src/app/api/book-content/route.ts

Content analysis and type detection

src/app/api/media-proxy/route.ts

CORS proxy for multimedia

src/lib/net-protocol.ts

Discovery index fetching

Operator vs UploaderAddress

CRITICAL: Two different addresses are used:

Field

Purpose

Example

operator

Backend wallet that wrote to blockchain

0x54D2B6AE54Bb947De4D270736e8Bd493D64560fa

uploaderAddress

User's wallet for attribution/credit

User's connected wallet

  • CDN URLs use operator (backend wallet)

  • Attribution/tipping uses uploaderAddress (user's wallet)

  • Ownership checks look at uploaderAddress first, then operator

Text Content & Markdown Rendering (v1.41.0)

Text files created via the "Write Something" editor support markdown formatting:

Editor Flow:

  1. User writes content in rich text editor (text-editor-upload.tsx)

  2. Links inserted via toolbar become <a> tags in HTML

  3. On save, htmlToTextWithLinks() converts HTML to markdown: [text](url)

  4. Content stored as .txt file on Net Protocol

Reader Flow:

  1. Content fetched via /api/media-proxy/api/book-file

  2. Base64/hex decoding applied (see above)

  3. multimedia-viewer.tsx renders content with react-markdown

  4. Links become clickable, headers render with styling

Supported Markdown:

Syntax

Renders As

[text](url)

Clickable link (green, opens in external browser via safeOpenUrl)

# Header

Green H1

**bold**

Bold text

*italic*

Italic text

- item

Bulleted list

1. item

Numbered list

`code`

Inline code (green on dark background)

> quote

Blockquote (left border)


Storage Types

Files are stored differently based on size:

Size

Storage Type

Contract

Method

< 20KB

Regular

STORAGE_CONTRACT_ADDRESS

Single transaction

20-80KB

Chunked

CHUNKED_STORAGE_ADDRESS

Multiple chunk transactions

> 80KB

XML with chunks

CHUNKED_STORAGE_ADDRESS

XML metadata + chunk transactions

Contract Addresses (Base)

STORAGE_CONTRACT_ADDRESS = "0x..." // Regular storage
CHUNKED_STORAGE_ADDRESS = "0x..."  // Chunked storage

Cost Estimation

// Each transaction costs ~0.000005 ETH on Base (~$0.015 at typical ETH prices)
const estimatedTransactions = file.size >= 80000
  ? Math.ceil(file.size / 20000) + 2  // Chunks + metadata
  : 2;                                  // Regular: store + index

const estimatedCostEth = estimatedTransactions * 0.000005;

Session Management

EIP-712 Signed Sessions

Sessions authorize the backend wallet to submit transactions on the user's behalf.

// Session structure
interface RelaySession {
  operatorAddress: Address;    // User's connected wallet
  chainId: number;             // 8453 (Base)
  expiresAt: number;           // Unix timestamp (1 hour from creation)
  signature: string;           // EIP-712 signature
}

Session Lifecycle

  1. Check cache - Memory refs store valid sessions

  2. If expired/missing - Request new signature from user

  3. User signs EIP-712 message - "Authorize relay for 1 hour"

  4. Cache session - Reuse for subsequent uploads

  5. After 1 hour - Session expires, new signature needed

Caching Strategy

// In-memory refs (survives re-renders, cleared on page refresh)
const sessionTokenRef = useRef<string | null>(null);
const sessionExpiresAtRef = useRef<number>(0);
const sessionAddressRef = useRef<string | null>(null); // Tracks which wallet the session belongs to

Session Invalidation on Wallet Change

CRITICAL: Sessions are now automatically cleared when the wallet address changes:

useEffect(() => {
  if (address !== sessionAddressRef.current) {
    // Clear session when wallet changes
    sessionTokenRef.current = null;
    sessionExpiresAtRef.current = 0;
    sessionAddressRef.current = address || null;

    // Also abort any in-progress operations
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    uploadInProgressRef.current = false;
  }
}, [address]);

This prevents using a session signed by Wallet A when Wallet B is now connected.


Archival Pattern

For any feature that needs permanent onchain storage (grids, social receipts, future features):

Standard Archival Flow

async function archiveContent(content: Blob, metadata: object) {
  // 1. Convert to File
  const file = new File([content], `${name}-${Date.now()}.png`, { type: "image/png" });

  // 2. Upload via relay
  const result = await uploadFile(file, {
    title: metadata.title,
    author: userAddress,
    categories: ["archive-type"],
    uploaderFid: fid,
  });

  // 3. Handle funding requirement
  if (result.error === "NEEDS_FUNDING") {
    // Show funding UI, return early
    return { needsFunding: true };
  }

  // 4. Store reference with contentKey
  await saveArchiveRecord({
    ...metadata,
    contentKey: result.contentKey,  // Links to onchain content
  });

  return { success: true, contentKey: result.contentKey };
}

What Gets Stored Where

Data

Location

Permanent?

Actual content (PNG, file)

Net Protocol (onchain)

Yes

Content reference (contentKey)

KV database

Yes

Metadata (title, creator, etc.)

KV database

Yes


Key Files Reference

Shared Utilities

src/lib/relay-upload-utils.ts
  • isSmartWalletDeployed() - Check if Smart Wallet contract is deployed

  • isSimulationFailure() - Detect false "insufficient funds" errors from simulation

  • analyzeUploadError() - Categorize errors for better user feedback

  • ERC20_ABI, USDC_ADDRESS, USDC_DECIMALS - Shared constants

  • basePublicClient - Shared Base chain client

CRITICAL: Both upload hooks import from this file to ensure consistent behavior across single and batch uploads.

Primary Hooks (THE upload paths)

Single File Uploads:

src/hooks/use-relay-upload-v2.ts
  • uploadFile() - Main upload function

  • fundWallet() - USDC funding with Smart Wallet handling

  • ensureSession() - Session management

  • getRelayBalance() / checkBalance() - Balance checking

  • cancelUpload() - Abort in-progress upload

  • isUploading - Check if upload is in progress

Batch File Uploads:

src/hooks/use-relay-batch-upload-v2.ts
  • uploadBatch() - Batch upload function (for true EIP-5792 batched transactions)

  • estimateBatchCost() - Cost estimation for multiple files (currently the main usage)

  • fundBackendWallet() - USDC funding with Smart Wallet handling

  • cancelBatchUpload() - Abort in-progress batch upload

  • isUploading - Check if batch upload is in progress

  • Shares session management pattern with single upload

Current Usage:

  • upload-tab.tsx uses uploadFile() in a loop for multi-file uploads (simpler, sequential)

  • multi-file-upload.tsx uses estimateBatchCost() only for showing cost preview

  • uploadBatch() is available for future use cases needing true transaction batching

Upload UI

src/features/app/components/upload-tab.tsx (~4,900 lines)
  • All upload handlers (single, batch loop, text, image, receipt, CDN)

  • UI for single/batch/drag-drop/text/image uploads

  • Funding modal integration

  • Batch uploads use sequential uploadFile() calls (not uploadBatch())

src/features/app/components/multi-file-upload.tsx
  • Batch file selection and preview UI

  • Uses use-relay-batch-upload-v2.estimateBatchCost() for cost estimation display

Upload Wallet Balance Display

src/features/app/components/upload-wallet-balance.tsx
  • Shows current balance

  • Info modal explaining relay system

  • Multi-platform wallet explanation

Platform Detection

src/lib/app-mode.ts
  • detectFarcasterFrame()

  • detectCoinbaseWallet()

  • detectAppMode()

  • detectDesktopBrowser()

  • safeOpenUrl() - Opens URLs with Farcaster SDK fallback

  • safeComposeCast() - Compose cast with web fallback

  • triggerHaptic() - Haptic feedback wrapper

Wallet Provider Configuration

src/neynar-web-sdk/src/blockchain/app/neynar-wagmi-provider.tsx
  • Platform-specific connector selection (Farcaster/Base App/Web)

  • EIP-6963 discovery for web mode

  • WalletGuard component for preference enforcement

  • disableCoinbaseAutoInjection() safeguard

  • Custom storage for reconnection filtering

  • getWalletPreference() / setWalletPreference() / clearWalletPreference()

Net Protocol Utilities

src/lib/net-protocol.ts
  • Contract addresses

  • ABIs

  • File preparation utilities

  • Storage key generation

Data URL Utilities

src/lib/data-url-utils.ts
  • dataUrlToBlob() - Safe manual base64 decode (no fetch)

  • safeDataUrlToBlob() - Decode with fetch fallback

  • dataUrlToFile() - Convert data URL directly to File

  • Required for Safari/iOS/Base App compatibility (avoids "Load failed" errors)

Grid Archival (Example of archival pattern)

src/features/app/components/grid-archive-modal.tsx
src/hooks/use-grid-archive.ts
src/app/api/grids/archive/route.ts

Dead Code Status

✅ Cleanup Completed

The following dead code files have been deleted (~4,300 lines total):

File

Lines

Status

src/hooks/use-net-upload.ts

~2,234

✅ Deleted

src/hooks/use-batch-upload.ts

~796

✅ Deleted

src/hooks/use-relay-batch-upload.ts

~1,285

✅ Deleted

src/hooks/use-relay-upload.ts

~1,500

✅ Deleted

These were workarounds before Net Protocol's relay system was available. Users had to sign 10-15+ transactions per upload. The relay v2 system eliminated this need.

Active Hooks (THE ONLY UPLOAD HOOKS)

  • use-relay-upload-v2.ts - Single file uploads (used by upload-tab.tsx)

  • use-relay-batch-upload-v2.ts - Batch file uploads (used by multi-file-upload.tsx)

DO NOT create new upload hooks. All uploads MUST use these two hooks.


UX Considerations

Upload Wallet Balance Visibility

  • Shown in Upload tab header

  • Balance displayed in ETH with USD equivalent

  • Warning when below $0.10 minimum

Funding Recommendations

Amount

Use Case

$0.10

Testing, a few uploads

$0.25

~10-25 small uploads

$5.00

~200+ uploads (recommended for regular use)

Multi-Platform Wallet Confusion

Users may not realize different apps connect different wallets. The info bubble in Upload Wallet explains:

  • Each platform connects its own wallet

  • Different wallets = separate balances

  • Library is shared, only funding is wallet-specific

Error Handling

Error

User Message

Action

NEEDS_FUNDING

"Please fund your Upload Wallet"

Show funding modal

Session expired

"Awaiting wallet signature..."

Request new signature

Upload failed

Specific error message

Allow retry

Safari/iOS/Base App: Data URL Fetch Bug

⚠️ IMPORTANT: Safari/iOS/WKWebView (used by Base App) can throw "Load failed" when fetch() is called on data: URLs due to WebKit security policies.

Problematic Pattern:

// ❌ DON'T DO THIS - fails on Safari/iOS/Base App
const response = await fetch(dataUrl);  // "Load failed" error!
const blob = await response.blob();

Safe Pattern:

// ✅ USE THIS - works everywhere
import { dataUrlToBlob, safeDataUrlToBlob } from "@/lib/data-url-utils";

// Option 1: Direct decode (throws on invalid data URL)
const blob = dataUrlToBlob(dataUrl);

// Option 2: Safe decode with fetch fallback
const blob = await safeDataUrlToBlob(dataUrl);

Manual decode (if not using utility):

const [header, base64Data] = dataUrl.split(",");
const mimeMatch = header.match(/data:([^;]+)/);
const mimeType = mimeMatch ? mimeMatch[1] : "image/png";
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
  bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: mimeType });

Affected Code Paths (all fixed):

  • handleReceiptUpload() - Social receipt archive uploads

  • handleImageUpload() - Created image uploads from editor

  • Image editor export/download


Quick Reference: Upload Flow

User clicks "Upload"
        ↓
    uploadFile(file, metadata)
        ↓
    checkBalance() → Sufficient?
        ↓ No              ↓ Yes
    Return NEEDS_FUNDING  ensureSession()
        ↓                     ↓
    Show funding UI       Session valid?
                              ↓ No           ↓ Yes
                          Sign EIP-712     Use cached
                              ↓                ↓
                          ←←←←←←←←←←←←←←←←←←←←←
                                    ↓
                          prepareFileForUpload()
                                    ↓
                          Submit to relay backend
                                    ↓
                          Backend pays gas, stores onchain
                                    ↓
                          Return { success, cdnUrl, contentKey }

Summary

  1. All uploads use relay - No exceptions, no fallbacks

  2. Two hooks, one pattern - useRelayUploadV2 for single files, useRelayBatchUploadV2 for batches

  3. Backend pays gas - User funds USDC, backend handles ETH

  4. Sessions last 1 hour - Cached in memory, re-sign when expired, cleared on wallet change

  5. Platform = Wallet = Balance - Different apps may have different balances

  6. Archival pattern is standard - Upload via relay, store contentKey in KV

  7. Smart Wallet aware - Detects undeployed wallets, handles simulation failures gracefully

  8. Protected operations - Concurrent upload prevention, cancellation support, cleanup on unmount

  9. Shared utilities - relay-upload-utils.ts ensures consistent behavior across both hooks

  10. Archives ARE uploads - Grid Archives and Social Archives use the same relay upload system. When editing upload logic, ALWAYS check: single uploads, batch uploads, grid archives, social archives

  11. Connector isolation - Each platform uses ONLY its designated connector (prevents simulation failures)

  12. EIP-6963 for web - Web mode uses wallet discovery, not explicit connectors (prevents Coinbase hijacking)

  13. WalletGuard protection - Enforces user's wallet preference, disconnects unwanted connections

  14. Payment recovery - localStorage + blockchain scan recovery for unverified USDC payments

  15. Data URL safety - Never use fetch() on data: URLs; use @/lib/data-url-utils to avoid Safari/iOS "Load failed" errors


Thanks for reading, sharing and engaging with this article!

I'm really excited about the future of onchain storage and communication - and how Net Protocol makes all of that happen.

Check out Net Library on Farcaster, The Base App, or simply via your favorite browser.
(If you connect via a web browser or The Base App, make sure to link that wallet to your Farcaster account. This allows Net Library to automatically detect your Net Library Membership across all three apps)

Have questions? Want to learn more? Like interviewing nerdy dads?
You can reach me:
@geaux_eth on X (twitter)
@geaux.eth on Farcaster

God Bless!