الدليل الشامل لتطوير تطبيقات إيثريوم المتكاملة باستخدام React وHardhat وEthers.js

دقائق القراءة: 11

مقدمة إلى تطوير تطبيقات إيثريوم المتكاملة

أصبح تطوير التطبيقات اللامركزية على شبكة Ethereum أكثر نضجاً بفضل توفر أدوات حديثة تختصر وقت البناء والاختبار والنشر. وإذا كنت ترغب في إنشاء تطبيق dApp متكامل، فأنت تحتاج إلى فهم الطبقات الأساسية التي تربط بين الواجهة الأمامية، والعقود الذكية، ومحفظة المستخدم، وبيئة التطوير المحلية، وآليات قراءة البيانات من الشبكة.

في هذا الدليل العملي، ستتعلم كيفية بناء تطبيقات Full Stack Ethereum باستخدام React وEthers.js وSolidity وHardhat. كما سنستعرض طريقة إنشاء العقود الذكية، وتجميعها، ونشرها محلياً وعلى شبكة اختبار، ثم ربطها بواجهة أمامية بسيطة للتفاعل معها بشكل مباشر.

دليل تطوير تطبيقات إيثريوم المتكاملة باستخدام React وHardhat وEthers.js

تكمن أهمية هذا الأسلوب في أنه يمنحك تصوراً كاملاً لكيفية عمل مكونات النظام معاً، بدلاً من تعلم كل أداة على حدة دون معرفة كيفية دمجها داخل مشروع حقيقي.

لماذا تحتاج إلى حزمة أدوات متكاملة لتطوير تطبيقات البلوك تشين؟

رغم وجود توثيق جيد لمعظم أدوات تطوير Ethereum بشكل منفصل، فإن التحدي الحقيقي يظهر عند محاولة ربط هذه الأدوات ضمن مشروع واحد. فالمطور يحتاج عادة إلى:

  • إنشاء العقود الذكية واختبارها محلياً.
  • نشر العقود على شبكة محلية أو شبكة اختبار أو الشبكة الرئيسية.
  • ربط الواجهة الأمامية بالعقود الذكية.
  • إدارة المحافظ وتوقيع المعاملات.
  • قراءة البيانات بكفاءة من البلوك تشين.

لهذا السبب، فإن اختيار حزمة تقنية واضحة ومترابطة يختصر كثيراً من الوقت ويقلل الأخطاء أثناء التطوير.

التقنيات الأساسية المستخدمة في المشروع

1. بيئة تطوير إيثريوم باستخدام Hardhat

تُعد Hardhat واحدة من أفضل بيئات تطوير العقود الذكية في الوقت الحالي. فهي توفر للمطور إمكانات متعددة، مثل:

  • تجميع ملفات Solidity.
  • تشغيل شبكة محلية للاختبار.
  • تنفيذ اختبارات آلية.
  • نشر العقود بسهولة.
  • إنتاج ملفات ABI اللازمة لربط العقود بالتطبيقات الأمامية.

وتتميز Hardhat بأنها مناسبة جداً لمشاريع Full Stack مقارنة بأدوات أخرى مثل Ganache وTruffle.

2. مكتبة التفاعل مع البلوك تشين Ethers.js

عند بناء واجهة أمامية باستخدام React، ستحتاج إلى مكتبة تسمح لك بقراءة بيانات العقود الذكية وإرسال المعاملات. هنا تظهر أهمية Ethers.js، إذ توفر واجهة برمجية خفيفة ومرنة للتعامل مع شبكة Ethereum من داخل تطبيقات JavaScript.

باستخدام Ethers.js يمكنك:

  • إنشاء Provider للاتصال بالشبكة.
  • إنشاء Signer لتوقيع المعاملات.
  • استدعاء دوال القراءة من العقود الذكية.
  • إرسال دوال الكتابة التي تتطلب معاملة وتكلفة Gas.

3. محفظة MetaMask

تلعب MetaMask دور الوسيط بين المستخدم والتطبيق اللامركزي. فهي تسمح بإدارة الحسابات والمفاتيح الخاصة، وتتيح للمستخدم توقيع المعاملات بأمان دون كشف بياناته الحساسة داخل التطبيق.

وبعد ربط المحفظة، يستطيع المطور التعامل مع الواجهة العامة المتاحة في المتصفح عبر window.ethereum، وهي البوابة الأساسية لطلب الاتصال بحساب المستخدم وتنفيذ التوقيعات.

4. الواجهة الأمامية باستخدام React

تُعد React خياراً ممتازاً لبناء واجهات التطبيقات اللامركزية، نظراً لمرونتها، وانتشارها الكبير، وسهولة دمجها مع الأدوات الحديثة. كما أنها تشكل أساساً قوياً يمكن تطويره لاحقاً باستخدام أطر مثل Next.js أو Gatsby.

5. بروتوكول The Graph

القراءة المباشرة من البلوك تشين قد تكون بطيئة ومكلفة من ناحية الأداء، خصوصاً عند التعامل مع تطبيقات كبيرة. هنا يأتي دور The Graph الذي يعمل كبروتوكول فهرسة يتيح الاستعلام عن بيانات البلوك تشين عبر طبقة GraphQL مرنة.

لن نقوم في هذا الدليل ببناء Subgraph، لكن من المهم معرفة أن هذه الأداة حيوية عند تطوير تطبيقات لامركزية تحتاج إلى استعلامات متقدمة وسريعة.

ما الذي سنقوم ببنائه؟

سنقوم بإنشاء مشروع عملي يتضمن عقدين ذكيين أساسيين:

  • عقد لتخزين رسالة وقراءتها وتحديثها على شبكة Ethereum.
  • عقد لتوليد رموز رقمية بسيطة وإرسالها بين الحسابات.

كما سنبني واجهة أمامية باستخدام React تسمح للمستخدم بما يلي:

  • قراءة الرسالة الحالية من العقد.
  • تحديث الرسالة عبر معاملة موقعة.
  • قراءة رصيد التوكنات.
  • إرسال التوكنات من حساب إلى آخر.

المتطلبات المسبقة قبل البدء

  • تثبيت Node.js على الجهاز.
  • تثبيت إضافة MetaMask في المتصفح.

لن تحتاج إلى امتلاك عملات Ether حقيقية، لأن هذا الشرح يعتمد على شبكة محلية وشبكات اختبار تستخدم أرصدة تجريبية.

بدء المشروع باستخدام create-react-app

ابدأ بإنشاء تطبيق React جديد عبر الأمر التالي:

npx create-react-app react-dapp

بعد ذلك، انتقل إلى مجلد المشروع وثبّت الحزم المطلوبة:

npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

إعداد بيئة تطوير Ethereum باستخدام Hardhat

قم بتهيئة مشروع Hardhat بالأمر التالي:

npx hardhat

ثم اختر إنشاء مشروع تجريبي. بعد ذلك ستلاحظ إنشاء مجموعة من الملفات والمجلدات المهمة:

  • hardhat.config.js لإعدادات المشروع.
  • scripts لملفات النشر.
  • test للاختبارات.
  • contracts للعقود الذكية.

بسبب مشكلة تتعلق بإعداد MetaMask، من الأفضل ضبط قيمة chainId إلى 1337، مع تغيير مسار ملفات artifacts إلى داخل مجلد src حتى يسهل استيرادها في تطبيق React.

module.exports = {
  solidity: "0.8.3",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {
      chainId: 1337
    }
  }
};

فهم العقد الذكي الأول Greeter

العقد الأول بسيط للغاية، ووظيفته تخزين رسالة نصية وإتاحتها للقراءة أو التعديل.

//SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

import "hardhat/console.sol";

contract Greeter {
    string greeting;

    constructor(string memory _greeting) {
        console.log("Deploying a Greeter with greeting:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }
}

بما أننا ضبطنا إصدار المترجم في ملف hardhat.config.js على 0.8.3، فمن الأفضل توحيد الإصدار داخل العقد أيضاً:

// contracts/Greeter.sol
pragma solidity ^0.8.3;

الفرق بين القراءة والكتابة على البلوك تشين

التعامل مع العقد الذكي يتم بطريقتين:

  • القراءة: مثل استدعاء الدالة greet()، وهي عملية مجانية لا تحتاج إلى Gas لأنها لا تغيّر حالة الشبكة.
  • الكتابة: مثل استدعاء الدالة setGreeting()، وهي تتطلب معاملة موقعة ورسوم Gas لأنها تعدّل البيانات على البلوك تشين.

هذه النقطة جوهرية لفهم كيفية تصميم التطبيق: ما يُقرأ يمكن جلبه مباشرة، وما يُكتب يحتاج إلى محفظة وتوقيع وموافقة من المستخدم.

ما هو ملف ABI ولماذا هو مهم؟

يرمز ABI إلى Application Binary Interface، وهو بمثابة الواجهة التي يفهم من خلالها التطبيق الأمامي كيفية التفاعل مع العقد الذكي. يحتوي هذا الملف على وصف للدوال والمتغيرات والأحداث التي يتيحها العقد.

بعد تجميع العقود، ستتمكن من استيراد ملف ABI داخل تطبيق React واستخدامه مع عنوان العقد لإنشاء كائن يمكنه تنفيذ الاستدعاءات.

تجميع العقد واستخراج ABI

لتجميع المشروع، نفذ الأمر التالي:

npx hardhat compile

بعد ذلك ستجد مجلد artifacts داخل src، ويمكنك استيراد العقد بهذه الطريقة:

import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

ولعرض ABI في وحدة التحكم:

console.log("Greeter ABI: ", Greeter.abi)

تشغيل شبكة محلية ونشر العقد الذكي

قبل النشر محلياً، يجب تشغيل عقدة اختبار محلية:

npx hardhat node

عند تشغيل هذا الأمر، سيعرض لك Hardhat مجموعة من الحسابات التجريبية مع المفاتيح الخاصة الخاصة بها، وكل حساب يحتوي على رصيد تجريبي كبير من ETH.

حسابات تجريبية ومفاتيح خاصة من شبكة Hardhat المحلية

بعد ذلك، غيّر اسم الملف scripts/sample-script.js إلى scripts/deploy.js، ثم شغّل أمر النشر التالي:

npx hardhat run scripts/deploy.js --network localhost

بعد نجاح النشر، سيظهر عنوان العقد، وهو العنوان الذي ستستخدمه لاحقاً في الواجهة الأمامية.

Greeter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

استيراد الحساب المحلي إلى MetaMask

لإرسال المعاملات من التطبيق، تحتاج إلى استخدام أحد الحسابات المحلية داخل MetaMask.

  1. افتح MetaMask وغيّر الشبكة إلى Localhost 8545.
  2. اختر Import Account من قائمة الحسابات.
  3. انسخ أحد المفاتيح الخاصة الظاهرة في نافذة Hardhat والصقها في MetaMask.
  4. بعد الاستيراد، سيظهر الرصيد التجريبي في المحفظة.

اختيار شبكة Localhost 8545 في MetaMaskاستيراد حساب جديد داخل محفظة MetaMaskظهور الرصيد التجريبي بعد استيراد الحساب في MetaMask

ربط تطبيق React بالعقد الذكي

الهدف من الواجهة الأمامية هنا ليس التصميم الجمالي، بل إثبات آلية الاتصال بالعقد الذكي وتنفيذ عمليتي القراءة والكتابة.

سنحتاج إلى:

  • حقل إدخال لتحديث الرسالة.
  • حالة محلية لإدارة قيمة الإدخال.
  • طلب إذن الاتصال بحساب المستخدم عبر MetaMask.
  • دوال لقراءة الرسالة من العقد وتحديثها.

يمكنك تحديث ملف src/App.js بالشيفرة التالية، مع استبدال قيمة greeterAddress بعنوان العقد الحقيقي:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

const greeterAddress = "your-contract-address"

function App() {
  const [greeting, setGreetingValue] = useState()

  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum)
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
      try {
        const data = await contract.greet()
        console.log('data: ', data)
      } catch (err) {
        console.log("Error: ", err)
      }
    }
  }

  async function setGreeting() {
    if (!greeting) return
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner()
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
      const transaction = await contract.setGreeting(greeting)
      await transaction.wait()
      fetchGreeting()
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={fetchGreeting}>Fetch Greeting</button>
        <button onClick={setGreeting}>Set Greeting</button>
        <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />
      </header>
    </div>
  );
}

export default App;

بعد ذلك شغّل التطبيق:

npm start

عند فتح التطبيق، يمكنك قراءة الرسالة الحالية وتحديثها بعد توقيع المعاملة من MetaMask.

واجهة React بسيطة لقراءة الرسالة من العقد الذكي وتحديثها

النشر على شبكة اختبار عامة

بعد التأكد من نجاح التجربة محلياً، يمكنك نشر العقد على شبكة اختبار عامة مثل Ropsten أو أي شبكة اختبار حديثة مدعومة من مزود الخدمة الذي تستخدمه.

ابدأ بتبديل الشبكة في MetaMask إلى شبكة الاختبار المناسبة، ثم احصل على Test Ether من خدمة Faucet.

تغيير شبكة MetaMask إلى شبكة اختبار إيثريوم

بعد ذلك، أنشئ حساباً في Infura أو Alchemy للحصول على نقطة اتصال RPC تشبه هذا المثال:

https://ropsten.infura.io/v3/your-project-id

ثم حدّث إعدادات الشبكات في ملف hardhat.config.js:

module.exports = {
  defaultNetwork: "hardhat",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {},
    ropsten: {
      url: "https://ropsten.infura.io/v3/your-project-id",
      accounts: [`0x${your-private-key}`]
    }
  },
  solidity: "0.7.3",
};

ثم نفّذ أمر النشر:

npx hardhat run scripts/deploy.js --network ropsten

وبعد اكتمال العملية، يمكنك عرض العقد على مستكشف الشبكة مثل Etherscan.

تصدير المفتاح الخاص من MetaMask لاستخدامه في النشر على شبكة الاختبار

إنشاء عقد توكن بسيط على Ethereum

من أكثر الاستخدامات شيوعاً للعقود الذكية إنشاء التوكنات. سننشئ الآن عقداً مبسطاً باسم Token.sol:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
import "hardhat/console.sol";

contract Token {
  string public name = "Nader Dabit Token";
  string public symbol = "NDT";
  uint public totalSupply = 1000000;
  mapping(address => uint) balances;

  constructor() {
    balances[msg.sender] = totalSupply;
  }

  function transfer(address to, uint amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");
    balances[msg.sender] -= amount;
    balances[to] += amount;
  }

  function balanceOf(address account) external view returns (uint) {
    return balances[account];
  }
}

هذا العقد مخصص للتجربة فقط، وهو ليس متوافقاً بالكامل مع معيار ERC20. لكنه مناسب لفهم المبدأ الأساسي لتحويل الأرصدة وقراءة الرصيد.

بعد إنشاء الملف، قم بتجميعه:

npx hardhat compile

تحديث سكربت النشر لنشر عقدين معاً

يمكنك تعديل ملف scripts/deploy.js ليشمل عقد الرسائل وعقد التوكن:

const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();
  console.log("Deploying contracts with the account:", deployer.address);

  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, World!");

  const Token = await hre.ethers.getContractFactory("Token");
  const token = await Token.deploy();

  await greeter.deployed();
  await token.deployed();

  console.log("Greeter deployed to:", greeter.address);
  console.log("Token deployed to:", token.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

ثم نفّذ النشر محلياً:

npx hardhat run scripts/deploy.js --network localhost

تحديث واجهة React لدعم التوكنات

بعد نشر عقد التوكن، حدّث الواجهة الأمامية لتتمكن من قراءة الرصيد وإرسال التوكنات:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'
import Token from './artifacts/contracts/Token.sol/Token.json'

const greeterAddress = "your-contract-address"
const tokenAddress = "your-contract-address"

function App() {
  const [greeting, setGreetingValue] = useState()
  const [userAccount, setUserAccount] = useState()
  const [amount, setAmount] = useState()

  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum)
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
      try {
        const data = await contract.greet()
        console.log('data: ', data)
      } catch (err) {
        console.log("Error: ", err)
      }
    }
  }

  async function getBalance() {
    if (typeof window.ethereum !== 'undefined') {
      const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' })
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const contract = new ethers.Contract(tokenAddress, Token.abi, provider)
      const balance = await contract.balanceOf(account);
      console.log("Balance: ", balance.toString());
    }
  }

  async function setGreeting() {
    if (!greeting) return
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner()
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
      const transaction = await contract.setGreeting(greeting)
      await transaction.wait()
      fetchGreeting()
    }
  }

  async function sendCoins() {
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      const contract = new ethers.Contract(tokenAddress, Token.abi, signer);
      const transation = await contract.transfer(userAccount, amount);
      await transation.wait();
      console.log(`${amount} Coins successfully sent to ${userAccount}`);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={fetchGreeting}>Fetch Greeting</button>
        <button onClick={setGreeting}>Set Greeting</button>
        <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />
        <br />
        <button onClick={getBalance}>Get Balance</button>
        <button onClick={sendCoins}>Send Coins</button>
        <input onChange={e => setUserAccount(e.target.value)} placeholder="Account ID" />
        <input onChange={e => setAmount(e.target.value)} placeholder="Amount" />
      </header>
    </div>
  );
}

export default App;

عند تشغيل التطبيق بالأمر التالي:

npm start

ستتمكن من قراءة الرصيد وإرسال العملات التجريبية بين الحسابات.

إضافة التوكن إلى محفظة MetaMask

يمكنك أيضاً عرض التوكن داخل MetaMask عبر إضافة عنوان العقد يدوياً:

  1. افتح المحفظة واضغط على Add Token.
  2. اختر Custom Token.
  3. ألصق عنوان عقد التوكن.
  4. أكد الإضافة لعرض الرصيد داخل المحفظة.

إضافة توكن مخصص إلى محفظة MetaMaskظهور التوكن المخصص داخل محفظة MetaMask بعد إضافته

الانتقال إلى معيار ERC20 الاحترافي

العقد السابق مفيد للتعلم، لكنه لا يحقق التوافق الكامل مع النظام البيئي لتوكنات Ethereum. لذلك يُنصح عملياً باستخدام معيار ERC20 عبر مكتبات موثوقة مثل OpenZeppelin.

ابدأ بتثبيت المكتبة:

npm install @openzeppelin/contracts

ثم أنشئ عقداً جديداً يرث من ERC20:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NDToken is ERC20 {
  constructor(string memory name, string memory symbol) ERC20(name, symbol) {
    _mint(msg.sender, 100000 * (10 ** 18));
  }
}

يتيح لك هذا العقد إنشاء توكن باسم ورمز قابلين للتخصيص، مع سك كمية أولية من التوكنات. وبشكل افتراضي، يستخدم معيار ERC20 عدد منازل عشرية يساوي 18.

ولنشر العقد، مرر القيم المطلوبة في سكربت النشر:

const NDToken = await hre.ethers.getContractFactory("NDToken");
const ndToken = await NDToken.deploy("Nader Dabit Token", "NDT");

أهم الدوال التي يوفرها معيار ERC20

عند الاعتماد على معيار ERC20، يرث عقدك مجموعة مهمة من الدوال الجاهزة:

function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)

هذه الدوال تجعل توكنك قابلاً للتكامل مع المحافظ، والمنصات، والعقود الأخرى في شبكة Ethereum دون الحاجة إلى إعادة اختراع الأساسيات.

نصائح عملية لرفع جودة المشروع تقنياً

  • استخدم المتغيرات البيئية بدلاً من تخزين المفاتيح الخاصة مباشرة داخل الملفات.
  • افصل منطق الواجهة عن منطق الاتصال بالشبكة لتسهيل الصيانة.
  • تحقق من الأخطاء ورسائل الرفض القادمة من MetaMask.
  • اختبر العقود محلياً قبل أي نشر عام.
  • اعتمد مكتبات موثوقة مثل OpenZeppelin عند بناء العقود الإنتاجية.

مقارنة سريعة بين مكونات الحزمة التقنية

المكوّن الدور الأساسي الفائدة العملية
Hardhat بيئة تطوير وتجميع ونشر تبسيط دورة تطوير العقود الذكية
Ethers.js التفاعل مع العقود والشبكة تنفيذ القراءة والكتابة من الواجهة الأمامية
MetaMask إدارة الحسابات وتوقيع المعاملات ربط المستخدم بالتطبيق اللامركزي
React بناء الواجهة الأمامية تقديم تجربة استخدام مرنة وسريعة
The Graph فهرسة واستعلام بيانات البلوك تشين تسريع الوصول إلى البيانات المعقدة

الخلاصة التقنية

يمثل هذا المسار نقطة انطلاق قوية لأي مطور يرغب في دخول عالم تطبيقات Ethereum المتكاملة بطريقة عملية ومنظمة. الجمع بين Hardhat وEthers.js وReact وMetaMask يوفر بنية متوازنة تجمع بين سهولة التطوير ووضوح سير العمل. وإذا أضفت لاحقاً معايير مثل ERC20 وأدوات فهرسة مثل The Graph، فستصبح قادراً على بناء تطبيقات لامركزية أقرب إلى مستوى الإنتاج الحقيقي، لا مجرد نماذج تجريبية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *