هندسة الويب اللامركزي (Web3.js & Ethers.js): كيف نربط الواجهات بالعقود الذكية؟
هندسة الويب اللامركزي (Web3.js & Ethers.js): كيف نربط الواجهات بالعقود الذكية؟
بناء تطبيق Web3 لا يكتمل عند كتابة العقد الذكي فقط، لأن القيمة الحقيقية تظهر عندما تتفاعل واجهة المستخدم مع البلوكتشين بشكل آمن وسلس. إذا كان العقد هو طبقة المنطق غير القابلة للتلاعب، فإن الواجهة هي نقطة الاتصال التي تسمح للمستخدم بإرسال المعاملات وقراءة البيانات وفهم حالة التطبيق لحظة بلحظة.
لهذا السبب تمثل مكتبتا Web3.js وEthers.js قلب هندسة التطبيقات اللامركزية الحديثة. فهما يوفران طبقة برمجية تصل المتصفح أو تطبيق React بالعقد المنشور على شبكة مثل Ethereum أو Polygon.
لفهم الصورة الكبرى من الجذور، يفيد الرجوع إلى مقال مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟. أما من جهة تجهيز المحفظة والاتصال بالشبكات، فالمسار العملي يبدأ غالباً من إعداد بيئة التطوير: تثبيت محفظة MetaMask والاتصال بشبكات الاختبار (Testnets).
ما الفرق العملي بين Web3.js وEthers.js؟
كلتا المكتبتين تؤديان الوظيفة الأساسية نفسها: قراءة بيانات العقد الذكي، إرسال المعاملات، متابعة الأحداث، وإدارة الاتصال بالمزوّد Provider. لكن الاختلاف يظهر في فلسفة التصميم وتجربة المطور.
Web3.jsأقدم انتشاراً، وارتبط مبكراً ببيئاتEthereumالكلاسيكية.Ethers.jsأخف وزناً وأكثر صرامة في البنية، ويستخدم كثيراً معHardhatوسكربتات النشر والاختبار الحديثة.- في المشاريع الجديدة، يميل كثير من المطورين إلى
Ethers.jsبسبب وضوح نماذجSignerوProviderوسهولة التعامل معABI.
إذا كنت تعمل ضمن مسار احترافي، فمن المفيد أن تكون قد مررت مسبقاً على الانتقال إلى بيئة العمل الاحترافية: تثبيت إطار عمل Hardhat باستخدام Node.js ثم إعداد مشروع Hardhat وكتابة أول سكربت JavaScript لترجمة (Compile) العقد الذكي.
المعمارية الأساسية لربط الواجهة بالعقد الذكي
أي تطبيق DApp يتكوّن عادة من أربع طبقات مترابطة:
- العقد الذكي المنشور على الشبكة.
- الواجهة الأمامية المكتوبة مثلاً بـ
HTMLأوReact. - مكتبة الربط مثل
Ethers.js. - المحفظة أو المزود مثل
MetaMaskالتي توقّع المعاملات.
الواجهة لا تتحدث مباشرة إلى البلوكتشين بل من خلال RPC Provider. وعندما يريد المستخدم تنفيذ دالة تغيّر حالة العقد، تنتقل العملية من الواجهة إلى المحفظة ثم إلى الشبكة ثم تعود النتيجة عبر transaction receipt.
ما الذي تحتاجه الواجهة كي تتعامل مع العقد؟
- عنوان العقد
Contract Address. - واجهة العقد الثنائية
ABI. - مزوّد قراءة أو توقيع مناسب.
- معرفة الفرق بين الدوال القارئة والدوال المعدّلة للحالة.
وهنا يصبح فهم الدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟ أمراً ضرورياً، مع أهمية إضافية لمقال أنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas.
مثال عقد ذكي بسيط للتكامل مع الواجهة
سنفترض عقداً بسيطاً يخزن عداداً ويمكن زيادته وقراءته من الواجهة. هذا النوع ممتاز لفهم دورة الربط من البداية إلى النهاية.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Counter {
uint256 public count;
event CountIncremented(address indexed user, uint256 newCount);
function increment() external {
count += 1;
emit CountIncremented(msg.sender, count);
}
function getCount() external view returns (uint256) {
return count;
}
}
هذا المثال يوضح بوضوح الفرق بين دالة تغيّر الحالة مثل increment ودالة قراءة مثل getCount. كما أن الحدث event يفيد في ربط الواجهة بالتغييرات الفعلية على السلسلة، وهو ما شرحناه بعمق في الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟.
ربط العقد باستخدام Ethers.js
في هذا الأسلوب نحتاج أولاً إلى التحقق من وجود المحفظة داخل المتصفح عبر الكائن window.ethereum. بعد ذلك ننشئ BrowserProvider ثم نستخرج signer ثم نبني كائن العقد.
const contractAddress = "0xYourContractAddress";
const abi = [
"function getCount() view returns (uint256)",
"function increment()",
"event CountIncremented(address indexed user, uint256 newCount)"
];
async function connectContract() {
if (!window.ethereum) throw new Error("MetaMask not found");
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
return contract;
}
async function readCount() {
const contract = await connectContract();
const count = await contract.getCount();
console.log(count.toString());
}
async function incrementCount() {
const contract = await connectContract();
const tx = await contract.increment();
await tx.wait();
console.log("Transaction confirmed");
}
رغم أن الكود أعلاه JavaScript وليس Solidity، فقد وضع داخل بنية الكود المطلوبة لضمان توافق العرض داخل المحرر. الفكرة الأساسية هنا أن القراءة لا تتطلب دائماً معاملة مدفوعة، بينما التعديل يتطلب توقيعاً وانتظار التأكيد على الشبكة.
ربط العقد باستخدام Web3.js
السيناريو نفسه يمكن تنفيذه عبر Web3.js. ستلاحظ اختلافاً في الصياغة، لكن المفهوم واحد: إنشاء كائن يمثل العقد ثم استدعاء methods المناسبة.
const web3 = new Web3(window.ethereum);
await window.ethereum.request({ method: "eth_requestAccounts" });
const contract = new web3.eth.Contract(abi, contractAddress);
async function readCountWeb3() {
const count = await contract.methods.getCount().call();
console.log(count);
}
async function incrementCountWeb3() {
const accounts = await web3.eth.getAccounts();
await contract.methods.increment().send({ from: accounts[0] });
console.log("Transaction sent");
}
من الناحية العملية، يعتمد الاختيار بين المكتبتين على بنية المشروع والفريق. لكن في التطبيقات الحديثة المتكاملة مع اختبارات ونشر آلي، يظهر انسجام Ethers.js بوضوح مع بيئة Hardhat وعمليات أتمتة نشر العقود (Deployment): كتابة سكربت لرفع العقد إلى شبكة Ethereum و Polygon.
التعامل مع الأحداث وتحديث الواجهة لحظياً
المستخدم لا يريد تحديث الصفحة يدوياً بعد كل معاملة. هنا تبرز قيمة Events التي تسمح للواجهة بالاستماع إلى ما يحدث على السلسلة ثم إعادة رسم الحالة المعروضة.
في Ethers.js يمكن ربط مستمع بالحدث بحيث يتم تحديث قيمة العداد بمجرد تأكيد المعاملة. هذا النمط مفيد جداً في تطبيقات DeFi، لوحات NFT، ومنصات التصويت.
لا تعتمد على تحديث الواجهة بناءً على نجاح الضغط على الزر فقط. يجب انتظار تأكيد المعاملة عبر
tx.wait()أو الاستماع إلىevent logs، لأن بث المعاملة لا يعني بالضرورة تنفيذها النهائي على السلسلة.
أخطاء شائعة عند ربط الواجهة بالعقد الذكي
- استخدام
ABIقديم لا يطابق النسخة المنشورة. - تنفيذ الواجهة على شبكة مختلفة عن شبكة العقد.
- عدم معالجة رفض المستخدم للمعاملة من المحفظة.
- الخلط بين استدعاء
callللقراءة وsendأو المعاملة للتعديل. - عدم عرض الأخطاء القادمة من
requireللمستخدم بشكل مفهوم.
ولأن كثيراً من هذه الحالات يرتبط بمنطق الحماية الداخلي، يجدر مراجعة التعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert، إضافة إلى المعدلات (Modifiers): حماية الدوال برمجياً.
عند بناء واجهة تتفاعل مع دوال مالية أو سحب أرصدة، لا تفترض أن العقد آمن لمجرد نجاح التكامل الأمامي. راجع مبادئ
Security Auditingواطلع على أمن العقود الذكية (1): ثغرة إعادة الدخول (Reentrancy Attack) ثم الحماية من ثغرة Reentrancy باستخدام ReentrancyGuard.
تحسين تجربة المستخدم وتقليل استهلاك الغاز
الواجهة الذكية لا تكتفي بإرسال المعاملات، بل تقلل أيضاً عدد المعاملات غير الضرورية. من الأفضل دائماً قراءة البيانات أولاً وتجنب أي تنفيذ مكلف ما لم يكن التغيير مطلوباً فعلاً. كما يجب إظهار تقدير تقريبي للرسوم قبل توقيع المعاملة.
لتقليل
Gas Fees، صمم الواجهة بحيث تعتمد على الدوالviewقدر الإمكان، وتمنع المستخدم من تكرار إرسال المعاملة نفسها. لفهم جانب الرسوم بدقة راجع التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟.
خاتمة
ربط الواجهة بالعقد الذكي ليس مجرد استيراد مكتبة واستدعاء دالة، بل هو هندسة متكاملة تجمع بين فهم ABI، إدارة المحافظ، التحقق من الشبكة، معالجة الأخطاء، متابعة الأحداث، وتحسين التكلفة والأمان. هنا تحديداً يظهر الفرق بين تطبيق تجريبي بسيط ومنتج Web3 جاهز للاستخدام الحقيقي.
سواء اخترت Web3.js أو Ethers.js، فإن النجاح الحقيقي يعتمد على فهم مسار المعاملة كاملاً من الزر في الواجهة حتى تأكيد الكتلة على الشبكة. وكلما كانت هذه الحلقة أوضح للمطور، أصبح بناء تطبيقات لامركزية موثوقة وقابلة للتوسع أكثر واقعية واحترافية.
20 comments