الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟
الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟
عند بناء تطبيق لامركزي، لا يكفي أن ينجح العقد الذكي في تعديل حالته الداخلية؛ بل يجب أيضاً أن تعرف واجهة المستخدم متى تم تنفيذ عملية ما، وما هي البيانات المرتبطة بها. هنا تظهر أهمية Events في Solidity، فهي الآلية القياسية التي يرسل بها العقد الذكي إشارات قابلة للقراءة من خارج السلسلة إلى الواجهات المبنية باستخدام React أو أي عميل Web3.
إذا كنت قد قرأت مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟، فأنت تعلم أن البلوكتشين لا يعمل مثل الخوادم التقليدية التي تدفع البيانات فوراً إلى الواجهة. بدلاً من ذلك، تسجل المعاملات وتنتج معها سجلات Logs يمكن التقاطها والاستماع إليها. هذه السجلات هي الجسر العملي بين منطق EVM وتجربة المستخدم في المتصفح.
ما هي Events داخل العقد الذكي؟
Events هي بنية خاصة في Solidity تُستخدم لتسجيل بيانات معينة داخل سجلات المعاملة. عند تنفيذ دالة تغيّر حالة العقد، يمكن استدعاء emit لإطلاق حدث يصف ما جرى: من استدعى العملية، ما القيمة الجديدة، وما هو التوقيت البلوكتشيني المرتبط بها.
هذه الأحداث لا تخزن ضمن storage مثل أساسيات لغة Solidity: أنواع البيانات والمتغيرات (State Variables)، لكنها تبقى قابلة للفهرسة والقراءة من خلال مزودي الشبكة مثل RPC Providers. لذلك فهي مناسبة جداً للتنبيه، التتبع، وبناء سجل نشاط للمستخدم.
لماذا تحتاج واجهة React إلى الأحداث بدلاً من الاستعلام المستمر؟
يمكن نظرياً أن تقوم الواجهة باستدعاء دوال قراءة من العقد كل بضع ثوانٍ. لكن هذا الأسلوب غير فعال، ويزيد عدد الطلبات، ويعقّد مزامنة الواجهة مع حالة السلسلة. أما الأحداث فتوفر نموذجاً أقرب إلى publish/subscribe، حيث تستمع الواجهة إلى نوع حدث معين، ثم تتفاعل عند وصوله.
الفائدة العملية هنا كبيرة:
- تحديث الواجهة فور تأكيد المعاملة.
- إظهار إشعارات دقيقة للمستخدم.
- بناء سجل نشاط مثل عمليات الإيداع أو الشراء.
- تقليل الحاجة إلى
Pollingالمستمر.
بنية الحدث داخل Solidity
يشبه تعريف الحدث توقيع الدوال، لكنه لا يعيد قيماً مباشرة. يمكن تمرير أنواع بيانات عديدة داخله مثل address وuint256. كما يمكن تعليم بعض الحقول بكلمة indexed لتسهيل فلترتها من خارج العقد.
pragma solidity ^0.8.20;
contract Counter {
uint256 public count;
event CountUpdated(address indexed user, uint256 newCount, uint256 timestamp);
function increment() external {
count += 1;
emit CountUpdated(msg.sender, count, block.timestamp);
}
}
في المثال السابق، كلما تم استدعاء الدالة increment، يتم تعديل المتغير ثم إطلاق الحدث CountUpdated. يمكن لواجهة React التقاط هذا الحدث لتحديث العدد المعروض دون الحاجة إلى إعادة تحميل الصفحة.
ماذا تعني indexed؟
عند تعليم متغير داخل الحدث بصفة indexed، يصبح قابلاً للفهرسة داخل موضوعات السجل topics. هذا يسمح للواجهة أو أدوات التحليل بجلب الأحداث الخاصة بعنوان معين فقط، مثل كل العمليات التي نفذها مستخدم محدد.
استخدم
indexedفقط للحقول التي تحتاج فعلاً إلى فلترتها خارجياً. الإفراط في تصميم الأحداث دون حاجة عملية قد يزيد التعقيد ويؤثر في كفاءة تتبع البيانات وتحليلها.
كيف تلتقط React هذه الأحداث باستخدام Ethers.js؟
بعد نشر العقد بواسطة Remix أو Hardhat، تحتاج الواجهة إلى ثلاثة عناصر: عنوان العقد، ملف ABI، ومزوّد اتصال بالشبكة. إذا كنت قد أعددت محفظتك سابقاً عبر إعداد بيئة التطوير: تثبيت محفظة MetaMask والاتصال بشبكات الاختبار (Testnets) فهذه الخطوة ستكون مباشرة.
// React + ethers.js example concept
// This snippet is intentionally shown inside Solidity code block per formatting rule.
وفي التطبيق الفعلي، يكون منطق الاستماع كالتالي:
- إنشاء كائن
providerباستخدامwindow.ethereum. - إنشاء نسخة من
ContractعبرABIوالعنوان. - استخدام
contract.onللاشتراك في الحدث. - عند وصول الحدث، تحديث
stateداخل الواجهة. - عند مغادرة المكوّن، إزالة المستمع عبر
removeListenerأوoff.
هذا النمط يجعل المكوّن الأمامي يستجيب مباشرة للأنشطة على الشبكة، خصوصاً في تطبيقات التصويت، لوحات التحكم، الأسواق، وأنظمة الاشتراكات.
الفرق بين قراءة الحالة وإطلاق حدث
دوال القراءة مثل view وpure شرحناها سابقاً في أنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas. هذه الدوال ممتازة لجلب الحالة الحالية، لكنها لا تخبرك متى تغيّرت تلك الحالة. أما Events فتركز على “متى” و”كيف” حدث التغيير.
لذلك، التصميم الاحترافي يجمع بين الاثنين:
- دوال قراءة للحصول على الحالة الحالية.
- أحداث لتتبع التغييرات الزمنية على تلك الحالة.
استخدامات عملية للأحداث في تطبيقات DApps
كلما زاد تعقيد العقد، زادت أهمية الأحداث. مثلاً، إذا كنت تستخدم القواميس (Mappings): أسرع طريقة لربط عناوين المحافظ بأرصدتها (Key-Value) لتخزين الأرصدة، فالواجهة لن تعرف تلقائياً متى تغيّر رصيد مستخدم معين. يجب أن تطلق حدثاً يصف عملية الإيداع أو السحب.
وبالمثل، عند التعامل مع الهياكل (Structs): تصميم أنواع بيانات مخصصة (مثل: كائن يمثل موظف أو منتج) أو المصفوفات (Arrays) في Solidity: تخزين وإدارة قوائم البيانات داخل العقد الذكي، يكون من المفيد إطلاق أحداث عند الإضافة أو التعديل حتى تتمكن الواجهة من تحديث العناصر الظاهرة للمستخدم بدقة.
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public balances;
event Deposited(address indexed user, uint256 amount, uint256 newBalance);
event Withdrawn(address indexed user, uint256 amount, uint256 newBalance);
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value, balances[msg.sender]);
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawn(msg.sender, amount, balances[msg.sender]);
}
}
اعتبارات الأمان وتحسين الغاز عند تصميم الأحداث
الأحداث ليست بديلاً عن التخزين الداخلي للعقد، ولا يجب الاعتماد عليها وحدها كمصدر للحقيقة Source of Truth. السجل ممتاز للمراقبة الخارجية، لكن منطق العقد نفسه يجب أن يعتمد على متغيرات الحالة والدوال المنضبطة كما شرحنا في الدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟.
أمنياً، لا تفترض أن إطلاق الحدث يعني نجاح التجربة من منظور الواجهة قبل تأكيد المعاملة على الشبكة. يجب انتظار التأكيد
confirmationومعالجة حالات الفشل أو الإلغاء من المستخدم.
من ناحية التكلفة، تذكّر أن الأحداث ليست مجانية تماماً. صحيح أنها غالباً أوفر من تخزين بيانات إضافية داخل
storage، لكن كل بيانات إضافية في السجل تؤثر على رسوم التنفيذ. راجع دائماً مبادئ التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟ عند تصميم الأحداث.
كيف تختبر الأحداث أثناء التطوير؟
أثناء العمل باستخدام Remix أو Hardhat، يمكنك تنفيذ دوال العقد ومراجعة السجلات الناتجة داخل تفاصيل المعاملة. وللتجارب على الشبكات العامة ستحتاج غالباً إلى رصيد اختباري من الحصول على عملات تجريبية مجانية (Faucet) للبدء في نشر واختبار العقود الذكية.
أفضل منهج عملي يتضمن:
- التحقق من أن الحدث يُطلق بالقيم الصحيحة.
- التأكد من أن الواجهة تستقبل الحدث مرة واحدة فقط.
- فحص سلوك إعادة التحميل أو تغيير الحساب في المحفظة.
- اختبار إزالة المستمعين لتفادي تسرب الذاكرة في
React.
خلاصة
تشكل Events جزءاً أساسياً من هندسة التطبيقات اللامركزية الحديثة، لأنها تمنح العقود الذكية وسيلة واضحة لإخبار الواجهة بأن عملية معينة قد وقعت بالفعل. وبدلاً من جعل React يبحث باستمرار عن تغييرات الحالة، يمكن للعقد إرسال إشارات دقيقة وقابلة للفهرسة، مما يحسن الأداء وتجربة المستخدم والتنظيم البرمجي.
كلما أتقنت تعريف الأحداث، واختيار الحقول indexed بعناية، وربطها منطقياً بالواجهة عبر Ethers.js، ستبني DApp أكثر احترافية وموثوقية وقابلية للتوسع.
33 comments