本文介绍了如何使用 Next.js 和 Avail 的 Light Client 构建一个去中心化的笔记应用,该应用可以将笔记直接存储在 Avail 的数据可用性(DA)层上。文章分步骤讲解了应用搭建过程,包括UI设计,连接 Light Client,提交笔记,以及增强功能,例如跟踪区块确认、本地存储和消息管理。
学习区块链概念就像在水下解魔方! 数据可用性 (DA) 是扩展难题的关键部分,但它通常与诸如 Rollup、欺诈证明和有效性证明等复杂主题混杂在一起。 有时,最好的学习方法是一次专注于一个部分。
在本教程中,我们将做到这一点——单独隔离并探索数据可用性。 可以把它想象成掌握魔方的一个面,然后再去解决整个难题。 虽然在生产环境中,你通常会通过 Rollup 或其他扩展解决方案与 DA 交互,但出于学习目的,我们将稍微打破规则。 我们将构建一个简单的笔记应用程序,该程序可直接与 Avail 的 DA 层对话!
在本教程结束时,你将通过构建一些有形的东西来了解数据可用性的工作原理——一个将消息直接存储在 Avail 的 DA 层的去中心化笔记应用程序。 这就像在深入研究之前,先亲身实践魔方的一个面!

💡 只想获取代码:你可以跳过本教程并在此处运行完整的应用程序 here!
我们将分四个主要步骤创建一个去中心化的笔记应用程序:
开始之前,请确保你已具备:
💡 Avail 新手?
让我们逐步构建我们的笔记界面。 目标是创建一个干净、现代的 UI,稍后我们将它连接到 Avail 的网络。
首先,让我们使用 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` 吗?› 否
✔ 你想自定义导入别名吗? › 否
安装用于图标的 Lucide React 库:
npm install lucide-react
创建一个新的 components 目录:
mkdir app/components
创建一个新文件 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;
将 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>
);
}
运行开发服务器:
npm run dev
访问 http://localhost:3000,你将看到闪亮的新笔记 UI!
你应该会看到一个干净的 UI,如下所示:

avail-notes/
├── app/
│ ├── components/
│ │ └── AvailNotesApp.js # 奇迹发生的地方
│ ├── layout.js # Tailwind 设置
│ └── page.js # 应用程序包装器
├── package.json
└── tailwind.config.js
💡
快速提示:我们暂时将所有内容放在一个组件中,以保持清晰。 稍后,你可能希望在应用程序增长时将其拆分!
在开始编码之前,让我们了解一下我们连接的内容:
/v2/status)首先,让我们添加跟踪客户端状态所需的状态:
const [status, setStatus] = useState('disconnected');
const [clientInfo, setClientInfo] = useState({
mode: '',
appId: null,
blockHeight: 0
});
const [error, setError] = useState(null);
接下来,让我们创建一个从轻客户端获取状态的函数:
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(); // Initial fetch
const interval = setInterval(fetchStatus, 2000);
return () => clearInterval(interval);
}, []);
最后,让我们将状态信息添加到我们的 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>
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 现在看起来像:

'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;
现在让我们的应用程序与 AvailDA 对话! 我们将通过 5 个清晰的步骤来构建它:
首先,让我们添加状态来管理我们的消息:
import { Terminal, RefreshCcw, Send, Clock, CheckCircle2 } from 'lucide-react';
const [messages, setMessages] = useState([]);
const [note, setNote] = useState('');
让我们定义一个助手函数来管理消息状态显示:
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
};
}
};
这是将笔记提交到轻客户端的核心函数:
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
));
}
};
用于新消息的输入区域:
{/* 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>
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 现在看起来像:

'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;
让我们使用一些高级功能来增强我们的应用程序! 我们将添加区块数据跟踪、本地存储和消息管理。
首先,让我们添加我们的弹性区块数据获取函数:
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;
}
};
使用专用密钥设置消息的存储:
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]);
为消息添加自动区块数据加载:
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 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));
};
将清除历史记录按钮添加到标题:
<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>
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 层对话,但生产应用程序通常使用 Rollup 或扩展解决方案。 将这视为你构建更大事物的基石!
查看 Light Client API 文档 以获取更多通过你的新轻客户端与 Avail 交互的方式!
📚 深入研究 Light Client API 文档
🔒 确保你的身份文件和 App ID 安全(它们是你访问 Avail 的 🔑!)
🛠️ 开始构建你自己的 DA 驱动的应用程序
来分享你构建的内容并获得支持:
- 原文链接: blog.availproject.org/bu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!