الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟
مرحباً بكم في درس جديد! سنتعلم اليوم كيف تستخدم العقود الذكية الأحداث (Events) كآلية فعالة لإبلاغ الواجهات الأمامية (مثل تطبيقات React) بالتغيرات أو الإجراءات التي تحدث على البلوك تشين، مما يتيح لنا بناء تطبيقات لامركزية تفاعلية وديناميكية.
الخطوة 1: تعريف وإصدار الأحداث في عقد Solidity
الأحداث في Solidity هي طريقة لتسجيل معلومات حول ما حدث في العقد الذكي على سجل المعاملات (transaction logs) الخاص بالبلوك تشين. هذه السجلات يمكن قراءتها بكفاءة من قبل التطبيقات خارج السلسلة (off-chain applications) مثل واجهات المستخدم.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EventExample {
uint public counter; // متغير عام لتخزين العداد
// تعريف حدث يتم إصداره عند تحديث العداد
// 'indexed' يسمح بالبحث والفلترة السريعة بناءً على هذا المعامل
event CounterUpdated(
address indexed user, // عنوان المستخدم الذي قام بالتحديث
uint newCounterValue, // القيمة الجديدة للعداد
uint timestamp // الطابع الزمني للعملية
);
constructor() {
counter = 0; // تهيئة العداد بقيمة صفر عند نشر العقد
}
function increment() public {
counter++; // زيادة العداد بمقدار واحد
// إصدار الحدث لإبلاغ الواجهات الأمامية بأن العداد قد تغير
emit CounterUpdated(msg.sender, counter, block.timestamp);
}
function decrement() public {
if (counter > 0) {
counter--; // إنقاص العداد بمقدار واحد، بشرط ألا يكون صفراً
// إصدار الحدث لإبلاغ الواجهات الأمامية بأن العداد قد تغير
emit CounterUpdated(msg.sender, counter, block.timestamp);
}
}
function getCurrentCounter() public view returns (uint) {
return counter; // دالة لعرض قيمة العداد الحالية
}
}
ملاحظة تقنية: استخدام الكلمة المفتاحية
indexedللمعاملات في الأحداث يسمح للمستمعين بالبحث والفلترة بكفاءة عالية بناءً على قيم هذه المعاملات دون الحاجة إلى قراءة جميع سجلات الأحداث. يمكن تحديد ما يصل إلى ثلاثة معاملاتindexedلكل حدث.
الخطوة 2: نشر العقد الذكي والحصول على ABI
لن ندخل في تفاصيل النشر هنا، ولكن بعد كتابة عقد Solidity، ستحتاج إلى نشره على شبكة بلوك تشين (مثل شبكة اختبار أو شبكة رئيسية) باستخدام أدوات مثل Hardhat أو Truffle أو Remix. بعد النشر، ستحصل على:
- عنوان العقد (Contract Address): وهو المعرف الفريد لعقدك على الشبكة.
- واجهة التطبيق الثنائية (ABI - Application Binary Interface):: وهي وصف للعقد يخبر واجهة المستخدم (أو أي تطبيق آخر) بالدوال والأحداث والمتغيرات المتاحة في العقد وكيفية التفاعل معها. عادة ما تكون ملف JSON.
الخطوة 3: الاستماع إلى الأحداث في واجهة React باستخدام Ethers.js
الآن بعد أن أصبح لدينا عقد ينبعث منه الأحداث، سنقوم بإنشاء واجهة React تستمع إلى هذه الأحداث وتحدّث واجهتها بناءً عليها. سنستخدم مكتبة ethers.js للاتصال بالبلوك تشين.
// src/App.js
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers'; // استيراد مكتبة ethers.js
import EventExampleABI from './EventExample.json'; // استيراد ABI الخاص بالعقد (يتم إنشاؤه عند بناء العقد)
const contractAddress = "0x..."; // <== استبدل بعنوان عقدك الذكي المنشور
function App() {
const [counter, setCounter] = useState(0); // حالة لتخزين قيمة العداد
const [lastUpdater, setLastUpdater] = useState(''); // لتخزين عنوان آخر مستخدم قام بالتحديث
const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState(''); // لتخزين وقت آخر تحديث
const [contract, setContract] = useState(null); // لتخزين كائن العقد للوصول إليه لاحقاً
const [signer, setSigner] = useState(null); // لتخزين كائن الموقّع لإرسال المعاملات
useEffect(() => {
const connectWalletAndContract = async () => {
if (window.ethereum) { // التحقق مما إذا كان MetaMask (أو محفظة متوافقة) متاحاً
try {
// طلب الوصول لحسابات MetaMask
await window.ethereum.request({ method: 'eth_requestAccounts' });
// إنشاء موفّر (Provider) باستخدام MetaMask
const provider = new ethers.BrowserProvider(window.ethereum);
// الحصول على الموقّع (Signer) لإرسال المعاملات
const _signer = await provider.getSigner();
setSigner(_signer);
// إنشاء نسخة من العقد للتفاعل معه
const _contract = new ethers.Contract(contractAddress, EventExampleABI.abi, _signer);
setContract(_contract);
// جلب القيمة الأولية للعداد من العقد عند التحميل
const initialCounter = await _contract.getCurrentCounter();
setCounter(initialCounter.toString());
// ******* الجزء الأهم: الاستماع إلى حدث CounterUpdated *******
_contract.on("CounterUpdated", (user, newCounterValue, timestamp, event) => {
console.log("حدث CounterUpdated تم إصداره:", { user, newCounterValue, timestamp: new Date(Number(timestamp) * 1000).toLocaleString() });
setCounter(newCounterValue.toString()); // تحديث العداد في واجهة المستخدم
setLastUpdater(user); // تحديث عنوان آخر محدّث
setLastUpdateTimestamp(new Date(Number(timestamp) * 1000).toLocaleString()); // تحديث الطابع الزمني
});
console.log("تم الاتصال بالعقد بنجاح وتم بدء الاستماع للأحداث.");
} catch (error) {
console.error("خطأ في الاتصال بـ MetaMask أو العقد:", error);
}
} else {
console.error("MetaMask غير متاح. يرجى تثبيته.");
}
};
connectWalletAndContract();
// دالة تنظيف: إزالة مستمعي الأحداث عند إلغاء تحميل المكون لتجنب تسرب الذاكرة
return () => {
if (contract) {
contract.removeAllListeners("CounterUpdated");
console.log("تم إزالة مستمعي الأحداث.");
}
};
}, [contractAddress]); // يعاد تشغيل هذا التأثير إذا تغير عنوان العقد
// ... (بقية الكود الخاص بالتعامل مع الأزرار في الخطوة التالية)
}
ملاحظة تقنية: الدالة
contract.on("EventName", (arg1, arg2, ..., event) => { ... })هي الطريقة القياسية للاستماع إلى الأحداث في Ethers.js. المعاملات التي تمرر إلى الدالة هي نفسها المعاملات التي تم إصدارها مع الحدث في Solidity، بالإضافة إلى كائنeventيحتوي على معلومات إضافية حول الحدث.
الخطوة 4: التفاعل مع العقد وتحديث الواجهة
الآن سنضيف الأزرار التي ستتفاعل مع دالات العقد increment() و decrement(). عندما يتم استدعاء هذه الدوال، سيقوم العقد بإصدار الحدث، والذي بدوره سيتم التقاطه بواسطة مستمع الأحداث في React لتحديث الواجهة تلقائياً.
// ... (الكود السابق في src/App.js)
const handleIncrement = async () => {
if (contract && signer) { // التأكد من أن العقد والموقّع جاهزان
try {
const tx = await contract.increment(); // استدعاء دالة الزيادة في العقد
await tx.wait(); // انتظار تأكيد المعاملة على البلوك تشين
console.log("تم إرسال معاملة الزيادة بنجاح.");
// لا نحتاج لجلب العداد يدوياً هنا، لأن الحدث 'CounterUpdated' سيقوم بتحديثه
} catch (error) {
console.error("خطأ عند زيادة العداد:", error);
}
} else {
alert("الرجاء الاتصال بـ MetaMask أولاً.");
}
};
const handleDecrement = async () => {
if (contract && signer) {
try {
const tx = await contract.decrement(); // استدعاء دالة الإنقاص في العقد
await tx.wait(); // انتظار تأكيد المعاملة
console.log("تم إرسال معاملة الإنقاص بنجاح.");
// لا نحتاج لجلب العداد يدوياً هنا، لأن الحدث 'CounterUpdated' سيقوم بتحديثه
} catch (error) {
console.error("خطأ عند إنقاص العداد:", error);
}
} else {
alert("الرجاء الاتصال بـ MetaMask أولاً.");
}
};
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1>تطبيق العداد اللامركزي (DApp)</h1>
<p>القيمة الحالية للعداد: <strong>{counter}</strong></p>
{lastUpdater && <p>آخر تحديث بواسطة: <code>{lastUpdater}</code> في: {lastUpdateTimestamp}</p>}
<button onClick={handleIncrement} style={{ margin: '10px', padding: '10px 20px', fontSize: '16px', cursor: 'pointer' }}>
زيادة العداد
</button>
<button onClick={handleDecrement} style={{ margin: '10px', padding: '10px 20px', fontSize: '16px', cursor: 'pointer' }}>
إنقاص العداد
</button>
<p style={{ marginTop: '20px', fontSize: '0.9em', color: '#666' }}>
تأكد من وجود MetaMask متصل بشبكة عليها عقدك الذكي.
</p>
</div>
);
}
export default App;
الكود النهائي الكامل
عقد Solidity (EventExample.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EventExample {
uint public counter;
event CounterUpdated(
address indexed user,
uint newCounterValue,
uint timestamp
);
constructor() {
counter = 0;
}
function increment() public {
counter++;
emit CounterUpdated(msg.sender, counter, block.timestamp);
}
function decrement() public {
if (counter > 0) {
counter--;
emit CounterUpdated(msg.sender, counter, block.timestamp);
}
}
function getCurrentCounter() public view returns (uint) {
return counter;
}
}
تطبيق React (src/App.js)
تأكد من استبدال "0x..." بعنوان عقدك المنشور ووجود ملف EventExample.json (الخاص بالـ ABI) في نفس المجلد أو مسار صحيح.
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import EventExampleABI from './EventExample.json'; // تأكد من وجود هذا الملف
const contractAddress = "0x..."; // <== استبدل بعنوان عقدك المنشور هنا
function App() {
const [counter, setCounter] = useState(0);
const [lastUpdater, setLastUpdater] = useState('');
const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState('');
const [contract, setContract] = useState(null);
const [signer, setSigner] = useState(null);
useEffect(() => {
const connectWalletAndContract = async () => {
if (window.ethereum) {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.BrowserProvider(window.ethereum);
const _signer = await provider.getSigner();
setSigner(_signer);
const _contract = new ethers.Contract(contractAddress, EventExampleABI.abi, _signer);
setContract(_contract);
const initialCounter = await _contract.getCurrentCounter();
setCounter(initialCounter.toString());
_contract.on("CounterUpdated", (user, newCounterValue, timestamp, event) => {
console.log("حدث CounterUpdated تم إصداره:", { user, newCounterValue, timestamp: new Date(Number(timestamp) * 1000).toLocaleString() });
setCounter(newCounterValue.toString());
setLastUpdater(user);
setLastUpdateTimestamp(new Date(Number(timestamp) * 1000).toLocaleString());
});
console.log("تم الاتصال بالعقد بنجاح وتم بدء الاستماع للأحداث.");
} catch (error) {
console.error("خطأ في الاتصال بـ MetaMask أو العقد:", error);
}
} else {
console.error("MetaMask غير متاح. يرجى تثبيته.");
}
};
connectWalletAndContract();
return () => {
if (contract) {
contract.removeAllListeners("CounterUpdated");
console.log("تم إزالة مستمعي الأحداث.");
}
};
}, [contractAddress]);
const handleIncrement = async () => {
if (contract && signer) {
try {
const tx = await contract.increment();
await tx.wait();
console.log("تم إرسال معاملة الزيادة بنجاح.");
} catch (error) {
console.error("خطأ عند زيادة العداد:", error);
}
} else {
alert("الرجاء الاتصال بـ MetaMask أولاً.");
}
};
const handleDecrement = async () => {
if (contract && signer) {
try {
const tx = await contract.decrement();
await tx.wait();
console.log("تم إرسال معاملة الإنقاص بنجاح.");
} catch (error) {
console.error("خطأ عند إنقاص العداد:", error);
}
} else {
alert("الرجاء الاتصال بـ MetaMask أولاً.");
}
};
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1>تطبيق العداد اللامركزي (DApp)</h1>
<p>القيمة الحالية للعداد: <strong>{counter}</strong></p>
{lastUpdater && <p>آخر تحديث بواسطة: <code>{lastUpdater}</code> في: {lastUpdateTimestamp}</p>}
<button onClick={handleIncrement} style={{ margin: '10px', padding: '10px 20px', fontSize: '16px', cursor: 'pointer' }}>
زيادة العداد
</button>
<button onClick={handleDecrement} style={{ margin: '10px', padding: '10px 20px', fontSize: '16px', cursor: 'pointer' }}>
إنقاص العداد
</button>
<p style={{ marginTop: '20px', fontSize: '0.9em', color: '#666' }}>
تأكد من وجود MetaMask متصل بشبكة عليها عقدك الذكي.
</p>
</div>
);
}
export default App;
النتيجة المتوقعة
عند تشغيل تطبيق React وربطه بعقدك الذكي المنشور، ستلاحظ ما يلي:
- ستظهر قيمة العداد الأولية (0) على الشاشة.
- عند النقر على زر "زيادة العداد" أو "إنقاص العداد"، سيتم إرسال معاملة إلى العقد الذكي.
- بعد تأكيد المعاملة على البلوك تشين، سيقوم العقد بإصدار حدث
CounterUpdated. - سيقوم مستمع الأحداث في تطبيق React بالتقاط هذا الحدث وتحديث قيمة العداد المعروضة على الشاشة تلقائياً، بالإضافة إلى عرض عنوان المستخدم الذي أجرى التحديث والطابع الزمني.
- ستظهر رسائل في Console المتصفح تؤكد إصدار الحدث واستلامه.
هذا يوضح كيف توفر الأحداث آلية قوية وفعالة للتواصل أحادي الاتجاه من العقد الذكي إلى الواجهة الأمامية، مما يتيح بناء تطبيقات لامركزية سريعة الاستجابة وتفاعلية.