使用 Avail 的 Light Client 构建一个简单的 Next.js 应用

本文介绍了如何使用 Next.js 和 Avail 的 Light Client 构建一个去中心化的笔记应用,该应用可以将笔记直接存储在 Avail 的数据可用性(DA)层上。文章分步骤讲解了应用搭建过程,包括UI设计,连接 Light Client,提交笔记,以及增强功能,例如跟踪区块确认、本地存储和消息管理。

学习区块链概念就像在水下解魔方! 数据可用性 (DA) 是扩展难题的关键部分,但它通常与诸如 Rollup、欺诈证明和有效性证明等复杂主题混杂在一起。 有时,最好的学习方法是一次专注于一个部分。

在本教程中,我们将做到这一点——单独隔离并探索数据可用性。 可以把它想象成掌握魔方的一个面,然后再去解决整个难题。 虽然在生产环境中,你通常会通过 Rollup 或其他扩展解决方案与 DA 交互,但出于学习目的,我们将稍微打破规则。 我们将构建一个简单的笔记应用程序,该程序可直接与 Avail 的 DA 层对话!

在本教程结束时,你将通过构建一些有形的东西来了解数据可用性的工作原理——一个将消息直接存储在 Avail 的 DA 层的去中心化笔记应用程序。 这就像在深入研究之前,先亲身实践魔方的一个面!


💡 只想获取代码:你可以跳过本教程并在此处运行完整的应用程序 here!


我们要构建什么

我们将分四个主要步骤创建一个去中心化的笔记应用程序:

  1. 基本的 Next.js 设置:一个干净、响应式的 UI,用于输入和显示笔记
  2. 轻客户端集成:本地 Avail 轻客户端的实时状态监控
  3. 消息功能:使用 Avail 轻客户端直接提交消息
  4. 增强功能:你在链上的笔记! 使用 Avail 轻客户端跟踪区块确认
    1. 跟踪笔记确认
    2. 重试失败的提交
    3. 本地备份存储
    4. 消息历史记录管理

前提条件

开始之前,请确保你已具备:

  • 安装了 Node.js 18.17 或更高版本
  • 对 React 和 Next.js 有基本的了解
  • 你选择的代码编辑器
  • 在本地运行的 Avail 轻客户端

💡 Avail 新手?


设置基本应用程序

让我们逐步构建我们的笔记界面。 目标是创建一个干净、现代的 UI,稍后我们将它连接到 Avail 的网络。

1. 创建一个新的 Next.js 项目

首先,让我们使用 Tailwind CSS 创建一个新的 Next.js 应用程序:

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

出现提示时选择这些选项(只需按 Enter 即可获取我们想要的默认值):

✔ 是否要使用 TypeScript? › 否
✔ 是否要使用 ESLint? › 是
✔ 是否要使用 Tailwind CSS? › 是
✔ 是否要使用 src/ 目录? › 否
✔ 是否要使用 App Router? › 是
✔ 你想使用 Turbopack 进行 `next dev` 吗?› 否
✔ 你想自定义导入别名吗? › 否

2. 添加我们的 UI 库

安装用于图标的 Lucide React 库:

npm install lucide-react

3. 设置项目结构

创建一个新的 components 目录:

mkdir app/components

4. 创建你的第一个组件

创建一个新文件 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. 连接起来

app/page.js 的内容替换为:

import AvailNotesApp from './components/AvailNotesApp';

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

6. 启动它

运行开发服务器:

npm run dev

访问 http://localhost:3000,你将看到闪亮的新笔记 UI!

你应该会看到一个干净的 UI,如下所示:

项目组织

avail-notes/
├── app/
│   ├── components/
│   │   └── AvailNotesApp.js    # 奇迹发生的地方
│   ├── layout.js               # Tailwind 设置
│   └── page.js                 # 应用程序包装器
├── package.json
└── tailwind.config.js

💡

快速提示:我们暂时将所有内容放在一个组件中,以保持清晰。 稍后,你可能希望在应用程序增长时将其拆分!


第 2 部分:添加轻客户端连接

在开始编码之前,让我们了解一下我们连接的内容:

  • Avail 轻客户端在本地端口 7007 上运行
  • 提供 REST API 端点(我们将使用 /v2/status
  • 以“轻客户端模式”或“应用程序客户端模式”运行

1. 设置状态管理

首先,让我们添加跟踪客户端状态所需的状态:

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

2. 创建状态检查器

接下来,让我们创建一个从轻客户端获取状态的函数:

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. 添加状态轮询

我们希望持续检查客户端的状态:

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

4. 显示连接状态

最后,让我们将状态信息添加到我们的 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. 试用一下!

启动你的轻客户端:

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

启动你的应用程序:

npm run dev

访问 http://localhost:3000,你将看到你的连接状态!

你的 UI 现在看起来像:

💡 想要第 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
  });

  // 从轻客户端获取状态
  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);
  }, []);

  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;

第 3 部分:添加消息功能

现在让我们的应用程序与 AvailDA 对话! 我们将通过 5 个清晰的步骤来构建它:

1. 设置消息状态

首先,让我们添加状态来管理我们的消息:

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

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

2. 添加状态显示助手

让我们定义一个助手函数来管理消息状态显示:

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. 消息提交函数

这是将笔记提交到轻客户端的核心函数:

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

  // 创建待处理消息
  const pendingMessage = {
    id: Date.now(),
    data: note,
    timestamp: new Date().toLocaleTimeString(),
    status: 'pending',
  };

  // 添加到消息并清除输入
  setMessages(prev => [pendingMessage, ...prev]);
  setNote('');

  try {
    // 提交到轻客户端
    const response = await fetch('http://localhost:7007/v2/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        data: btoa(note)  // 对笔记进行 Base64 编码
      })
    });

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

    const result = await response.json();

    // 成功后更新消息状态
    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
    ));
  }
};

4. 添加消息输入 UI

用于新消息的输入区域:

{/* 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. 添加消息显示 UI

用于显示所有消息的组件:

{/* 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. 试用一下!

启动你的轻客户端:

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

启动你的应用程序:

npm run dev

访问 http://localhost:3000,然后发送你的第一条链上笔记!

你的 UI 现在看起来像:

💡 想要第 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);
  }, []);

  // 提交笔记
  const submitNote = async () => {
    if (!note) return;

    // 立即添加待处理消息
    const pendingMessage = {
      id: Date.now(),
      data: note,
      timestamp: new Date().toLocaleTimeString(),
      status: 'pending',
    };

    setMessages(prev => [pendingMessage, ...prev]);
    setNote(''); // 立即清除输入

    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();

      // 使用提交详细信息更新消息
      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
      ));
    }
  };

  // 获取状态显示信息
  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;

第 4 部分:使用区块数据增强消息功能

让我们使用一些高级功能来增强我们的应用程序! 我们将添加区块数据跟踪、本地存储和消息管理。

添加智能区块数据获取

首先,让我们添加我们的弹性区块数据获取函数:

const fetchBlockData = async (blockNumber, attempt = 0) => {
  try {
    const response = await fetch(
      `http://localhost:7007/v2/blocks/${blockNumber}/data?fields=data,extrinsic`
    );

    if (!response.ok) {
      // 如果我们得到 404,则区块可能尚未同步
      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. 在本地保存笔记

使用专用密钥设置消息的存储:

const STORAGE_KEY = 'avail-notes-blocks';

// 在初始渲染时加载消息
useEffect(() => {
  const savedMessages = localStorage.getItem(STORAGE_KEY);
  if (savedMessages) {
    try {
      setMessages(JSON.parse(savedMessages));
    } catch (e) {
      console.error('Error loading saved messages:', e);
    }
  }
}, []);

// 在消息更改时保存消息
useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
}, [messages]);

3. 跟踪区块确认

为消息添加自动区块数据加载:

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. 添加消息管理

添加处理消息操作的函数:

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. 增强的用户界面

将清除历史记录按钮添加到标题:

<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>

更新消息组件以包含删除功能和块数据:

<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. 尝试一下!

启动你的轻客户端:

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

启动你的应用:

npm run dev

访问 http://localhost:3000 并尝试:

  • 发送消息
  • 观看区块确认
  • 删除消息
  • 清除历史记录
  • 刷新页面(你的笔记会保留!)

你的用户界面现在看起来像:

💡 想要第四部分的完整代码吗?

'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;

任务完成!

祝贺你 - 你已经构建了一个功能齐全的去中心化笔记应用程序!你已经迈出了进入数据可用性世界的第一步,创建了一个应用程序,它可以:

  • ✍️ 将笔记直接写入 Avail 的 DA 层
  • 🔍 通过区块链跟踪你的数据
  • ✅ 验证你的笔记是否安全存储

你写的每条笔记现在都以去中心化、可验证的方式存储。 对于你的第一个区块链应用程序来说,非常酷!

💡 专家提示:虽然我们为了学习直接与 Avail 的 DA 层对话,但生产应用程序通常使用 Rollup 或扩展解决方案。 将这视为你构建更大事物的基石!

查看 Light Client API 文档 以获取更多通过你的新轻客户端与 Avail 交互的方式!


接下来是什么?

  • 📚 深入研究 Light Client API 文档

  • 🔒 确保你的身份文件和 App ID 安全(它们是你访问 Avail 的 🔑!)

  • 🛠️ 开始构建你自己的 DA 驱动的应用程序

    • *

来分享你构建的内容并获得支持:

教程 传递区块 Avail DA 开发者 黑客松

  • 原文链接: blog.availproject.org/bu...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Avail Project
Avail Project
Build with Avail DA, the validity proven data availability layer unifying Web3