Build a Simple Next.js App with Avail's Light Client

In this tutorial, we're going to isolate and explore Data Availability on its own. By the end of this tutorial, you'll have a simple Next.js app posting data to Avail.

By Christine Perry 22 min read
Build a Simple Next.js App with Avail's Light Client

Learning about blockchain concepts can feel like solving a Rubik’s cube underwater! Data Availability (DA) is a crucial piece of the scaling puzzle, but it’s often mixed in with complex topics like rollups, fraud proofs, and validity proofs. Sometimes the best way to learn is to focus on one piece at a time.

In this tutorial, we're going to do just that — isolate and explore Data Availability on its own. Think of it like taking one face of that Rubik's cube and mastering it before tackling the whole puzzle. While in a production environment you'd typically interact with DA through a rollup or other scaling solution, we're going to break the rules a bit for learning purposes. We'll build a simple notes app that talks directly to Avail's DA layer!

By the end of this tutorial, you'll understand how Data Availability works by building something tangible - a decentralized notes app that stores messages directly on Avail's DA layer. It's like getting hands-on practice with one face of the Rubik's cube before diving into the deep end!


💡 Just want the code: You can skip the tutorial and run the full app here!

What We're Building

We'll create a decentralized notes application in four main steps:

  1. Basic Next.js Setup: A clean, responsive UI for entering and displaying notes
  2. Light Client Integration: Real-time status monitoring of your local Avail Light Client
  3. Message Functionality: Direct message submission using Avail Light Client
  4. Enhanced Features: Your notes on chain! Block confirmation tracking with Avail Light Client
    1. Track note confirmations
    2. Retry failed submissions
    3. Local backup storage
    4. Message history management

Prerequisites

Before starting, ensure you have:

  • Node.js 18.17 or later installed
  • Basic familiarity with React and Next.js
  • A code editor of your choice
  • An Avail Light Client running locally

💡 New to Avail?


Setting Up the Basic App

Let's build our note-taking interface step by step. The goal is to create a clean, modern UI that we'll later connect to Avail's network.

1. Create a New Next.js Project

First, let’s create a new Next.js application with Tailwind CSS:

npx create-next-app@latest avail-notes
cd avail-notes

Choose these options when prompted (just hit enter for the defaults we want):

✔ Would you like to use TypeScript? › No
✔ Would you like to use ESLint? › Yes
✔ Would you like to use Tailwind CSS? › Yes
✔ Would you like to use src/ directory? › No
✔ Would you like to use App Router? › Yes
✔ Would you like to use Turbopack for `next dev`?› No
✔ Would you like to customize the import alias? › No

2. Add Our UI Library

Install the Lucide React library for icons:

npm install lucide-react

3. Set Up the Project Structure

Create a new components directory:

mkdir app/components

4. Create Your First Component

Create a new file app/components/AvailNotesApp.js:

'use client';

import React, { useState } from 'react';
import { Terminal } from 'lucide-react';

const AvailNotesApp = () => {
  const [note, setNote] = useState('');

  return (
    <div className="max-w-xl mx-auto p-4">
      <div className="bg-white rounded-lg shadow-lg overflow-hidden">
        {/* Header */}
        <div className="p-4 border-b border-gray-200">
          <div className="flex items-center space-x-2">
            <Terminal className="w-6 h-6" />
            <h1 className="text-lg font-bold text-gray-900">Avail Notes</h1>
          </div>
        </div>

        {/* Content */}
        <div className="p-4">
          {/* Input area */}
          <div className="flex gap-2 mb-6">
            <input
              type="text"
              value={note}
              onChange={(e) => setNote(e.target.value)}
              placeholder="Enter your note..."
              className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              disabled={!note}
              className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-600 transition-colors"
            >
              Send
            </button>
          </div>

          {/* Messages */}
          <div className="text-center text-gray-500 p-4">
            No notes yet. Start by sending one!
          </div>
        </div>
      </div>
    </div>
  );
};

export default AvailNotesApp;

5. Wire It Up

Replace the contents of app/page.js with:

import AvailNotesApp from './components/AvailNotesApp';

export default function Home() {
  return (
    <main className="min-h-screen bg-gray-100 py-8">
      <AvailNotesApp />
    </main>
  );
}

6. Fire It Up

Run the development server:

npm run dev

Visit http://localhost:3000 and you should see your shiny new notes UI!

You should see a clean UI that looks like:

Project Organization

avail-notes/
├── app/
│   ├── components/
│   │   └── AvailNotesApp.js    # Where the magic happens
│   ├── layout.js               # Tailwind setup
│   └── page.js                 # App wrapper
├── package.json
└── tailwind.config.js
💡
Quick Note: We're keeping everything in one component for now to keep things clear. Later, you might want to split this up as your app grows!

Part 2: Adding Light Client Connectivity

Before we start coding, let's understand what we're connecting to:

  • Avail Light Client runs locally on port 7007
  • Provides REST API endpoints (we'll use /v2/status)
  • Operates in either "Light Client Mode" or "App Client Mode"

1. Set Up State Management

First, let's add the state we need to track the client's status:

const [status, setStatus] = useState('disconnected');
const [clientInfo, setClientInfo] = useState({
  mode: '',
  appId: null,
  blockHeight: 0
});
const [error, setError] = useState(null);

2. Create Status Checker

Next, let's create a function to fetch status from the Light Client:

const fetchStatus = async () => {
  try {
    const response = await fetch('http://localhost:7007/v2/status');
    if (!response.ok) {
      throw new Error('Light Client returned error status');
    }
    
    const data = await response.json();
    const clientMode = data.modes.includes('app') 
      ? 'App Client Mode' 
      : 'Light Client Mode';
    
    setClientInfo({
      mode: clientMode,
      appId: data.app_id,
      blockHeight: data.blocks.latest
    });
    setStatus('connected');
    setError(null);
  } catch (error) {
    console.error('Error fetching status:', error);
    setStatus('error');
    setError(error.message);
  }
};

3. Add Status Polling

We want to continuously check the client's status:

useEffect(() => {
  fetchStatus(); // Initial fetch
  const interval = setInterval(fetchStatus, 2000);
  return () => clearInterval(interval);
}, []);

4. Show Connection Status

Finally, let's add the status information to our UI:

{/* Status Cards */}
<div className="bg-gray-50 rounded-lg p-4 space-y-3">
  {/* Connection Status */}
  <div className="flex items-center justify-between">
    <span className="text-gray-600">Status</span>
    <span className="flex items-center text-green-500">
      {status === 'connected' && <RefreshCcw className="w-4 h-4 mr-1 animate-spin" />}
      {status === 'connected' ? 'Active' : status}
    </span>
  </div>
  
  {/* Client Mode */}
  <div className="flex items-center justify-between">
    <span className="text-gray-600">Client Mode</span>
    <span className="text-blue-500">{clientInfo.mode || 'Unknown'}</span>
  </div>
  
  {/* App ID (if available) */}
  {clientInfo.appId && (
    <div className="flex items-center justify-between">
      <span className="text-gray-600">App ID</span>
      <span className="text-orange-500">#{clientInfo.appId}</span>
    </div>
  )}
  
  {/* Block Height */}
  <div className="flex items-center justify-between">
    <span className="text-gray-600">Block Height</span>
    <span className="text-purple-500">#{clientInfo.blockHeight}</span>
  </div>
  
  {/* Error Display */}
  {error && (
    <div className="flex items-center gap-2 text-red-500 text-sm">
      <AlertCircle className="w-4 h-4" />
      {error}
    </div>
  )}
</div>

5. Try it out!

Start your Light Client:

curl -sL1 avail.sh | bash -s – --app_id YOUR_APP_ID --network turing --identity ~/.avail/identity/identity.toml

Start your app:

npm run dev

Visit http://localhost:3000 and you should see your connection status!

Your UI will now look like:

💡 Want the full code for part 2?

'use client';

import React, { useState, useEffect } from 'react';
import { Terminal, RefreshCcw } from 'lucide-react';

const AvailNotesApp = () => {
  const [note, setNote] = useState('');
  const [status, setStatus] = useState('disconnected');
  const [clientInfo, setClientInfo] = useState({
    mode: '',
    appId: null,
    blockHeight: 0
  });

  // Fetch status from Light Client
  const fetchStatus = async () => {
    try {
      const response = await fetch('http://localhost:7007/v2/status');
      const data = await response.json();
      
      const clientMode = data.modes.includes('app') ? 'App Client Mode' : 'Light Client Mode';
      setClientInfo({
        mode: clientMode,
        appId: data.app_id,
        blockHeight: data.blocks.latest
      });
      setStatus('connected');
    } catch (error) {
      console.error('Error fetching status:', error);
      setStatus('error');
    }
  };

  // Poll for status updates
  useEffect(() => {
    fetchStatus(); // Initial fetch
    const interval = setInterval(fetchStatus, 2000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="max-w-xl mx-auto p-4">
      <div className="bg-white rounded-lg shadow-lg overflow-hidden">
        {/* Header */}
        <div className="p-4 border-b border-gray-200">
          <div className="flex items-center justify-between mb-4">
            <div className="flex items-center space-x-2">
              <Terminal className="w-6 h-6" />
              <h1 className="text-lg font-bold text-gray-900">Avail Notes</h1>
            </div>
          </div>

          {/* Status Cards */}
          <div className="bg-gray-50 rounded-lg p-4 space-y-3">
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Status</span>
              <span className="flex items-center text-green-500">
                {status === 'connected' && <RefreshCcw className="w-4 h-4 mr-1 animate-spin" />}
                {status === 'connected' ? 'Active' : status}
              </span>
            </div>
            
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Client Mode</span>
              <span className="text-blue-500">{clientInfo.mode || 'Unknown'}</span>
            </div>

            {clientInfo.appId && (
              <div className="flex items-center justify-between">
                <span className="text-gray-600">App ID</span>
                <span className="text-orange-500">#{clientInfo.appId}</span>
              </div>
            )}
            
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Block Height</span>
              <span className="text-purple-500">#{clientInfo.blockHeight}</span>
            </div>
          </div>
        </div>

        {/* Content */}
        <div className="p-4">
          {/* Input area */}
          <div className="flex gap-2 mb-6">
            <input
              type="text"
              value={note}
              onChange={(e) => setNote(e.target.value)}
              placeholder="Enter your note..."
              className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              disabled={!note || status !== 'connected'}
              className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-600 transition-colors"
            >
              Send
            </button>
          </div>

          <div className="text-center text-gray-500 p-4">
            No notes yet. Start by sending one!
          </div>
        </div>
      </div>
    </div>
  );
}; 

export default AvailNotesApp;

Part 3: Adding Message Functionality

Now let's make our app talk to AvailDA! We'll build this in 5 clear steps:

1. Set Up Message State

First, let's add state for managing our messages:

import { Terminal, RefreshCcw, Send, Clock, CheckCircle2 } from 'lucide-react';

const [messages, setMessages] = useState([]);
const [note, setNote] = useState('');

2. Add Status Display Helper

Let's define a helper function to manage message status displays:

const getStatusDisplay = (messageStatus) => {
  switch (messageStatus) {
    case 'pending':
      return {
        color: 'text-yellow-500',
        icon: <Clock className="w-3 h-3" />,
        text: 'Pending'
      };
    case 'submitted':
      return {
        color: 'text-green-500',
        icon: <CheckCircle2 className="w-3 h-3" />,
        text: 'Submitted'
      };
    case 'failed':
      return {
        color: 'text-red-500',
        icon: null,
        text: 'Failed'
      };
    default:
      return {
        color: 'text-gray-500',
        icon: null,
        text: messageStatus
      };
  }
};

3. Message Submission Function

Here's the core function for submitting notes to the Light Client:

const submitNote = async () => {
  if (!note) return;

  // Create pending message
  const pendingMessage = {
    id: Date.now(),
    data: note,
    timestamp: new Date().toLocaleTimeString(),
    status: 'pending',
  };

  // Add to messages and clear input
  setMessages(prev => [pendingMessage, ...prev]);
  setNote('');

  try {
    // Submit to Light Client
    const response = await fetch('http://localhost:7007/v2/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        data: btoa(note)  // Base64 encode the note
      })
    });

    if (!response.ok) {
      throw new Error('Failed to submit note');
    }

    const result = await response.json();
    
    // Update message status on success
    setMessages(prev => prev.map(msg => 
      msg.id === pendingMessage.id 
        ? {
            ...msg,
            status: 'submitted',
            blockNumber: result.block_number
          }
        : msg
    ));
  } catch (error) {
    console.error('Error submitting note:', error);
    // Update message status on failure
    setMessages(prev => prev.map(msg => 
      msg.id === pendingMessage.id 
        ? {
            ...msg,
            status: 'failed'
          }
        : msg
    ));
  }
};

4. Add Message Input UI

The input area for new messages:

{/* Input area */}
<div className="flex gap-2 mb-6">
  <input
    type="text"
    value={note}
    onChange={(e) => setNote(e.target.value)}
    onKeyPress={(e) => e.key === 'Enter' && submitNote()}
    placeholder="Enter your note..."
    className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
  />
  <button
    onClick={submitNote}
    disabled={!note || status !== 'connected'}
    className="px-4 py-2 bg-blue-500 text-white rounded-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-600 transition-colors"
  >
    <Send className="w-4 h-4" />
    Send
  </button>
</div>

5. Add Message Display UI

The component to display all messages:

{/* Messages */}
<div className="space-y-3">
  {messages.map((msg) => {
    const statusDisplay = getStatusDisplay(msg.status);
    return (
      <div 
        key={msg.id} 
        className="p-3 bg-gray-50 rounded-lg border border-gray-200"
      >
        <p className="text-gray-800">{msg.data}</p>
        <div className="flex justify-between items-center mt-1">
          <p className="text-xs text-gray-500">{msg.timestamp}</p>
          <div className="flex items-center gap-2">
            <span className={`text-xs flex items-center gap-1 ${statusDisplay.color}`}>
              {statusDisplay.icon}
              {statusDisplay.text}
            </span>
            {msg.blockNumber && (
              <span className="text-xs text-gray-400">Block #{msg.blockNumber}</span>
            )}
          </div>
        </div>
      </div>
    );
  })}
  {messages.length === 0 && (
    <div className="text-center text-gray-500 p-4">
      No notes yet. Start by sending one!
    </div>
  )}
</div>

6. Try It Out!

Start your Light Client:

curl -sL1 avail.sh | bash -s – --app_id YOUR_APP_ID --network turing --identity ~/.avail/identity/identity.toml

Start your app:

npm run dev

Visit http://localhost:3000 and send your first on-chain note!

Your UI will now look like:

💡 Want the full code for part 3?

'use client';

import React, { useState, useEffect } from 'react';
import { Terminal, RefreshCcw, Send, Clock, CheckCircle2 } from 'lucide-react';

const AvailNotesApp = () => {
  const [note, setNote] = useState('');
  const [status, setStatus] = useState('disconnected');
  const [messages, setMessages] = useState([]);
  const [clientInfo, setClientInfo] = useState({
    mode: '',
    appId: null,
    blockHeight: 0
  });

  const fetchStatus = async () => {
    try {
      const response = await fetch('http://localhost:7007/v2/status');
      const data = await response.json();
      
      const clientMode = data.modes.includes('app') ? 'App Client Mode' : 'Light Client Mode';
      setClientInfo({
        mode: clientMode,
        appId: data.app_id,
        blockHeight: data.blocks.latest
      });
      setStatus('connected');
    } catch (error) {
      console.error('Error fetching status:', error);
      setStatus('error');
    }
  };

  useEffect(() => {
    fetchStatus();
    const interval = setInterval(fetchStatus, 2000);
    return () => clearInterval(interval);
  }, []);

  // Submit note
  const submitNote = async () => {
    if (!note) return;

    // Add pending message immediately
    const pendingMessage = {
      id: Date.now(),
      data: note,
      timestamp: new Date().toLocaleTimeString(),
      status: 'pending',
    };

    setMessages(prev => [pendingMessage, ...prev]);
    setNote(''); // Clear input immediately

    try {
      const response = await fetch('http://localhost:7007/v2/submit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          data: btoa(note)
        })
      });

      if (!response.ok) {
        throw new Error('Failed to submit note');
      }

      const result = await response.json();
      
      // Update the message with submission details
      setMessages(prev => prev.map(msg => 
        msg.id === pendingMessage.id 
          ? {
              ...msg,
              status: 'submitted',
              blockNumber: result.block_number
            }
          : msg
      ));
    } catch (error) {
      console.error('Error submitting note:', error);
      setMessages(prev => prev.map(msg => 
        msg.id === pendingMessage.id 
          ? {
              ...msg,
              status: 'failed'
            }
          : msg
      ));
    }
  };

  // Get status display info
  const getStatusDisplay = (messageStatus) => {
    switch (messageStatus) {
      case 'pending':
        return {
          color: 'text-yellow-500',
          icon: <Clock className="w-3 h-3" />,
          text: 'Pending'
        };
      case 'submitted':
        return {
          color: 'text-green-500',
          icon: <CheckCircle2 className="w-3 h-3" />,
          text: 'Submitted'
        };
      case 'failed':
        return {
          color: 'text-red-500',
          icon: null,
          text: 'Failed'
        };
      default:
        return {
          color: 'text-gray-500',
          icon: null,
          text: messageStatus
        };
    }
  };

  return (
    <div className="max-w-xl mx-auto p-4">
      <div className="bg-white rounded-lg shadow-lg overflow-hidden">
        {/* Header */}
        <div className="p-4 border-b border-gray-200">
          <div className="flex items-center justify-between mb-4">
            <div className="flex items-center space-x-2">
              <Terminal className="w-6 h-6" />
              <h1 className="text-lg font-bold text-gray-900">Avail Notes</h1>
            </div>
          </div>

          {/* Status Cards */}
          <div className="bg-gray-50 rounded-lg p-4 space-y-3">
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Status</span>
              <span className="flex items-center text-green-500">
                {status === 'connected' && <RefreshCcw className="w-4 h-4 mr-1 animate-spin" />}
                {status === 'connected' ? 'Active' : status}
              </span>
            </div>
            
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Client Mode</span>
              <span className="text-blue-500">{clientInfo.mode || 'Unknown'}</span>
            </div>

            {clientInfo.appId && (
              <div className="flex items-center justify-between">
                <span className="text-gray-600">App ID</span>
                <span className="text-orange-500">#{clientInfo.appId}</span>
              </div>
            )}
            
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Block Height</span>
              <span className="text-purple-500">#{clientInfo.blockHeight}</span>
            </div>
          </div>
        </div>

        {/* Content */}
        <div className="p-4">
          {/* Input area */}
          <div className="flex gap-2 mb-6">
            <input
              type="text"
              value={note}
              onChange={(e) => setNote(e.target.value)}
              onKeyPress={(e) => e.key === 'Enter' && submitNote()}
              placeholder="Enter your note..."
              className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={submitNote}
              disabled={!note || status !== 'connected'}
              className="px-4 py-2 bg-blue-500 text-white rounded-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-600 transition-colors"
            >
              <Send className="w-4 h-4" />
              Send
            </button>
          </div>

          {/* Messages */}
          <div className="space-y-3">
            {messages.map((msg) => {
              const statusDisplay = getStatusDisplay(msg.status);
              return (
                <div 
                  key={msg.id} 
                  className="p-3 bg-gray-50 rounded-lg border border-gray-200"
                >
                  <p className="text-gray-800">{msg.data}</p>
                  <div className="flex justify-between items-center mt-1">
                    <p className="text-xs text-gray-500">{msg.timestamp}</p>
                    <div className="flex items-center gap-2">
                      <span className={`text-xs flex items-center gap-1 ${statusDisplay.color}`}>
                        {statusDisplay.icon}
                        {statusDisplay.text}
                      </span>
                      {msg.blockNumber && (
                        <span className="text-xs text-gray-400">Block #{msg.blockNumber}</span>
                      )}
                    </div>
                  </div>
                </div>
              );
            })}
            {messages.length === 0 && (
              <div className="text-center text-gray-500 p-4">
                No notes yet. Start by sending one!
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default AvailNotesApp;

Part 4: Enhanced Message Functionality with Block Data

Let's supercharge our app with some advanced features! We'll add block data tracking, local storage, and message management.

Add Smart Block Data Fetching

First, let's add our resilient block data fetching function:

const fetchBlockData = async (blockNumber, attempt = 0) => {
  try {
    const response = await fetch(
      `http://localhost:7007/v2/blocks/${blockNumber}/data?fields=data,extrinsic`
    );
    
    if (!response.ok) {
      // If we get a 404, the block might not be synced yet
      if (response.status === 404) {
        if (attempt < 5) {
          const delay = 2000 * Math.pow(2, attempt);
          await new Promise(resolve => setTimeout(resolve, delay));
          return fetchBlockData(blockNumber, attempt + 1);
        }
        return null;
      }
      throw new Error('Failed to fetch block data');
    }
    
    const blockData = await response.json();
    return blockData;
  } catch (error) {
    console.error(`Error fetching block ${blockNumber}:`, error);
    if (attempt < 5) {
      const delay = 2000 * Math.pow(2, attempt);
      await new Promise(resolve => setTimeout(resolve, delay));
      return fetchBlockData(blockNumber, attempt + 1);
    }
    return null;
  }
};

2. Save Notes Locally

Set up storage for messages with a dedicated key:

const STORAGE_KEY = 'avail-notes-blocks';

// Load messages on initial render
useEffect(() => {
  const savedMessages = localStorage.getItem(STORAGE_KEY);
  if (savedMessages) {
    try {
      setMessages(JSON.parse(savedMessages));
    } catch (e) {
      console.error('Error loading saved messages:', e);
    }
  }
}, []);

// Save messages when they change
useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
}, [messages]);

3. Track Block Confirmations

Add automatic block data loading for messages:

useEffect(() => {
  const loadBlockData = async () => {
    const updatedMessages = await Promise.all(
      messages.map(async (msg) => {
        if (msg.status === 'submitted' && msg.blockNumber && !msg.blockData) {
          const blockData = await fetchBlockData(msg.blockNumber);
          return {
            ...msg,
            blockData
          };
        }
        return msg;
      })
    );
    
    if (JSON.stringify(messages) !== JSON.stringify(updatedMessages)) {
      setMessages(updatedMessages);
    }
  };

  loadBlockData();
}, [messages]);

4. Add Message Management

Add functions to handle message operations:

const clearHistory = () => {
  if (window.confirm('Are you sure you want to clear all message history?')) {
    setMessages([]);
    localStorage.removeItem(STORAGE_KEY);
  }
};

const deleteMessage = (id) => {
  setMessages(prev => prev.filter(msg => msg.id !== id));
};

5. Enhanced UI

Add the clear history button to the header:

<div className="flex items-center justify-between mb-4">
  <div className="flex items-center space-x-2">
    <Terminal className="w-6 h-6" />
    <h1 className="text-lg font-bold text-gray-900">Avail Notes</h1>
  </div>
  {messages.length > 0 && (
    <button
      onClick={clearHistory}
      className="text-sm text-red-500 hover:text-red-600"
    >
      Clear History
    </button>
  )}
</div>

Update the message component to include delete functionality and block data:

<div 
  key={msg.id} 
  className="p-3 bg-gray-50 rounded-lg border border-gray-200 relative group"
>
  <button
    onClick={() => deleteMessage(msg.id)}
    className="absolute top-2 right-2 text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
  >
    <Trash2 className="w-4 h-4" />
  </button>
  <p className="text-gray-800 pr-8">{msg.data}</p>
  <div className="flex justify-between items-center mt-1">
    <p className="text-xs text-gray-500">{msg.timestamp}</p>
    <div className="flex items-center gap-2">
      <span className={`text-xs flex items-center gap-1 ${statusDisplay.color}`}>
        {statusDisplay.icon}
        {statusDisplay.text}
        {msg.retries > 0 && ` (${msg.retries} ${msg.retries === 1 ? 'retry' : 'retries'})`}
      </span>
      {msg.blockNumber && (
        <span className="text-xs text-gray-400 hover:text-blue-500 cursor-pointer">
          Block #{msg.blockNumber}
          {msg.blockData && (
            <span className="ml-1 text-xs text-green-500">✓</span>
          )}
        </span>
      )}
    </div>
  </div>
  {msg.blockData && (
    <div className="mt-2 text-xs bg-gray-100 p-2 rounded">
      <p className="font-medium text-gray-700">Block Data:</p>
      <pre className="overflow-x-auto text-gray-600">
        {JSON.stringify(msg.blockData, null, 2)}
      </pre>
    </div>
  )}
</div>

6. Try it out!

Start your Light Client:

curl -sL1 avail.sh | bash -s – --app_id YOUR_APP_ID --network turing --identity ~/.avail/identity/identity.toml

Start your app:

npm run dev

Visit http://localhost:3000 and try:

  • Sending messages
  • Watching block confirmations
  • Deleting messages
  • Clearing history
  • Refreshing the page (your notes persist!)

Your UI will now look like:

💡 Want the full code for part 4?

'use client';

import React, { useState, useEffect } from 'react';
import { Terminal, RefreshCcw, Send, Clock, CheckCircle2, Trash2, AlertCircle } from 'lucide-react';

const STORAGE_KEY = 'avail-notes-blocks';

const AvailNotesApp = () => {
  const [note, setNote] = useState('');
  const [status, setStatus] = useState('disconnected');
  const [messages, setMessages] = useState([]);
  const [clientInfo, setClientInfo] = useState({
    mode: '',
    appId: null,
    blockHeight: 0
  });
  const [error, setError] = useState(null);

  const fetchBlockData = async (blockNumber, attempt = 0) => {
    try {
      const response = await fetch(
        `http://localhost:7007/v2/blocks/${blockNumber}/data?fields=data,extrinsic`
      );
      
      if (!response.ok) {
        // If we get a 404, the block might not be synced yet
        if (response.status === 404) {
          if (attempt < 5) { // Try up to 5 times
            // Exponential backoff: 2s, 4s, 8s, 16s, 32s
            const delay = 2000 * Math.pow(2, attempt);
            await new Promise(resolve => setTimeout(resolve, delay));
            return fetchBlockData(blockNumber, attempt + 1);
          }
          // After max retries, return null but don't treat as error
          return null;
        }
        // For other errors, throw
        throw new Error('Failed to fetch block data');
      }
      
      const blockData = await response.json();
      return blockData;
    } catch (error) {
      console.error(`Error fetching block ${blockNumber}:`, error);
      if (attempt < 5) {
        // Retry on network errors too
        const delay = 2000 * Math.pow(2, attempt);
        await new Promise(resolve => setTimeout(resolve, delay));
        return fetchBlockData(blockNumber, attempt + 1);
      }
      return null;
    }
  };

  // Load messages from localStorage on initial render
  useEffect(() => {
    const savedMessages = localStorage.getItem(STORAGE_KEY);
    if (savedMessages) {
      try {
        setMessages(JSON.parse(savedMessages));
      } catch (e) {
        console.error('Error loading saved messages:', e);
      }
    }
  }, []);

  // Save messages to localStorage whenever they change
  useEffect(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
  }, [messages]);

  // Load block data for messages
  useEffect(() => {
    const loadBlockData = async () => {
      const updatedMessages = await Promise.all(
        messages.map(async (msg) => {
          if (msg.status === 'submitted' && msg.blockNumber && !msg.blockData) {
            const blockData = await fetchBlockData(msg.blockNumber);
            return {
              ...msg,
              blockData
            };
          }
          return msg;
        })
      );
      
      if (JSON.stringify(messages) !== JSON.stringify(updatedMessages)) {
        setMessages(updatedMessages);
      }
    };

    loadBlockData();
  }, [messages]);

  const fetchStatus = async () => {
    try {
      const response = await fetch('http://localhost:7007/v2/status');
      if (!response.ok) {
        throw new Error('Light Client returned error status');
      }
      const data = await response.json();
      
      const clientMode = data.modes.includes('app') ? 'App Client Mode' : 'Light Client Mode';
      setClientInfo({
        mode: clientMode,
        appId: data.app_id,
        blockHeight: data.blocks.latest
      });
      setStatus('connected');
      setError(null);
    } catch (error) {
      console.error('Error fetching status:', error);
      setStatus('error');
      setError(error.message);
    }
  };

  useEffect(() => {
    fetchStatus();
    const interval = setInterval(fetchStatus, 2000);
    return () => clearInterval(interval);
  }, []);

  const submitNote = async () => {
    if (!note) return;

    const pendingMessage = {
      id: Date.now(),
      data: note,
      timestamp: new Date().toLocaleTimeString(),
      status: 'pending',
      retries: 0,
      blockNumber: null,
      blockData: null
    };

    setMessages(prev => [pendingMessage, ...prev]);
    setNote('');

    const submitWithRetry = async (message, attempt = 0) => {
      try {
        const response = await fetch('http://localhost:7007/v2/submit', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            data: btoa(message.data)
          })
        });

        if (!response.ok) {
          throw new Error('Failed to submit note');
        }

        const result = await response.json();
        
        setMessages(prev => prev.map(msg => 
          msg.id === message.id 
            ? {
                ...msg,
                status: 'submitted',
                blockNumber: result.block_number,
                retries: attempt
              }
            : msg
        ));
      } catch (error) {
        console.error('Error submitting note:', error);
        
        if (attempt < 2) { // Retry up to 2 times
          setTimeout(() => {
            submitWithRetry(message, attempt + 1);
          }, 1000 * (attempt + 1)); // Exponential backoff
          
          setMessages(prev => prev.map(msg => 
            msg.id === message.id 
              ? {
                  ...msg,
                  status: 'retrying',
                  retries: attempt + 1
                }
              : msg
          ));
        } else {
          setMessages(prev => prev.map(msg => 
            msg.id === message.id 
              ? {
                  ...msg,
                  status: 'failed',
                  error: error.message,
                  retries: attempt
                }
              : msg
          ));
        }
      }
    };

    submitWithRetry(pendingMessage);
  };

  const clearHistory = () => {
    if (window.confirm('Are you sure you want to clear all message history?')) {
      setMessages([]);
      localStorage.removeItem(STORAGE_KEY);
    }
  };

  const deleteMessage = (id) => {
    setMessages(prev => prev.filter(msg => msg.id !== id));
  };

  const getStatusDisplay = (messageStatus) => {
    switch (messageStatus) {
      case 'pending':
        return {
          color: 'text-yellow-500',
          icon: <Clock className="w-3 h-3" />,
          text: 'Pending'
        };
      case 'retrying':
        return {
          color: 'text-orange-500',
          icon: <RefreshCcw className="w-3 h-3 animate-spin" />,
          text: 'Retrying'
        };
      case 'submitted':
        return {
          color: 'text-green-500',
          icon: <CheckCircle2 className="w-3 h-3" />,
          text: 'Submitted'
        };
      case 'failed':
        return {
          color: 'text-red-500',
          icon: <AlertCircle className="w-3 h-3" />,
          text: 'Failed'
        };
      default:
        return {
          color: 'text-gray-500',
          icon: null,
          text: messageStatus
        };
    }
  };

  return (
    <div className="max-w-xl mx-auto p-4">
      <div className="bg-white rounded-lg shadow-lg overflow-hidden">
        {/* Header */}
        <div className="p-4 border-b border-gray-200">
          <div className="flex items-center justify-between mb-4">
            <div className="flex items-center space-x-2">
              <Terminal className="w-6 h-6" />
              <h1 className="text-lg font-bold text-gray-900">Avail Notes</h1>
            </div>
            {messages.length > 0 && (
              <button
                onClick={clearHistory}
                className="text-sm text-red-500 hover:text-red-600"
              >
                Clear History
              </button>
            )}
          </div>

          {/* Status Cards */}
          <div className="bg-gray-50 rounded-lg p-4 space-y-3">
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Status</span>
              <span className="flex items-center text-green-500">
                {status === 'connected' && <RefreshCcw className="w-4 h-4 mr-1 animate-spin" />}
                {status === 'connected' ? 'Active' : status}
              </span>
            </div>
            
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Client Mode</span>
              <span className="text-blue-500">{clientInfo.mode || 'Unknown'}</span>
            </div>

            {clientInfo.appId && (
              <div className="flex items-center justify-between">
                <span className="text-gray-600">App ID</span>
                <span className="text-orange-500">#{clientInfo.appId}</span>
              </div>
            )}
            
            <div className="flex items-center justify-between">
              <span className="text-gray-600">Block Height</span>
              <span className="text-purple-500">#{clientInfo.blockHeight}</span>
            </div>

            {error && (
              <div className="flex items-center gap-2 text-red-500 text-sm">
                <AlertCircle className="w-4 h-4" />
                {error}
              </div>
            )}
          </div>
        </div>

        {/* Content */}
        <div className="p-4">
          {/* Input area */}
          <div className="flex gap-2 mb-6">
            <input
              type="text"
              value={note}
              onChange={(e) => setNote(e.target.value)}
              onKeyPress={(e) => e.key === 'Enter' && submitNote()}
              placeholder="Enter your note..."
              className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              onClick={submitNote}
              disabled={!note || status !== 'connected'}
              className="px-4 py-2 bg-blue-500 text-white rounded-lg flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-600 transition-colors"
            >
              <Send className="w-4 h-4" />
              Send
            </button>
          </div>

          {/* Messages */}
          <div className="space-y-3">
            {messages.map((msg) => {
              const statusDisplay = getStatusDisplay(msg.status);
              return (
                <div 
                  key={msg.id} 
                  className="p-3 bg-gray-50 rounded-lg border border-gray-200 relative group"
                >
                  <button
                    onClick={() => deleteMessage(msg.id)}
                    className="absolute top-2 right-2 text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
                  >
                    <Trash2 className="w-4 h-4" />
                  </button>
                  <p className="text-gray-800 pr-8">{msg.data}</p>
                  <div className="flex justify-between items-center mt-1">
                    <p className="text-xs text-gray-500">{msg.timestamp}</p>
                    <div className="flex items-center gap-2">
                      <span className={`text-xs flex items-center gap-1 ${statusDisplay.color}`}>
                        {statusDisplay.icon}
                        {statusDisplay.text}
                        {msg.retries > 0 && ` (${msg.retries} ${msg.retries === 1 ? 'retry' : 'retries'})`}
                      </span>
                      {msg.blockNumber && (
                        <span className="text-xs text-gray-400 hover:text-blue-500 cursor-pointer">
                          Block #{msg.blockNumber}
                          {msg.blockData && (
                            <span className="ml-1 text-xs text-green-500">✓</span>
                          )}
                        </span>
                      )}
                    </div>
                  </div>
                  {msg.blockData && (
                    <div className="mt-2 text-xs bg-gray-100 p-2 rounded">
                      <p className="font-medium text-gray-700">Block Data:</p>
                      <pre className="overflow-x-auto text-gray-600">
                        {JSON.stringify(msg.blockData, null, 2)}
                      </pre>
                    </div>
                  )}
                  {msg.error && (
                    <p className="text-xs text-red-500 mt-1">{msg.error}</p>
                  )}
                </div>
              );
            })}
            {messages.length === 0 && (
              <div className="text-center text-gray-500 p-4">
                No notes yet. Start by sending one!
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default AvailNotesApp;

Mission Accomplished!

Congratulations - you've built a fully functional decentralized notes app! You've taken your first steps into the world of Data Availability by creating an app that:

  • ✍️ Writes notes directly to Avail's DA layer
  • 🔍 Tracks your data through the blockchain
  • ✅ Verifies your notes are securely stored

Every note you write is now stored in a decentralized, verifiable way. Pretty cool for your first blockchain app!

💡 Pro Tip: While we talked directly to Avail's DA layer for learning, production apps typically use rollups or scaling solutions. Consider this your foundation for building bigger things!

Check out the Light Client API docs for more ways to interact with Avail via your new light client!


What's Next?

  • 📚 Dive deeper into the Light Client API docs
  • 🔒 Keep your identity file and App ID secure (they're your 🔑 to Avail!)
  • 🛠️ Start building your own DA-powered applications

Come share what you've built and get support: